diff --git a/backend/src/suggest.js b/backend/src/suggest.js index eddc017..bdaf07c 100644 --- a/backend/src/suggest.js +++ b/backend/src/suggest.js @@ -282,15 +282,50 @@ export function suggestEngine({ config, store, lidarr, fetchImpl = fetch }) { async function enrichGenres(rawItems, count, library, likedTitles) { const libSet = new Set(library.map((n) => n.toLowerCase())) const likedSet = new Set(likedTitles.map((t) => t.toLowerCase())) + // samma artist slås ofta upp flera gånger (toppartist + låtarnas artister) + const hitCache = new Map() + const cachedHit = async (name) => { + const key = (name ?? '').toLowerCase() + if (!hitCache.has(key)) hitCache.set(key, await lidarrArtistHit(name)) + return hitCache.get(key) + } + const out = [] for (const it of rawItems.slice(0, count)) { const topArtists = [] for (const name of (it.topArtists ?? []).slice(0, 5)) { if (libSet.has((name ?? '').toLowerCase())) continue - const hit = await lidarrArtistHit(name) + const hit = await cachedHit(name) if (hit?.inLibrary) continue - topArtists.push({ name, mbid: hit?.mbid ?? null, poster: hit?.poster ?? null }) + topArtists.push({ + kind: 'artist', + name, + secondary: '', + motivation: '', + year: '', + mbid: hit?.mbid ?? null, + poster: hit?.poster ?? null, + inLibrary: false, + }) } + + const topTracks = [] + for (const t of (it.topTracks ?? []).filter((t) => !likedSet.has((t.name ?? '').toLowerCase())).slice(0, 5)) { + const hit = await cachedHit(t.artist ?? t.name) + topTracks.push({ + kind: 'track', + name: t.name, + secondary: t.artist ?? '', + motivation: '', + year: '', + mbid: hit?.mbid ?? null, + artistMbid: hit?.mbid ?? null, + albumMbid: null, + poster: hit?.poster ?? null, + inLibrary: false, + }) + } + out.push({ kind: 'genre', name: it.name, @@ -301,10 +336,7 @@ export function suggestEngine({ config, store, lidarr, fetchImpl = fetch }) { poster: topArtists.find((a) => a.poster)?.poster ?? null, inLibrary: false, topArtists, - topTracks: (it.topTracks ?? []) - .filter((t) => !likedSet.has((t.name ?? '').toLowerCase())) - .slice(0, 5) - .map((t) => ({ name: t.name, artist: t.artist ?? '' })), + topTracks, }) } return out diff --git a/backend/test/suggest.test.js b/backend/test/suggest.test.js index 9d3377b..e3f24a4 100644 --- a/backend/test/suggest.test.js +++ b/backend/test/suggest.test.js @@ -165,6 +165,10 @@ test('run(genres): artister i prompt, filtrering mot bibliotek + gillat', async 'Powerwolf finns i biblioteket och ska filtreras bort', ) assert.equal(g.topArtists[0].mbid, 'mbid-x') + assert.equal(g.topArtists[0].kind, 'artist', 'toppartister ska vara färdiga kort') + assert.equal(g.topTracks[0].kind, 'track') + assert.equal(g.topTracks[0].artistMbid, 'mbid-x', 'låtar i genren berikas med artist-mbid') + assert.equal(g.topTracks[0].poster, 'http://img/s.jpg', 'låtar i genren får omslag') assert.equal(g.poster, 'http://img/s.jpg', 'genrekortet lånar första toppartistens omslag') }) diff --git a/frontend/src/components/DetailModal.vue b/frontend/src/components/DetailModal.vue index aedd78f..b8a3503 100644 --- a/frontend/src/components/DetailModal.vue +++ b/frontend/src/components/DetailModal.vue @@ -1,73 +1,63 @@ @@ -76,7 +66,7 @@ const stateLabel = { busy: '…', done: '✓', fanns: '✓ finns', fel: 'fel' } .overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.65); display: grid; place-items: center; z-index: 50; @@ -84,19 +74,20 @@ const stateLabel = { busy: '…', done: '✓', fanns: '✓ finns', fel: 'fel' } } .modal { - background: var(--card); + background: var(--bg); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 1rem; padding: 1.2rem 1.4rem; - width: min(30rem, 95vw); - max-height: 85vh; + width: min(58rem, 95vw); + max-height: 88vh; overflow-y: auto; } header { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; + gap: 1rem; } h3 { @@ -104,61 +95,26 @@ h3 { } h4 { - margin: 1rem 0 0.4rem; + margin: 1.1rem 0 0.6rem; } .close { background: transparent; color: var(--text-dim); padding: 0.2rem 0.5rem; + flex: 0 0 auto; } .dim { color: var(--text-dim); font-weight: 400; font-size: 0.9rem; + margin: 0.2rem 0 0; } -ul { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -li { - display: flex; - justify-content: space-between; - align-items: center; - gap: 0.6rem; -} - -.name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.row-actions { - display: flex; - gap: 0.4rem; - flex: 0 0 auto; -} - -.yt { - color: #ff8080; - font-size: 0.85rem; - white-space: nowrap; -} - -.mini { - padding: 0.25rem 0.6rem; - font-size: 0.85rem; -} - -.foot { - margin-top: 1.2rem; +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); + gap: 0.8rem; } diff --git a/frontend/src/components/MusicCard.vue b/frontend/src/components/MusicCard.vue index a7e0697..fba07fd 100644 --- a/frontend/src/components/MusicCard.vue +++ b/frontend/src/components/MusicCard.vue @@ -53,17 +53,21 @@ async function grab() {

{{ item.motivation }}

- - - - - + + ▶ YT
-