feat: support genre and year (closes #1503) (#1509)

This commit is contained in:
Phan An 2022-09-23 13:21:29 +07:00 committed by GitHub
parent 4eab978bb0
commit e068a4ca8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 813 additions and 85 deletions

View file

@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Models\Song;
use Illuminate\Console\Command;
class CollectTagsCommand extends Command
{
protected $signature = 'koel:tags:collect {tag*}';
protected $description = 'Collect additional tags from existing songs';
private const ALL_TAGS = [
'title',
'album',
'artist',
'albumartist',
'track',
'disc',
'year',
'genre',
'lyrics',
'cover',
];
private const COLLECTABLE_TAGS = ['year', 'genre'];
public function handle(): int
{
$tags = collect($this->argument('tag'))->unique();
if ($tags->diff(self::COLLECTABLE_TAGS)->isNotEmpty()) {
$this->error(
sprintf(
'Invalid tag(s): %s. Allowed tags are: %s.',
$tags->diff(self::COLLECTABLE_TAGS)->join(', '),
implode(', ', self::COLLECTABLE_TAGS)
)
);
return self::FAILURE;
}
Song::withoutSyncingToSearch(function () use ($tags): void {
$this->call('koel:sync', [
'--force' => true,
'--ignore' => collect(self::ALL_TAGS)->diff($tags)->all(),
]);
});
return self::SUCCESS;
}
}

View file

@ -15,7 +15,7 @@ class SyncCommand extends Command
{
protected $signature = 'koel:sync
{record? : A single watch record. Consult Wiki for more info.}
{--ignore= : The comma-separated tags to ignore (exclude) from syncing}
{--ignore=* : The comma-separated tags to ignore (exclude) from syncing}
{--force : Force re-syncing even unchanged files}';
protected $description = 'Sync songs found in configured directory against the database.';
@ -59,7 +59,11 @@ class SyncCommand extends Command
// The tags to ignore from syncing.
// Notice that this is only meaningful for existing records.
// New records will have every applicable field synced in.
$ignores = $this->option('ignore') ? explode(',', $this->option('ignore')) : [];
$ignores = collect($this->option('ignore'))->sort()->values()->all();
if ($ignores) {
$this->components->info('Ignoring tag(s): ' . implode(', ', $ignores));
}
$results = $this->mediaSyncService->sync($ignores, $this->option('force'));

View file

@ -32,6 +32,8 @@ class SongResource extends JsonResource
'play_count' => (int) $this->song->play_count,
'track' => $this->song->track,
'disc' => $this->song->disc,
'genre' => $this->song->genre,
'year' => $this->song->year,
'created_at' => $this->song->created_at,
];
}

View file

@ -23,6 +23,8 @@ use Laravel\Scout\Searchable;
* @property int $track
* @property int $disc
* @property int $album_id
* @property int|null $year
* @property string $genre
* @property string $id
* @property int $artist_id
* @property int $mtime

View file

@ -90,7 +90,7 @@ class FileSynchronizer
$albumArtist = Arr::get($info, 'albumartist') ? Artist::getOrCreate($info['albumartist']) : $artist;
$album = Arr::get($info, 'album') ? Album::getOrCreate($albumArtist, $info['album']) : $this->song->album;
if (!$album->has_cover) {
if (!in_array('cover', $ignores, true) && !$album->has_cover) {
$this->tryGenerateAlbumCover($album, Arr::get($info, 'cover', []));
}

View file

@ -76,9 +76,13 @@ class SongService
$maybeSetAlbum();
}
$song->title = $data->title ?? $song->title; // Empty string still has effects
$song->lyrics = $data->lyrics ?? $song->lyrics; // Empty string still has effects
// For string attributes like title, lyrics, and genre, we use "??" because empty strings still have effects
$song->title = $data->title ?? $song->title;
$song->lyrics = $data->lyrics ?? $song->lyrics;
$song->genre = $data->genre ?? $song->genre;
$song->track = $data->track ?: $song->track;
$song->year = $data->year ?: $song->year;
$song->disc = $data->disc ?: $song->disc;
$song->push();

View file

@ -42,6 +42,8 @@ final class SmartPlaylistRule implements Arrayable
private const MODEL_LENGTH = 'length';
private const MODEL_DATE_ADDED = 'created_at';
private const MODEL_DATE_MODIFIED = 'updated_at';
private const MODEL_GENRE = 'genre';
private const MODEL_YEAR = 'year';
private const VALID_MODELS = [
self::MODEL_TITLE,
@ -52,13 +54,13 @@ final class SmartPlaylistRule implements Arrayable
self::MODEL_LENGTH,
self::MODEL_DATE_ADDED,
self::MODEL_DATE_MODIFIED,
self::MODEL_GENRE,
self::MODEL_YEAR,
];
private const MODEL_COLUMN_MAP = [
self::MODEL_TITLE => 'songs.title',
self::MODEL_ALBUM_NAME => 'albums.name',
self::MODEL_ARTIST_NAME => 'artists.name',
self::MODEL_LENGTH => 'songs.length',
self::MODEL_DATE_ADDED => 'songs.created_at',
self::MODEL_DATE_MODIFIED => 'songs.updated_at',
];

View file

@ -17,6 +17,8 @@ final class SongScanInformation implements Arrayable
public ?string $albumArtistName,
public ?int $track,
public ?int $disc,
public ?int $year,
public ?string $genre,
public ?string $lyrics,
public ?float $length,
public ?array $cover,
@ -65,15 +67,17 @@ final class SongScanInformation implements Arrayable
albumArtistName: html_entity_decode($albumArtistName),
track: (int) self::getTag($tags, ['track', 'tracknumber', 'track_number']),
disc: (int) self::getTag($tags, ['discnumber', 'part_of_a_set'], 1),
year: (int) self::getTag($tags, 'year') ?: null,
genre: self::getTag($tags, 'genre'),
lyrics: $lyrics,
length: (float) Arr::get($info, 'playtime_seconds'),
cover: $cover,
path: $path,
mTime: Helper::getModifiedTime($path),
mTime: Helper::getModifiedTime($path)
);
}
private static function getTag(array $arr, string | array $keys, $default = ''): mixed
private static function getTag(array $arr, string|array $keys, $default = ''): mixed
{
$keys = Arr::wrap($keys);
@ -98,6 +102,8 @@ final class SongScanInformation implements Arrayable
'albumartist' => $this->albumArtistName,
'track' => $this->track,
'disc' => $this->disc,
'year' => $this->year,
'genre' => $this->genre,
'lyrics' => $this->lyrics,
'length' => $this->length,
'cover' => $this->cover,

View file

@ -14,6 +14,8 @@ final class SongUpdateData implements Arrayable
public ?string $albumArtistName,
public ?int $track,
public ?int $disc,
public ?string $genre,
public ?int $year,
public ?string $lyrics,
) {
$this->albumArtistName = $this->albumArtistName ?: $this->artistName;
@ -28,6 +30,8 @@ final class SongUpdateData implements Arrayable
albumArtistName: $request->input('data.album_artist_name'),
track: (int) $request->input('data.track'),
disc: (int) $request->input('data.disc'),
genre: $request->input('data.genre'),
year: (int) $request->input('data.year') ?: null,
lyrics: $request->input('data.lyrics'),
);
}
@ -39,6 +43,8 @@ final class SongUpdateData implements Arrayable
?string $albumArtistName,
?int $track,
?int $disc,
?string $genre,
?int $year,
?string $lyrics
): self {
return new self(
@ -48,6 +54,8 @@ final class SongUpdateData implements Arrayable
$albumArtistName,
$track,
$disc,
$genre,
$year,
$lyrics,
);
}
@ -62,6 +70,8 @@ final class SongUpdateData implements Arrayable
'album_artist' => $this->albumArtistName,
'track' => $this->track,
'disc' => $this->disc,
'genre' => $this->genre,
'year' => $this->year,
'lyrics' => $this->lyrics,
];
}

View file

@ -22,8 +22,11 @@ class SongFactory extends Factory
'title' => $this->faker->sentence,
'length' => $this->faker->randomFloat(2, 10, 500),
'track' => random_int(1, 20),
'disc' => random_int(1, 5),
'lyrics' => $this->faker->paragraph(),
'path' => '/tmp/' . uniqid() . '.mp3',
'genre' => $this->faker->randomElement(['Rock', 'Pop', 'Jazz', 'Classical', 'Metal', 'Hip Hop', 'Rap']),
'year' => $this->faker->year(),
'mtime' => time(),
];
}

View file

@ -0,0 +1,16 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('songs', static function (Blueprint $table): void {
$table->year('year')->nullable();
$table->string('genre')->default('');
});
}
};

View file

@ -1,4 +1,5 @@
import { Faker, faker } from '@faker-js/faker'
import { genres } from '@/config'
const generate = (partOfCompilation = false): Song => {
const artistId = faker.datatype.number({ min: 3 })
@ -18,6 +19,8 @@ const generate = (partOfCompilation = false): Song => {
length: faker.datatype.number(),
track: faker.datatype.number(),
disc: faker.datatype.number({ min: 1, max: 2 }),
genre: faker.random.arrayElement(genres),
year: faker.random.arrayElement([null, 1990, 2000, 2011, 2022]),
lyrics: faker.lorem.paragraph(),
play_count: faker.datatype.number(),
liked: faker.datatype.boolean(),

View file

@ -39,7 +39,8 @@ new class extends UnitTestCase {
title: 'Rocket to Heaven',
artist_name: 'Led Zeppelin',
album_name: 'IV',
album_cover: 'http://localhost/album.jpg'
album_cover: 'http://localhost/album.jpg',
genre: 'Rock'
}))
expect(html()).toMatchSnapshot()
@ -50,6 +51,8 @@ new class extends UnitTestCase {
await fireEvent.update(getByTestId('album-input'), 'Back in Black')
await fireEvent.update(getByTestId('disc-input'), '1')
await fireEvent.update(getByTestId('track-input'), '10')
await fireEvent.update(getByTestId('genre-input'), 'Rock & Roll')
await fireEvent.update(getByTestId('year-input'), '1971')
await fireEvent.update(getByTestId('lyrics-input'), 'I\'m gonna make him an offer he can\'t refuse')
await fireEvent.click(getByRole('button', { name: 'Update' }))
@ -61,7 +64,9 @@ new class extends UnitTestCase {
album_artist_name: 'AC/DC',
lyrics: 'I\'m gonna make him an offer he can\'t refuse',
track: 10,
disc: 1
disc: 1,
genre: 'Rock & Roll',
year: 1971
})
expect(alertMock).toHaveBeenCalledWith('Updated 1 song.')
@ -84,6 +89,8 @@ new class extends UnitTestCase {
await fireEvent.update(getByTestId('album-input'), 'Back in Black')
await fireEvent.update(getByTestId('disc-input'), '1')
await fireEvent.update(getByTestId('track-input'), '10')
await fireEvent.update(getByTestId('year-input'), '1990')
await fireEvent.update(getByTestId('genre-input'), 'Pop')
await fireEvent.click(getByRole('button', { name: 'Update' }))
@ -92,7 +99,9 @@ new class extends UnitTestCase {
artist_name: 'AC/DC',
album_artist_name: 'AC/DC',
track: 10,
disc: 1
disc: 1,
genre: 'Pop',
year: 1990
})
expect(alertMock).toHaveBeenCalledWith('Updated 3 songs.')

View file

@ -70,16 +70,28 @@
</div>
<div class="form-row">
<label>
Artist
<input
v-model="formData.artist_name"
:placeholder="inputPlaceholder"
data-testid="artist-input"
name="artist"
type="text"
>
</label>
<div class="cols">
<label>
Artist
<input
v-model="formData.artist_name"
:placeholder="inputPlaceholder"
data-testid="artist-input"
name="artist"
type="text"
>
</label>
<label>
Album Artist
<input
v-model="formData.album_artist_name"
:placeholder="inputPlaceholder"
data-testid="albumArtist-input"
name="album_artist"
type="text"
>
</label>
</div>
</div>
<div class="form-row">
@ -96,46 +108,58 @@
</div>
<div class="form-row">
<label>
Album Artist
<input
v-model="formData.album_artist_name"
:placeholder="inputPlaceholder"
data-testid="albumArtist-input"
name="album_artist"
type="text"
>
</label>
<div class="cols">
<label>
Track
<input
v-model="formData.track"
:placeholder="inputPlaceholder"
data-testid="track-input"
min="1"
name="track"
type="number"
>
</label>
<label>
Disc
<input
v-model="formData.disc"
:placeholder="inputPlaceholder"
data-testid="disc-input"
min="1"
name="disc"
type="number"
>
</label>
</div>
</div>
<div class="form-row">
<div class="cols">
<div>
<label>
Track
<input
v-model="formData.track"
:placeholder="inputPlaceholder"
data-testid="track-input"
min="1"
name="track"
type="number"
>
</label>
</div>
<div>
<label>
Disc
<input
v-model="formData.disc"
:placeholder="inputPlaceholder"
data-testid="disc-input"
min="1"
name="disc"
type="number"
>
</label>
</div>
<label>
Genre
<input
v-model="formData.genre"
:placeholder="inputPlaceholder"
data-testid="genre-input"
name="genre"
type="text"
list="genres"
>
<datalist id="genres">
<option v-for="genre in genres" :key="genre" :value="genre"/>
</datalist>
</label>
<label>
Year
<input
v-model="formData.year"
:placeholder="inputPlaceholder"
data-testid="year-input"
name="year"
type="number"
>
</label>
</div>
</div>
</div>
@ -175,6 +199,7 @@ import { isEqual } from 'lodash'
import { defaultCover, eventBus, pluralize, requireInjection } from '@/utils'
import { songStore, SongUpdateData } from '@/stores'
import { DialogBoxKey, EditSongFormInitialTabKey, MessageToasterKey, SongsKey } from '@/symbols'
import { genres } from '@/config'
import Btn from '@/components/ui/Btn.vue'
import SoundBars from '@/components/ui/SoundBars.vue'
@ -199,7 +224,9 @@ const formData = reactive<SongUpdateData>({
album_artist_name: '',
lyrics: '',
track: null,
disc: null
disc: null,
year: null,
genre: ''
})
const initialFormData = {}
@ -261,6 +288,9 @@ const open = async () => {
formData.disc = allSongsShareSameValue('disc') ? firstSong.disc : null
formData.disc = formData.disc || null // if 0, just don't show it
formData.year = allSongsShareSameValue('year') ? firstSong.year : null
formData.genre = allSongsShareSameValue('genre') ? firstSong.genre : ''
if (!editingOnlyOneSong.value) {
delete formData.title
delete formData.lyrics
@ -330,7 +360,7 @@ form {
place-content: space-between;
gap: 1rem;
> div {
> * {
flex: 1;
}
}

View file

@ -15,14 +15,207 @@ exports[`edits a single song 1`] = `
<div class="panes" data-v-210b4214="">
<div id="editSongPanelDetails" aria-labelledby="editSongTabDetails" role="tabpanel" tabindex="0" data-v-210b4214="">
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Title <input data-testid="title-input" name="title" title="Title" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Artist <input placeholder="" data-testid="artist-input" name="artist" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Album <input placeholder="" data-testid="album-input" name="album" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Album Artist <input placeholder="" data-testid="albumArtist-input" name="album_artist" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214="">
<div class="cols" data-v-210b4214="">
<div data-v-210b4214=""><label data-v-210b4214=""> Track <input placeholder="" data-testid="track-input" min="1" name="track" type="number" data-v-210b4214=""></label></div>
<div data-v-210b4214=""><label data-v-210b4214=""> Disc <input placeholder="" data-testid="disc-input" min="1" name="disc" type="number" data-v-210b4214=""></label></div>
</div>
<div class="cols" data-v-210b4214=""><label data-v-210b4214=""> Artist <input placeholder="" data-testid="artist-input" name="artist" type="text" data-v-210b4214=""></label><label data-v-210b4214=""> Album Artist <input placeholder="" data-testid="albumArtist-input" name="album_artist" type="text" data-v-210b4214=""></label></div>
</div>
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Album <input placeholder="" data-testid="album-input" name="album" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214="">
<div class="cols" data-v-210b4214=""><label data-v-210b4214=""> Track <input placeholder="" data-testid="track-input" min="1" name="track" type="number" data-v-210b4214=""></label><label data-v-210b4214=""> Disc <input placeholder="" data-testid="disc-input" min="1" name="disc" type="number" data-v-210b4214=""></label></div>
</div>
<div class="form-row" data-v-210b4214="">
<div class="cols" data-v-210b4214=""><label data-v-210b4214=""> Genre <input placeholder="" data-testid="genre-input" name="genre" type="text" list="genres" data-v-210b4214=""><datalist id="genres" data-v-210b4214="">
<option value="Blues" data-v-210b4214=""></option>
<option value="Classic Rock" data-v-210b4214=""></option>
<option value="Country" data-v-210b4214=""></option>
<option value="Dance" data-v-210b4214=""></option>
<option value="Disco" data-v-210b4214=""></option>
<option value="Funk" data-v-210b4214=""></option>
<option value="Grunge" data-v-210b4214=""></option>
<option value="Hip-Hop" data-v-210b4214=""></option>
<option value="Jazz" data-v-210b4214=""></option>
<option value="Metal" data-v-210b4214=""></option>
<option value="New Age" data-v-210b4214=""></option>
<option value="Oldies" data-v-210b4214=""></option>
<option value="Other" data-v-210b4214=""></option>
<option value="Pop" data-v-210b4214=""></option>
<option value="R&amp;B" data-v-210b4214=""></option>
<option value="Rap" data-v-210b4214=""></option>
<option value="Reggae" data-v-210b4214=""></option>
<option value="Rock" data-v-210b4214=""></option>
<option value="Techno" data-v-210b4214=""></option>
<option value="Industrial" data-v-210b4214=""></option>
<option value="Alternative" data-v-210b4214=""></option>
<option value="Ska" data-v-210b4214=""></option>
<option value="Death Metal" data-v-210b4214=""></option>
<option value="Pranks" data-v-210b4214=""></option>
<option value="Soundtrack" data-v-210b4214=""></option>
<option value="Euro-Techno" data-v-210b4214=""></option>
<option value="Ambient" data-v-210b4214=""></option>
<option value="Trip-Hop" data-v-210b4214=""></option>
<option value="Vocal" data-v-210b4214=""></option>
<option value="Jazz &amp; Funk" data-v-210b4214=""></option>
<option value="Fusion" data-v-210b4214=""></option>
<option value="Trance" data-v-210b4214=""></option>
<option value="Classical" data-v-210b4214=""></option>
<option value="Instrumental" data-v-210b4214=""></option>
<option value="Acid" data-v-210b4214=""></option>
<option value="House" data-v-210b4214=""></option>
<option value="Game" data-v-210b4214=""></option>
<option value="Sound Clip" data-v-210b4214=""></option>
<option value="Gospel" data-v-210b4214=""></option>
<option value="Noise" data-v-210b4214=""></option>
<option value="Alternative Rock" data-v-210b4214=""></option>
<option value="Bass" data-v-210b4214=""></option>
<option value="Punk" data-v-210b4214=""></option>
<option value="Space" data-v-210b4214=""></option>
<option value="Meditative" data-v-210b4214=""></option>
<option value="Instrumental Pop" data-v-210b4214=""></option>
<option value="Instrumental Rock" data-v-210b4214=""></option>
<option value="Ethnic" data-v-210b4214=""></option>
<option value="Gothic" data-v-210b4214=""></option>
<option value="Darkwave" data-v-210b4214=""></option>
<option value="Techno-Industrial" data-v-210b4214=""></option>
<option value="Electronic" data-v-210b4214=""></option>
<option value="Pop-Folk" data-v-210b4214=""></option>
<option value="Eurodance" data-v-210b4214=""></option>
<option value="Dream" data-v-210b4214=""></option>
<option value="Southern Rock" data-v-210b4214=""></option>
<option value="Comedy" data-v-210b4214=""></option>
<option value="Cult" data-v-210b4214=""></option>
<option value="Gangsta" data-v-210b4214=""></option>
<option value="Top 40" data-v-210b4214=""></option>
<option value="Christian Rap" data-v-210b4214=""></option>
<option value="Pop/Funk" data-v-210b4214=""></option>
<option value="Jungle" data-v-210b4214=""></option>
<option value="Native US" data-v-210b4214=""></option>
<option value="Cabaret" data-v-210b4214=""></option>
<option value="New Wave" data-v-210b4214=""></option>
<option value="Psychedelic" data-v-210b4214=""></option>
<option value="Rave" data-v-210b4214=""></option>
<option value="Showtunes" data-v-210b4214=""></option>
<option value="Trailer" data-v-210b4214=""></option>
<option value="Lo-Fi" data-v-210b4214=""></option>
<option value="Tribal" data-v-210b4214=""></option>
<option value="Acid Punk" data-v-210b4214=""></option>
<option value="Acid Jazz" data-v-210b4214=""></option>
<option value="Polka" data-v-210b4214=""></option>
<option value="Retro" data-v-210b4214=""></option>
<option value="Musical" data-v-210b4214=""></option>
<option value="Rock &amp; Roll" data-v-210b4214=""></option>
<option value="Hard Rock" data-v-210b4214=""></option>
<option value="Folk" data-v-210b4214=""></option>
<option value="Folk-Rock" data-v-210b4214=""></option>
<option value="National Folk" data-v-210b4214=""></option>
<option value="Swing" data-v-210b4214=""></option>
<option value="Fast Fusion" data-v-210b4214=""></option>
<option value="Bebob" data-v-210b4214=""></option>
<option value="Latin" data-v-210b4214=""></option>
<option value="Revival" data-v-210b4214=""></option>
<option value="Celtic" data-v-210b4214=""></option>
<option value="Bluegrass" data-v-210b4214=""></option>
<option value="Avantgarde" data-v-210b4214=""></option>
<option value="Gothic Rock" data-v-210b4214=""></option>
<option value="Progressive Rock" data-v-210b4214=""></option>
<option value="Psychedelic Rock" data-v-210b4214=""></option>
<option value="Symphonic Rock" data-v-210b4214=""></option>
<option value="Slow Rock" data-v-210b4214=""></option>
<option value="Big Band" data-v-210b4214=""></option>
<option value="Chorus" data-v-210b4214=""></option>
<option value="Easy Listening" data-v-210b4214=""></option>
<option value="Acoustic" data-v-210b4214=""></option>
<option value="Humour" data-v-210b4214=""></option>
<option value="Speech" data-v-210b4214=""></option>
<option value="Chanson" data-v-210b4214=""></option>
<option value="Opera" data-v-210b4214=""></option>
<option value="Chamber Music" data-v-210b4214=""></option>
<option value="Sonata" data-v-210b4214=""></option>
<option value="Symphony" data-v-210b4214=""></option>
<option value="Booty Bass" data-v-210b4214=""></option>
<option value="Primus" data-v-210b4214=""></option>
<option value="Porn Groove" data-v-210b4214=""></option>
<option value="Satire" data-v-210b4214=""></option>
<option value="Slow Jam" data-v-210b4214=""></option>
<option value="Club" data-v-210b4214=""></option>
<option value="Tango" data-v-210b4214=""></option>
<option value="Samba" data-v-210b4214=""></option>
<option value="Folklore" data-v-210b4214=""></option>
<option value="Ballad" data-v-210b4214=""></option>
<option value="Power Ballad" data-v-210b4214=""></option>
<option value="Rhythmic Soul" data-v-210b4214=""></option>
<option value="Freestyle" data-v-210b4214=""></option>
<option value="Duet" data-v-210b4214=""></option>
<option value="Punk Rock" data-v-210b4214=""></option>
<option value="Drum Solo" data-v-210b4214=""></option>
<option value="A cappella" data-v-210b4214=""></option>
<option value="Euro-House" data-v-210b4214=""></option>
<option value="Dance Hall" data-v-210b4214=""></option>
<option value="Goa" data-v-210b4214=""></option>
<option value="Drum &amp; Bass" data-v-210b4214=""></option>
<option value="Club-House" data-v-210b4214=""></option>
<option value="Hardcore Techno" data-v-210b4214=""></option>
<option value="Terror" data-v-210b4214=""></option>
<option value="Indie" data-v-210b4214=""></option>
<option value="BritPop" data-v-210b4214=""></option>
<option value="Negerpunk" data-v-210b4214=""></option>
<option value="Polsk Punk" data-v-210b4214=""></option>
<option value="Beat" data-v-210b4214=""></option>
<option value="Christian Gangsta Rap" data-v-210b4214=""></option>
<option value="Heavy Metal" data-v-210b4214=""></option>
<option value="Black Metal" data-v-210b4214=""></option>
<option value="Crossover" data-v-210b4214=""></option>
<option value="Contemporary Christian" data-v-210b4214=""></option>
<option value="Christian Rock" data-v-210b4214=""></option>
<option value="Merengue" data-v-210b4214=""></option>
<option value="Salsa" data-v-210b4214=""></option>
<option value="Thrash Metal" data-v-210b4214=""></option>
<option value="Anime" data-v-210b4214=""></option>
<option value="JPop" data-v-210b4214=""></option>
<option value="SynthPop" data-v-210b4214=""></option>
<option value="Abstract" data-v-210b4214=""></option>
<option value="Art Rock" data-v-210b4214=""></option>
<option value="Baroque" data-v-210b4214=""></option>
<option value="Bhangra" data-v-210b4214=""></option>
<option value="Big beat" data-v-210b4214=""></option>
<option value="Breakbeat" data-v-210b4214=""></option>
<option value="Chillout" data-v-210b4214=""></option>
<option value="Downtempo" data-v-210b4214=""></option>
<option value="Dub" data-v-210b4214=""></option>
<option value="EBM" data-v-210b4214=""></option>
<option value="Eclectic" data-v-210b4214=""></option>
<option value="Electro" data-v-210b4214=""></option>
<option value="Electroclash" data-v-210b4214=""></option>
<option value="Emo" data-v-210b4214=""></option>
<option value="Experimental" data-v-210b4214=""></option>
<option value="Garage" data-v-210b4214=""></option>
<option value="Global" data-v-210b4214=""></option>
<option value="IDM" data-v-210b4214=""></option>
<option value="Illbient" data-v-210b4214=""></option>
<option value="Industro-Goth" data-v-210b4214=""></option>
<option value="Jam Band" data-v-210b4214=""></option>
<option value="Krautrock" data-v-210b4214=""></option>
<option value="Leftfield" data-v-210b4214=""></option>
<option value="Lounge" data-v-210b4214=""></option>
<option value="Math Rock" data-v-210b4214=""></option>
<option value="New Romantic" data-v-210b4214=""></option>
<option value="Nu-Breakz" data-v-210b4214=""></option>
<option value="Post-Punk" data-v-210b4214=""></option>
<option value="Post-Rock" data-v-210b4214=""></option>
<option value="Psytrance" data-v-210b4214=""></option>
<option value="Shoegaze" data-v-210b4214=""></option>
<option value="Space Rock" data-v-210b4214=""></option>
<option value="Trop Rock" data-v-210b4214=""></option>
<option value="World Music" data-v-210b4214=""></option>
<option value="Neoclassical" data-v-210b4214=""></option>
<option value="Audiobook" data-v-210b4214=""></option>
<option value="Audio Theatre" data-v-210b4214=""></option>
<option value="Neue Deutsche Welle" data-v-210b4214=""></option>
<option value="Podcast" data-v-210b4214=""></option>
<option value="Indie-Rock" data-v-210b4214=""></option>
<option value="G-Funk" data-v-210b4214=""></option>
<option value="Dubstep" data-v-210b4214=""></option>
<option value="Garage Rock" data-v-210b4214=""></option>
<option value="Psybient" data-v-210b4214=""></option>
</datalist></label><label data-v-210b4214=""> Year <input placeholder="" data-testid="year-input" name="year" type="number" data-v-210b4214=""></label></div>
</div>
</div>
<div id="editSongPanelLyrics" aria-labelledby="editSongTabLyrics" role="tabpanel" tabindex="0" data-v-210b4214="" style="display: none;">
@ -52,14 +245,207 @@ exports[`edits multiple songs 1`] = `
<div class="panes" data-v-210b4214="">
<div id="editSongPanelDetails" aria-labelledby="editSongTabDetails" role="tabpanel" tabindex="0" data-v-210b4214="">
<!--v-if-->
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Artist <input placeholder="Leave unchanged" data-testid="artist-input" name="artist" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Album <input placeholder="Leave unchanged" data-testid="album-input" name="album" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Album Artist <input placeholder="Leave unchanged" data-testid="albumArtist-input" name="album_artist" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214="">
<div class="cols" data-v-210b4214="">
<div data-v-210b4214=""><label data-v-210b4214=""> Track <input placeholder="Leave unchanged" data-testid="track-input" min="1" name="track" type="number" data-v-210b4214=""></label></div>
<div data-v-210b4214=""><label data-v-210b4214=""> Disc <input placeholder="Leave unchanged" data-testid="disc-input" min="1" name="disc" type="number" data-v-210b4214=""></label></div>
</div>
<div class="cols" data-v-210b4214=""><label data-v-210b4214=""> Artist <input placeholder="Leave unchanged" data-testid="artist-input" name="artist" type="text" data-v-210b4214=""></label><label data-v-210b4214=""> Album Artist <input placeholder="Leave unchanged" data-testid="albumArtist-input" name="album_artist" type="text" data-v-210b4214=""></label></div>
</div>
<div class="form-row" data-v-210b4214=""><label data-v-210b4214=""> Album <input placeholder="Leave unchanged" data-testid="album-input" name="album" type="text" data-v-210b4214=""></label></div>
<div class="form-row" data-v-210b4214="">
<div class="cols" data-v-210b4214=""><label data-v-210b4214=""> Track <input placeholder="Leave unchanged" data-testid="track-input" min="1" name="track" type="number" data-v-210b4214=""></label><label data-v-210b4214=""> Disc <input placeholder="Leave unchanged" data-testid="disc-input" min="1" name="disc" type="number" data-v-210b4214=""></label></div>
</div>
<div class="form-row" data-v-210b4214="">
<div class="cols" data-v-210b4214=""><label data-v-210b4214=""> Genre <input placeholder="Leave unchanged" data-testid="genre-input" name="genre" type="text" list="genres" data-v-210b4214=""><datalist id="genres" data-v-210b4214="">
<option value="Blues" data-v-210b4214=""></option>
<option value="Classic Rock" data-v-210b4214=""></option>
<option value="Country" data-v-210b4214=""></option>
<option value="Dance" data-v-210b4214=""></option>
<option value="Disco" data-v-210b4214=""></option>
<option value="Funk" data-v-210b4214=""></option>
<option value="Grunge" data-v-210b4214=""></option>
<option value="Hip-Hop" data-v-210b4214=""></option>
<option value="Jazz" data-v-210b4214=""></option>
<option value="Metal" data-v-210b4214=""></option>
<option value="New Age" data-v-210b4214=""></option>
<option value="Oldies" data-v-210b4214=""></option>
<option value="Other" data-v-210b4214=""></option>
<option value="Pop" data-v-210b4214=""></option>
<option value="R&amp;B" data-v-210b4214=""></option>
<option value="Rap" data-v-210b4214=""></option>
<option value="Reggae" data-v-210b4214=""></option>
<option value="Rock" data-v-210b4214=""></option>
<option value="Techno" data-v-210b4214=""></option>
<option value="Industrial" data-v-210b4214=""></option>
<option value="Alternative" data-v-210b4214=""></option>
<option value="Ska" data-v-210b4214=""></option>
<option value="Death Metal" data-v-210b4214=""></option>
<option value="Pranks" data-v-210b4214=""></option>
<option value="Soundtrack" data-v-210b4214=""></option>
<option value="Euro-Techno" data-v-210b4214=""></option>
<option value="Ambient" data-v-210b4214=""></option>
<option value="Trip-Hop" data-v-210b4214=""></option>
<option value="Vocal" data-v-210b4214=""></option>
<option value="Jazz &amp; Funk" data-v-210b4214=""></option>
<option value="Fusion" data-v-210b4214=""></option>
<option value="Trance" data-v-210b4214=""></option>
<option value="Classical" data-v-210b4214=""></option>
<option value="Instrumental" data-v-210b4214=""></option>
<option value="Acid" data-v-210b4214=""></option>
<option value="House" data-v-210b4214=""></option>
<option value="Game" data-v-210b4214=""></option>
<option value="Sound Clip" data-v-210b4214=""></option>
<option value="Gospel" data-v-210b4214=""></option>
<option value="Noise" data-v-210b4214=""></option>
<option value="Alternative Rock" data-v-210b4214=""></option>
<option value="Bass" data-v-210b4214=""></option>
<option value="Punk" data-v-210b4214=""></option>
<option value="Space" data-v-210b4214=""></option>
<option value="Meditative" data-v-210b4214=""></option>
<option value="Instrumental Pop" data-v-210b4214=""></option>
<option value="Instrumental Rock" data-v-210b4214=""></option>
<option value="Ethnic" data-v-210b4214=""></option>
<option value="Gothic" data-v-210b4214=""></option>
<option value="Darkwave" data-v-210b4214=""></option>
<option value="Techno-Industrial" data-v-210b4214=""></option>
<option value="Electronic" data-v-210b4214=""></option>
<option value="Pop-Folk" data-v-210b4214=""></option>
<option value="Eurodance" data-v-210b4214=""></option>
<option value="Dream" data-v-210b4214=""></option>
<option value="Southern Rock" data-v-210b4214=""></option>
<option value="Comedy" data-v-210b4214=""></option>
<option value="Cult" data-v-210b4214=""></option>
<option value="Gangsta" data-v-210b4214=""></option>
<option value="Top 40" data-v-210b4214=""></option>
<option value="Christian Rap" data-v-210b4214=""></option>
<option value="Pop/Funk" data-v-210b4214=""></option>
<option value="Jungle" data-v-210b4214=""></option>
<option value="Native US" data-v-210b4214=""></option>
<option value="Cabaret" data-v-210b4214=""></option>
<option value="New Wave" data-v-210b4214=""></option>
<option value="Psychedelic" data-v-210b4214=""></option>
<option value="Rave" data-v-210b4214=""></option>
<option value="Showtunes" data-v-210b4214=""></option>
<option value="Trailer" data-v-210b4214=""></option>
<option value="Lo-Fi" data-v-210b4214=""></option>
<option value="Tribal" data-v-210b4214=""></option>
<option value="Acid Punk" data-v-210b4214=""></option>
<option value="Acid Jazz" data-v-210b4214=""></option>
<option value="Polka" data-v-210b4214=""></option>
<option value="Retro" data-v-210b4214=""></option>
<option value="Musical" data-v-210b4214=""></option>
<option value="Rock &amp; Roll" data-v-210b4214=""></option>
<option value="Hard Rock" data-v-210b4214=""></option>
<option value="Folk" data-v-210b4214=""></option>
<option value="Folk-Rock" data-v-210b4214=""></option>
<option value="National Folk" data-v-210b4214=""></option>
<option value="Swing" data-v-210b4214=""></option>
<option value="Fast Fusion" data-v-210b4214=""></option>
<option value="Bebob" data-v-210b4214=""></option>
<option value="Latin" data-v-210b4214=""></option>
<option value="Revival" data-v-210b4214=""></option>
<option value="Celtic" data-v-210b4214=""></option>
<option value="Bluegrass" data-v-210b4214=""></option>
<option value="Avantgarde" data-v-210b4214=""></option>
<option value="Gothic Rock" data-v-210b4214=""></option>
<option value="Progressive Rock" data-v-210b4214=""></option>
<option value="Psychedelic Rock" data-v-210b4214=""></option>
<option value="Symphonic Rock" data-v-210b4214=""></option>
<option value="Slow Rock" data-v-210b4214=""></option>
<option value="Big Band" data-v-210b4214=""></option>
<option value="Chorus" data-v-210b4214=""></option>
<option value="Easy Listening" data-v-210b4214=""></option>
<option value="Acoustic" data-v-210b4214=""></option>
<option value="Humour" data-v-210b4214=""></option>
<option value="Speech" data-v-210b4214=""></option>
<option value="Chanson" data-v-210b4214=""></option>
<option value="Opera" data-v-210b4214=""></option>
<option value="Chamber Music" data-v-210b4214=""></option>
<option value="Sonata" data-v-210b4214=""></option>
<option value="Symphony" data-v-210b4214=""></option>
<option value="Booty Bass" data-v-210b4214=""></option>
<option value="Primus" data-v-210b4214=""></option>
<option value="Porn Groove" data-v-210b4214=""></option>
<option value="Satire" data-v-210b4214=""></option>
<option value="Slow Jam" data-v-210b4214=""></option>
<option value="Club" data-v-210b4214=""></option>
<option value="Tango" data-v-210b4214=""></option>
<option value="Samba" data-v-210b4214=""></option>
<option value="Folklore" data-v-210b4214=""></option>
<option value="Ballad" data-v-210b4214=""></option>
<option value="Power Ballad" data-v-210b4214=""></option>
<option value="Rhythmic Soul" data-v-210b4214=""></option>
<option value="Freestyle" data-v-210b4214=""></option>
<option value="Duet" data-v-210b4214=""></option>
<option value="Punk Rock" data-v-210b4214=""></option>
<option value="Drum Solo" data-v-210b4214=""></option>
<option value="A cappella" data-v-210b4214=""></option>
<option value="Euro-House" data-v-210b4214=""></option>
<option value="Dance Hall" data-v-210b4214=""></option>
<option value="Goa" data-v-210b4214=""></option>
<option value="Drum &amp; Bass" data-v-210b4214=""></option>
<option value="Club-House" data-v-210b4214=""></option>
<option value="Hardcore Techno" data-v-210b4214=""></option>
<option value="Terror" data-v-210b4214=""></option>
<option value="Indie" data-v-210b4214=""></option>
<option value="BritPop" data-v-210b4214=""></option>
<option value="Negerpunk" data-v-210b4214=""></option>
<option value="Polsk Punk" data-v-210b4214=""></option>
<option value="Beat" data-v-210b4214=""></option>
<option value="Christian Gangsta Rap" data-v-210b4214=""></option>
<option value="Heavy Metal" data-v-210b4214=""></option>
<option value="Black Metal" data-v-210b4214=""></option>
<option value="Crossover" data-v-210b4214=""></option>
<option value="Contemporary Christian" data-v-210b4214=""></option>
<option value="Christian Rock" data-v-210b4214=""></option>
<option value="Merengue" data-v-210b4214=""></option>
<option value="Salsa" data-v-210b4214=""></option>
<option value="Thrash Metal" data-v-210b4214=""></option>
<option value="Anime" data-v-210b4214=""></option>
<option value="JPop" data-v-210b4214=""></option>
<option value="SynthPop" data-v-210b4214=""></option>
<option value="Abstract" data-v-210b4214=""></option>
<option value="Art Rock" data-v-210b4214=""></option>
<option value="Baroque" data-v-210b4214=""></option>
<option value="Bhangra" data-v-210b4214=""></option>
<option value="Big beat" data-v-210b4214=""></option>
<option value="Breakbeat" data-v-210b4214=""></option>
<option value="Chillout" data-v-210b4214=""></option>
<option value="Downtempo" data-v-210b4214=""></option>
<option value="Dub" data-v-210b4214=""></option>
<option value="EBM" data-v-210b4214=""></option>
<option value="Eclectic" data-v-210b4214=""></option>
<option value="Electro" data-v-210b4214=""></option>
<option value="Electroclash" data-v-210b4214=""></option>
<option value="Emo" data-v-210b4214=""></option>
<option value="Experimental" data-v-210b4214=""></option>
<option value="Garage" data-v-210b4214=""></option>
<option value="Global" data-v-210b4214=""></option>
<option value="IDM" data-v-210b4214=""></option>
<option value="Illbient" data-v-210b4214=""></option>
<option value="Industro-Goth" data-v-210b4214=""></option>
<option value="Jam Band" data-v-210b4214=""></option>
<option value="Krautrock" data-v-210b4214=""></option>
<option value="Leftfield" data-v-210b4214=""></option>
<option value="Lounge" data-v-210b4214=""></option>
<option value="Math Rock" data-v-210b4214=""></option>
<option value="New Romantic" data-v-210b4214=""></option>
<option value="Nu-Breakz" data-v-210b4214=""></option>
<option value="Post-Punk" data-v-210b4214=""></option>
<option value="Post-Rock" data-v-210b4214=""></option>
<option value="Psytrance" data-v-210b4214=""></option>
<option value="Shoegaze" data-v-210b4214=""></option>
<option value="Space Rock" data-v-210b4214=""></option>
<option value="Trop Rock" data-v-210b4214=""></option>
<option value="World Music" data-v-210b4214=""></option>
<option value="Neoclassical" data-v-210b4214=""></option>
<option value="Audiobook" data-v-210b4214=""></option>
<option value="Audio Theatre" data-v-210b4214=""></option>
<option value="Neue Deutsche Welle" data-v-210b4214=""></option>
<option value="Podcast" data-v-210b4214=""></option>
<option value="Indie-Rock" data-v-210b4214=""></option>
<option value="G-Funk" data-v-210b4214=""></option>
<option value="Dubstep" data-v-210b4214=""></option>
<option value="Garage Rock" data-v-210b4214=""></option>
<option value="Psybient" data-v-210b4214=""></option>
</datalist></label><label data-v-210b4214=""> Year <input placeholder="Leave unchanged" data-testid="year-input" name="year" type="number" data-v-210b4214=""></label></div>
</div>
</div>
<!--v-if-->

View file

@ -0,0 +1,193 @@
export const genres = [
'Blues',
'Classic Rock',
'Country',
'Dance',
'Disco',
'Funk',
'Grunge',
'Hip-Hop',
'Jazz',
'Metal',
'New Age',
'Oldies',
'Other',
'Pop',
'R&B',
'Rap',
'Reggae',
'Rock',
'Techno',
'Industrial',
'Alternative',
'Ska',
'Death Metal',
'Pranks',
'Soundtrack',
'Euro-Techno',
'Ambient',
'Trip-Hop',
'Vocal',
'Jazz & Funk',
'Fusion',
'Trance',
'Classical',
'Instrumental',
'Acid',
'House',
'Game',
'Sound Clip',
'Gospel',
'Noise',
'Alternative Rock',
'Bass',
'Punk',
'Space',
'Meditative',
'Instrumental Pop',
'Instrumental Rock',
'Ethnic',
'Gothic',
'Darkwave',
'Techno-Industrial',
'Electronic',
'Pop-Folk',
'Eurodance',
'Dream',
'Southern Rock',
'Comedy',
'Cult',
'Gangsta',
'Top 40',
'Christian Rap',
'Pop/Funk',
'Jungle',
'Native US',
'Cabaret',
'New Wave',
'Psychedelic',
'Rave',
'Showtunes',
'Trailer',
'Lo-Fi',
'Tribal',
'Acid Punk',
'Acid Jazz',
'Polka',
'Retro',
'Musical',
'Rock & Roll',
'Hard Rock',
'Folk',
'Folk-Rock',
'National Folk',
'Swing',
'Fast Fusion',
'Bebob',
'Latin',
'Revival',
'Celtic',
'Bluegrass',
'Avantgarde',
'Gothic Rock',
'Progressive Rock',
'Psychedelic Rock',
'Symphonic Rock',
'Slow Rock',
'Big Band',
'Chorus',
'Easy Listening',
'Acoustic',
'Humour',
'Speech',
'Chanson',
'Opera',
'Chamber Music',
'Sonata',
'Symphony',
'Booty Bass',
'Primus',
'Porn Groove',
'Satire',
'Slow Jam',
'Club',
'Tango',
'Samba',
'Folklore',
'Ballad',
'Power Ballad',
'Rhythmic Soul',
'Freestyle',
'Duet',
'Punk Rock',
'Drum Solo',
'A cappella',
'Euro-House',
'Dance Hall',
'Goa',
'Drum & Bass',
'Club-House',
'Hardcore Techno',
'Terror',
'Indie',
'BritPop',
'Negerpunk',
'Polsk Punk',
'Beat',
'Christian Gangsta Rap',
'Heavy Metal',
'Black Metal',
'Crossover',
'Contemporary Christian',
'Christian Rock',
'Merengue',
'Salsa',
'Thrash Metal',
'Anime',
'JPop',
'SynthPop',
'Abstract',
'Art Rock',
'Baroque',
'Bhangra',
'Big beat',
'Breakbeat',
'Chillout',
'Downtempo',
'Dub',
'EBM',
'Eclectic',
'Electro',
'Electroclash',
'Emo',
'Experimental',
'Garage',
'Global',
'IDM',
'Illbient',
'Industro-Goth',
'Jam Band',
'Krautrock',
'Leftfield',
'Lounge',
'Math Rock',
'New Romantic',
'Nu-Breakz',
'Post-Punk',
'Post-Rock',
'Psytrance',
'Shoegaze',
'Space Rock',
'Trop Rock',
'World Music',
'Neoclassical',
'Audiobook',
'Audio Theatre',
'Neue Deutsche Welle',
'Podcast',
'Indie-Rock',
'G-Funk',
'Dubstep',
'Garage Rock',
'Psybient'
]

View file

@ -1,3 +1,4 @@
export * from './events'
export * from './upload.types'
export * from './acceptedMediaTypes'
export * from './genres'

View file

@ -11,16 +11,15 @@ const models: SmartPlaylistModel[] = [
name: 'artist.name',
type: 'text',
label: 'Artist'
// }, {
// name: 'genre',
// type: 'text',
// label: 'Genre'
}, {
// name: 'bit_rate',
// type: 'number',
// label: 'Bit Rate',
// unit: 'kbps'
// }, {
name: 'genre',
type: 'text',
label: 'Genre'
}, {
name: 'year',
type: 'number',
label: 'Year'
}, {
name: 'interactions.play_count',
type: 'number',
label: 'Plays'

View file

@ -14,6 +14,8 @@ export type SongUpdateData = {
track?: number | null
disc?: number | null
lyrics?: string
year?: number | null
genre?: string
}
export interface SongUpdateResult {

View file

@ -152,6 +152,8 @@ interface Song {
readonly length: number
track: number | null
disc: number | null
genre: string
year: number | null
lyrics: string
play_count_registered?: boolean
preloaded?: boolean
@ -170,7 +172,7 @@ interface SmartPlaylistRuleGroup {
}
interface SmartPlaylistModel {
name: 'title' | 'length' | 'created_at' | 'updated_at' | 'album.name' | 'artist.name' | 'interactions.play_count' | 'interactions.updated_at'
name: 'title' | 'length' | 'created_at' | 'updated_at' | 'album.name' | 'artist.name' | 'interactions.play_count' | 'interactions.updated_at' | 'genre' | 'year'
type: 'text' | 'number' | 'date'
label: string
unit?: 'seconds' | 'days'

View file

@ -125,7 +125,6 @@ em {
label {
font-size: 1.1rem;
margin-bottom: 8px;
display: block;
&.small {

View file

@ -24,6 +24,8 @@ class SongTest extends TestCase
'liked',
'play_count',
'track',
'genre',
'year',
'disc',
'created_at',
];