mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-18 06:22:15 +00:00
v3 library: genre filter facet (reads feedpak genres) (#690)
* feat(v3): genre filter facet (reads the feedpak genres field) Adds a Genre facet to the library Filters drawer, populated from each song's primary genre. Server: a genre column (idempotent ALTER, indexed) written from the sloppak manifest's genres list on scan (primary = genres[0]); a genre band in _build_where (OR within the selected set); GET /api/library/genres for the facet's distinct list. Client: a multi-select Genre section mirroring the tuning/mastery facets. Follows the merged spec 1.12.0 genres field (#40). v1 stores only the PRIMARY genre (genres[0]); secondary genres aren't filterable yet. Threaded like the mastery filter (separate query_page kwarg, so query_artists /query_stats are unaffected) -- genre filters the grid view. Needs a rescan to backfill genre on existing packs (only packs whose manifest carries genres). Verified live: a sloppak tagged genres:[Metal, Rock] -> /api/library/genres returns [Metal]; ?genre=Metal returns it; ?genre=Rock (secondary) returns none; a plain song stays ungenred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3): scope genre facet to local provider (PR #690 review) The /api/library/genres facet always read the local meta DB, so a remote provider showed local genres while the `genre` filter was a no-op on that provider's grid. Make the endpoint provider-aware: return an empty facet for remote providers (kind != "local") and keep serving genres for the local library and its smart collections, which share the local DB. The v3 client now passes the active provider, mirroring the tuning-names facet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
d20b33348b
commit
80caf78306
@@ -919,6 +919,8 @@ def extract_meta(path: Path) -> dict:
|
|||||||
"artist": str(manifest.get("artist", "")),
|
"artist": str(manifest.get("artist", "")),
|
||||||
"album": str(manifest.get("album", "")),
|
"album": str(manifest.get("album", "")),
|
||||||
"year": str(manifest.get("year", "") or ""),
|
"year": str(manifest.get("year", "") or ""),
|
||||||
|
# Primary genre from the feedpak `genres` list (spec 1.12.0); [0] = primary.
|
||||||
|
"genre": (lambda g: str(g[0]) if isinstance(g, list) and g else "")(manifest.get("genres")),
|
||||||
"duration": float(manifest.get("duration", 0) or 0),
|
"duration": float(manifest.get("duration", 0) or 0),
|
||||||
"tuning_offsets": tuning_offsets, # caller maps to a name via tunings.tuning_name
|
"tuning_offsets": tuning_offsets, # caller maps to a name via tunings.tuning_name
|
||||||
"arrangements": arrangements,
|
"arrangements": arrangements,
|
||||||
|
|||||||
@@ -550,7 +550,8 @@ class MetadataDB:
|
|||||||
stem_ids TEXT DEFAULT '[]',
|
stem_ids TEXT DEFAULT '[]',
|
||||||
tuning_name TEXT DEFAULT '',
|
tuning_name TEXT DEFAULT '',
|
||||||
tuning_sort_key INTEGER DEFAULT 0,
|
tuning_sort_key INTEGER DEFAULT 0,
|
||||||
tuning_offsets TEXT DEFAULT ''
|
tuning_offsets TEXT DEFAULT '',
|
||||||
|
genre TEXT DEFAULT ''
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
# Idempotent migrations for installs that predate each column.
|
# Idempotent migrations for installs that predate each column.
|
||||||
@@ -569,6 +570,9 @@ class MetadataDB:
|
|||||||
# distinct custom tunings distinct (tuning_name collapses them all
|
# distinct custom tunings distinct (tuning_name collapses them all
|
||||||
# to "Custom Tuning"). Cache; repopulated on rescan.
|
# to "Custom Tuning"). Cache; repopulated on rescan.
|
||||||
"ALTER TABLE songs ADD COLUMN tuning_offsets TEXT DEFAULT ''",
|
"ALTER TABLE songs ADD COLUMN tuning_offsets TEXT DEFAULT ''",
|
||||||
|
# Primary genre from the feedpak `genres` list (spec 1.12.0). Cache;
|
||||||
|
# repopulated on rescan.
|
||||||
|
"ALTER TABLE songs ADD COLUMN genre TEXT DEFAULT ''",
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
self.conn.execute(ddl)
|
self.conn.execute(ddl)
|
||||||
@@ -584,6 +588,7 @@ class MetadataDB:
|
|||||||
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_title_fn ON songs(title COLLATE NOCASE, filename)")
|
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_title_fn ON songs(title COLLATE NOCASE, filename)")
|
||||||
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_mtime_fn ON songs(mtime, filename)")
|
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_mtime_fn ON songs(mtime, filename)")
|
||||||
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_tuning_name ON songs(tuning_name COLLATE NOCASE)")
|
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_tuning_name ON songs(tuning_name COLLATE NOCASE)")
|
||||||
|
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_genre ON songs(genre COLLATE NOCASE)")
|
||||||
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_tuning_sort_key ON songs(tuning_sort_key)")
|
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_tuning_sort_key ON songs(tuning_sort_key)")
|
||||||
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_year ON songs(year)")
|
self.conn.execute("CREATE INDEX IF NOT EXISTS idx_songs_year ON songs(year)")
|
||||||
self.conn.execute("CREATE TABLE IF NOT EXISTS favorites (filename TEXT PRIMARY KEY)")
|
self.conn.execute("CREATE TABLE IF NOT EXISTS favorites (filename TEXT PRIMARY KEY)")
|
||||||
@@ -2548,8 +2553,8 @@ class MetadataDB:
|
|||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT OR REPLACE INTO songs "
|
"INSERT OR REPLACE INTO songs "
|
||||||
"(filename, mtime, size, title, artist, album, year, duration, tuning, arrangements, "
|
"(filename, mtime, size, title, artist, album, year, duration, tuning, arrangements, "
|
||||||
"has_lyrics, format, stem_count, stem_ids, tuning_name, tuning_sort_key, tuning_offsets) "
|
"has_lyrics, format, stem_count, stem_ids, tuning_name, tuning_sort_key, tuning_offsets, genre) "
|
||||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
(filename, mtime, size, meta.get("title", ""), meta.get("artist", ""),
|
(filename, mtime, size, meta.get("title", ""), meta.get("artist", ""),
|
||||||
meta.get("album", ""), meta.get("year", ""), meta.get("duration", 0),
|
meta.get("album", ""), meta.get("year", ""), meta.get("duration", 0),
|
||||||
meta.get("tuning", ""), json.dumps(meta.get("arrangements", [])),
|
meta.get("tuning", ""), json.dumps(meta.get("arrangements", [])),
|
||||||
@@ -2559,7 +2564,8 @@ class MetadataDB:
|
|||||||
json.dumps(meta.get("stem_ids", []) or []),
|
json.dumps(meta.get("stem_ids", []) or []),
|
||||||
meta.get("tuning_name", "") or "",
|
meta.get("tuning_name", "") or "",
|
||||||
int(meta.get("tuning_sort_key", 0) or 0),
|
int(meta.get("tuning_sort_key", 0) or 0),
|
||||||
meta.get("tuning_offsets", "") or ""),
|
meta.get("tuning_offsets", "") or "",
|
||||||
|
meta.get("genre", "") or ""),
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
# A song's identity may have changed → the grouping read-model is stale.
|
# A song's identity may have changed → the grouping read-model is stale.
|
||||||
@@ -2929,6 +2935,7 @@ class MetadataDB:
|
|||||||
tags_has: list[str] | None = None,
|
tags_has: list[str] | None = None,
|
||||||
user_difficulty_in: list[str] | None = None,
|
user_difficulty_in: list[str] | None = None,
|
||||||
match_states: list[str] | None = None,
|
match_states: list[str] | None = None,
|
||||||
|
genre: list[str] | None = None,
|
||||||
naming_mode: str = "legacy",
|
naming_mode: str = "legacy",
|
||||||
include_intrinsic: bool = True) -> tuple[str, list]:
|
include_intrinsic: bool = True) -> tuple[str, list]:
|
||||||
"""Shared WHERE-clause builder for query_page / query_artists /
|
"""Shared WHERE-clause builder for query_page / query_artists /
|
||||||
@@ -2958,6 +2965,12 @@ class MetadataDB:
|
|||||||
if album_filter:
|
if album_filter:
|
||||||
where += " AND album = ? COLLATE NOCASE"
|
where += " AND album = ? COLLATE NOCASE"
|
||||||
params.append(album_filter)
|
params.append(album_filter)
|
||||||
|
# Genre facet (primary genre column, populated from the feedpak `genres`
|
||||||
|
# list on scan). OR within the selected set.
|
||||||
|
if genre:
|
||||||
|
_gph = ",".join(["?"] * len(genre))
|
||||||
|
where += f" AND genre COLLATE NOCASE IN ({_gph})"
|
||||||
|
params += list(genre)
|
||||||
# Mastery bands = best accuracy across a song's arrangements (song_stats,
|
# Mastery bands = best accuracy across a song's arrangements (song_stats,
|
||||||
# a separate table -> correlated subquery). mastered >= 0.9, in_progress =
|
# a separate table -> correlated subquery). mastered >= 0.9, in_progress =
|
||||||
# attempted but < 0.9, not_started = no score. OR within the selected set.
|
# attempted but < 0.9, not_started = no score. OR within the selected set.
|
||||||
@@ -3440,6 +3453,7 @@ class MetadataDB:
|
|||||||
tags_has: list[str] | None = None,
|
tags_has: list[str] | None = None,
|
||||||
user_difficulty_in: list[str] | None = None,
|
user_difficulty_in: list[str] | None = None,
|
||||||
match_states: list[str] | None = None,
|
match_states: list[str] | None = None,
|
||||||
|
genre: list[str] | None = None,
|
||||||
after: str | None = None,
|
after: str | None = None,
|
||||||
group: bool = False,
|
group: bool = False,
|
||||||
naming_mode: str = "legacy") -> tuple[list[dict], int]:
|
naming_mode: str = "legacy") -> tuple[list[dict], int]:
|
||||||
@@ -3470,7 +3484,7 @@ class MetadataDB:
|
|||||||
stems_has=stems_has, stems_lacks=stems_lacks,
|
stems_has=stems_has, stems_lacks=stems_lacks,
|
||||||
has_lyrics=has_lyrics, tunings=tunings, mastery=mastery,
|
has_lyrics=has_lyrics, tunings=tunings, mastery=mastery,
|
||||||
tags_has=tags_has, user_difficulty_in=user_difficulty_in,
|
tags_has=tags_has, user_difficulty_in=user_difficulty_in,
|
||||||
match_states=match_states,
|
match_states=match_states, genre=genre,
|
||||||
naming_mode=naming_mode, include_intrinsic=not group,
|
naming_mode=naming_mode, include_intrinsic=not group,
|
||||||
)
|
)
|
||||||
ifrag, iparams = "", []
|
ifrag, iparams = "", []
|
||||||
@@ -7034,7 +7048,7 @@ async def list_library(q: str = "", page: int = 0, size: int = 24, sort: str = "
|
|||||||
stems_has: str = "", stems_lacks: str = "",
|
stems_has: str = "", stems_lacks: str = "",
|
||||||
has_lyrics: str = "", tunings: str = "", provider: str = "local",
|
has_lyrics: str = "", tunings: str = "", provider: str = "local",
|
||||||
mastery: str = "", tags: str = "", user_difficulty: str = "",
|
mastery: str = "", tags: str = "", user_difficulty: str = "",
|
||||||
match: str = "", after: str = "", group: int = 0,
|
match: str = "", genre: str = "", after: str = "", group: int = 0,
|
||||||
naming_mode: str = "legacy"):
|
naming_mode: str = "legacy"):
|
||||||
"""Paginated library search through the selected library provider.
|
"""Paginated library search through the selected library provider.
|
||||||
|
|
||||||
@@ -7064,6 +7078,7 @@ async def list_library(q: str = "", page: int = 0, size: int = 24, sort: str = "
|
|||||||
tags_has=_split_csv(tags),
|
tags_has=_split_csv(tags),
|
||||||
user_difficulty_in=_split_csv(user_difficulty),
|
user_difficulty_in=_split_csv(user_difficulty),
|
||||||
match_states=_split_csv(match),
|
match_states=_split_csv(match),
|
||||||
|
genre=_split_csv(genre),
|
||||||
**_library_filter_args(
|
**_library_filter_args(
|
||||||
q=q, favorites=favorites, format=format,
|
q=q, favorites=favorites, format=format,
|
||||||
artist=artist, album=album,
|
artist=artist, album=album,
|
||||||
@@ -7233,6 +7248,31 @@ async def library_stats(favorites: int = 0, q: str = "", format: str = "",
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/library/genres")
|
||||||
|
def library_genres(provider: str = "local"):
|
||||||
|
"""Distinct non-empty genres for the filter facet.
|
||||||
|
|
||||||
|
Genres are a local-library facet: they're populated from the feedpak
|
||||||
|
`genres` field at scan time and live in the local meta DB. Local-backed
|
||||||
|
providers (the local library and its smart collections, kind="local")
|
||||||
|
share that DB, so they surface the same set. Remote providers don't
|
||||||
|
expose genres here, so return an empty facet for them — the client then
|
||||||
|
hides the filter rather than offering local genres that don't apply to
|
||||||
|
the remote grid. Mirrors the local/remote gating used elsewhere for
|
||||||
|
provider calls (see `_call_library_provider`)."""
|
||||||
|
library_provider = _get_library_provider(provider)
|
||||||
|
kind = str(library_providers.provider_field(library_provider, "kind", "") or "")
|
||||||
|
is_remote = kind not in ("", "local") if kind else provider != "local"
|
||||||
|
if is_remote:
|
||||||
|
return {"genres": []}
|
||||||
|
with meta_db._lock:
|
||||||
|
rows = meta_db.conn.execute(
|
||||||
|
"SELECT DISTINCT genre FROM songs WHERE genre IS NOT NULL AND genre != '' "
|
||||||
|
"ORDER BY genre COLLATE NOCASE"
|
||||||
|
).fetchall()
|
||||||
|
return {"genres": [r[0] for r in rows]}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/library/tuning-names")
|
@app.get("/api/library/tuning-names")
|
||||||
async def list_tuning_names(provider: str = "local"):
|
async def list_tuning_names(provider: str = "local"):
|
||||||
"""Distinct tuning names present in the library, with per-tuning
|
"""Distinct tuning names present in the library, with per-tuning
|
||||||
|
|||||||
+10
-4
@@ -59,8 +59,8 @@
|
|||||||
artist: '', album: '',
|
artist: '', album: '',
|
||||||
grouping: true, // one card per song (multi-chart grouping); persisted
|
grouping: true, // one card per song (multi-chart grouping); persisted
|
||||||
|
|
||||||
filters: { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [], match: [] },
|
filters: { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [], match: [], genre: [] },
|
||||||
page: 0, total: 0, loading: false, built: false, accuracy: {}, tuningNames: [],
|
page: 0, total: 0, loading: false, built: false, accuracy: {}, tuningNames: [], genres: [],
|
||||||
artistCatalog: [], renderedHash: '',
|
artistCatalog: [], renderedHash: '',
|
||||||
scrollBound: false,
|
scrollBound: false,
|
||||||
songsById: {}, selectMode: false, selected: new Set(),
|
songsById: {}, selectMode: false, selected: new Set(),
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
const f = state.filters;
|
const f = state.filters;
|
||||||
return f.arr_has.length + f.arr_lacks.length + f.stem_has.length + f.stem_lacks.length +
|
return f.arr_has.length + f.arr_lacks.length + f.stem_has.length + f.stem_lacks.length +
|
||||||
(f.lyrics ? 1 : 0) + f.tunings.length + (f.mastery ? f.mastery.length : 0) +
|
(f.lyrics ? 1 : 0) + f.tunings.length + (f.mastery ? f.mastery.length : 0) +
|
||||||
(f.match ? f.match.length : 0) +
|
(f.match ? f.match.length : 0) + (f.genre ? f.genre.length : 0) +
|
||||||
(state.artist ? 1 : 0) + (state.album ? 1 : 0);
|
(state.artist ? 1 : 0) + (state.album ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +135,7 @@
|
|||||||
tunings: [...(f.tunings || [])].sort(),
|
tunings: [...(f.tunings || [])].sort(),
|
||||||
mastery: [...(f.mastery || [])].sort(),
|
mastery: [...(f.mastery || [])].sort(),
|
||||||
match: [...(f.match || [])].sort(),
|
match: [...(f.match || [])].sort(),
|
||||||
|
genre: [...(f.genre || [])].sort(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -271,6 +272,7 @@
|
|||||||
if (f.tunings.length) p.set('tunings', f.tunings.join(','));
|
if (f.tunings.length) p.set('tunings', f.tunings.join(','));
|
||||||
if (f.mastery && f.mastery.length) p.set('mastery', f.mastery.join(','));
|
if (f.mastery && f.mastery.length) p.set('mastery', f.mastery.join(','));
|
||||||
if (f.match && f.match.length) p.set('match', f.match.join(','));
|
if (f.match && f.match.length) p.set('match', f.match.join(','));
|
||||||
|
if (f.genre && f.genre.length) p.set('genre', f.genre.join(','));
|
||||||
Object.entries(extra || {}).forEach(([k, v]) => p.set(k, v));
|
Object.entries(extra || {}).forEach(([k, v]) => p.set(k, v));
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -2040,6 +2042,8 @@
|
|||||||
// Match (P8) — the song's metadata-match lifecycle state, a triage
|
// Match (P8) — the song's metadata-match lifecycle state, a triage
|
||||||
// facet for the enrichment layer. Session-only, like Progress.
|
// facet for the enrichment layer. Session-only, like Progress.
|
||||||
section('Match', [['review', 'To review'], ['matched', 'Matched'], ['unmatched', 'Unmatched'], ['pending', 'Not scanned']].map((it) => '<button data-match="' + it[0] + '" class="px-2 py-1 rounded-md text-xs border ' + (f.match.includes(it[0]) ? 'bg-fb-primary text-white border-fb-primary' : 'bg-gray-800/50 text-fb-textDim border-gray-700') + '">' + it[1] + '</button>').join('')) +
|
section('Match', [['review', 'To review'], ['matched', 'Matched'], ['unmatched', 'Unmatched'], ['pending', 'Not scanned']].map((it) => '<button data-match="' + it[0] + '" class="px-2 py-1 rounded-md text-xs border ' + (f.match.includes(it[0]) ? 'bg-fb-primary text-white border-fb-primary' : 'bg-gray-800/50 text-fb-textDim border-gray-700') + '">' + it[1] + '</button>').join('')) +
|
||||||
|
// Genre facet — dynamic list from /api/library/genres (primary genre).
|
||||||
|
(state.genres && state.genres.length ? section('Genre', state.genres.map((g) => '<button data-genre="' + esc(g) + '" class="px-2 py-1 rounded-md text-xs border ' + (f.genre.includes(g) ? 'bg-fb-primary text-white border-fb-primary' : 'bg-gray-800/50 text-fb-textDim border-gray-700') + '">' + esc(g) + '</button>').join('')) : '') +
|
||||||
section('Tuning', (state.tuningNames || []).map((t) => {
|
section('Tuning', (state.tuningNames || []).map((t) => {
|
||||||
// Filter on the server's grouping key (raw offsets for customs)
|
// Filter on the server's grouping key (raw offsets for customs)
|
||||||
// so two "Custom Tuning" entries are distinct; show their target
|
// so two "Custom Tuning" entries are distinct; show their target
|
||||||
@@ -2092,11 +2096,12 @@
|
|||||||
reload(); // re-fetches grid + rail with/without group=1, saves prefs
|
reload(); // re-fetches grid + rail with/without group=1, saves prefs
|
||||||
});
|
});
|
||||||
d.querySelectorAll('[data-match]').forEach((b) => b.addEventListener('click', () => { const v = b.getAttribute('data-match'); const i = f.match.indexOf(v); if (i >= 0) f.match.splice(i, 1); else f.match.push(v); renderDrawer(); }));
|
d.querySelectorAll('[data-match]').forEach((b) => b.addEventListener('click', () => { const v = b.getAttribute('data-match'); const i = f.match.indexOf(v); if (i >= 0) f.match.splice(i, 1); else f.match.push(v); renderDrawer(); }));
|
||||||
|
d.querySelectorAll('[data-genre]').forEach((b) => b.addEventListener('click', () => { const v = b.getAttribute('data-genre'); const i = f.genre.indexOf(v); if (i >= 0) f.genre.splice(i, 1); else f.genre.push(v); renderDrawer(); }));
|
||||||
d.querySelector('[data-drawer-save]')?.addEventListener('click', saveCurrentAsCollection);
|
d.querySelector('[data-drawer-save]')?.addEventListener('click', saveCurrentAsCollection);
|
||||||
d.querySelector('[data-drawer-tidy]')?.addEventListener('click', openArtistTidyUp);
|
d.querySelector('[data-drawer-tidy]')?.addEventListener('click', openArtistTidyUp);
|
||||||
d.querySelector('[data-drawer-close]')?.addEventListener('click', closeDrawer);
|
d.querySelector('[data-drawer-close]')?.addEventListener('click', closeDrawer);
|
||||||
d.querySelector('[data-drawer-clear]')?.addEventListener('click', async () => {
|
d.querySelector('[data-drawer-clear]')?.addEventListener('click', async () => {
|
||||||
state.filters = { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [], match: [] };
|
state.filters = { arr_has: [], arr_lacks: [], stem_has: [], stem_lacks: [], lyrics: '', tunings: [], mastery: [], match: [], genre: [] };
|
||||||
state.artist = '';
|
state.artist = '';
|
||||||
state.album = '';
|
state.album = '';
|
||||||
renderDrawer();
|
renderDrawer();
|
||||||
@@ -2543,6 +2548,7 @@
|
|||||||
loadArtistCatalog(),
|
loadArtistCatalog(),
|
||||||
]);
|
]);
|
||||||
state.tuningNames = (tn && tn.tunings) || [];
|
state.tuningNames = (tn && tn.tunings) || [];
|
||||||
|
try { const _g = await jget('/api/library/genres?provider=' + enc(state.provider)); state.genres = (_g && _g.genres) || []; } catch (e) { state.genres = []; }
|
||||||
|
|
||||||
const opt = (arr, sel) => arr.map(([v, l]) => '<option value="' + esc(v) + '"' + (v === sel ? ' selected' : '') + '>' + esc(l) + '</option>').join('');
|
const opt = (arr, sel) => arr.map(([v, l]) => '<option value="' + esc(v) + '"' + (v === sel ? ' selected' : '') + '>' + esc(l) + '</option>').join('');
|
||||||
const provOpts = providers.map((p) => '<option value="' + esc(p.id) + '"' + (p.id === state.provider ? ' selected' : '') + '>' + esc(p.label || p.id) + '</option>').join('');
|
const provOpts = providers.map((p) => '<option value="' + esc(p.id) + '"' + (p.id === state.provider ? ' selected' : '') + '>' + esc(p.label || p.id) + '</option>').join('');
|
||||||
|
|||||||
Reference in New Issue
Block a user