Genrekort: bara Utforska + YT; popupen visar kortrutnät med artister och låtar
All checks were successful
build-and-push / build (push) Successful in 13s
All checks were successful
build-and-push / build (push) Successful in 13s
Utforska-popupen renderar två MusicCard-grids (bilder, Hämta, ▶ YT) — genrens låtar berikas nu också med artist-mbid/omslag i backend (cachade uppslag så samma artist inte slås upp dubbelt). Äldre batcher normaliseras i modalen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup
This commit is contained in:
@@ -1,73 +1,63 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import MusicCard from './MusicCard.vue'
|
||||
|
||||
const props = defineProps({ item: { type: Object, required: true } })
|
||||
const emit = defineEmits(['close'])
|
||||
const added = ref({})
|
||||
|
||||
// genrer har topArtists, gamla sångar-batcher har relatedArtists
|
||||
const artists = computed(() => props.item.topArtists ?? props.item.relatedArtists ?? [])
|
||||
// nya genre-batcher har färdiga kort; äldre batcher (sångare/tidiga genrer)
|
||||
// har enklare objekt — normalisera till MusicCard-format
|
||||
const artistCards = computed(() =>
|
||||
(props.item.topArtists ?? props.item.relatedArtists ?? []).map((a) => ({
|
||||
kind: 'artist',
|
||||
name: a.name,
|
||||
secondary: a.secondary ?? '',
|
||||
motivation: a.motivation ?? '',
|
||||
year: a.year ?? '',
|
||||
mbid: a.mbid ?? null,
|
||||
poster: a.poster ?? null,
|
||||
inLibrary: a.inLibrary ?? false,
|
||||
})),
|
||||
)
|
||||
|
||||
function ytLink(name, artist) {
|
||||
return `https://music.youtube.com/search?q=${encodeURIComponent(`${artist ?? ''} ${name}`.trim())}`
|
||||
}
|
||||
|
||||
async function grabArtist(mbid, key) {
|
||||
if (!mbid) return
|
||||
added.value = { ...added.value, [key]: 'busy' }
|
||||
try {
|
||||
const res = await fetch('/api/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ kind: 'artist', mbid }),
|
||||
})
|
||||
const body = await res.json()
|
||||
added.value = { ...added.value, [key]: res.ok ? (body.already ? 'fanns' : 'done') : 'fel' }
|
||||
} catch {
|
||||
added.value = { ...added.value, [key]: 'fel' }
|
||||
}
|
||||
}
|
||||
|
||||
const stateLabel = { busy: '…', done: '✓', fanns: '✓ finns', fel: 'fel' }
|
||||
const trackCards = computed(() =>
|
||||
(props.item.topTracks ?? []).map((t) => ({
|
||||
kind: 'track',
|
||||
name: t.name,
|
||||
secondary: t.secondary ?? t.artist ?? '',
|
||||
motivation: t.motivation ?? '',
|
||||
year: t.year ?? '',
|
||||
mbid: t.mbid ?? t.artistMbid ?? null,
|
||||
artistMbid: t.artistMbid ?? t.mbid ?? null,
|
||||
albumMbid: t.albumMbid ?? null,
|
||||
poster: t.poster ?? null,
|
||||
inLibrary: t.inLibrary ?? false,
|
||||
})),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overlay" @click.self="emit('close')">
|
||||
<div class="modal">
|
||||
<header>
|
||||
<h3>{{ item.name }}</h3>
|
||||
<div>
|
||||
<h3>{{ item.name }}</h3>
|
||||
<p class="dim">{{ item.motivation }}</p>
|
||||
</div>
|
||||
<button class="close" @click="emit('close')">✕</button>
|
||||
</header>
|
||||
<p class="dim">{{ item.motivation }}</p>
|
||||
|
||||
<h4>{{ item.kind === 'genre' ? 'Toppartister' : 'Liknande artister' }} <span class="dim">(sådant du inte redan har)</span></h4>
|
||||
<ul>
|
||||
<li v-for="(a, i) in artists" :key="`a-${i}`">
|
||||
<span class="name">{{ a.name }}</span>
|
||||
<span class="row-actions">
|
||||
<a class="yt" :href="ytLink(a.name)" target="_blank" rel="noopener">▶ YT</a>
|
||||
<button v-if="a.mbid" class="mini" :disabled="added[`a-${i}`]" @click="grabArtist(a.mbid, `a-${i}`)">
|
||||
{{ stateLabel[added[`a-${i}`]] ?? '⤓ Hämta' }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="!artists.length" class="dim">inget kvar efter filtrering</li>
|
||||
</ul>
|
||||
|
||||
<h4>Topplåtar</h4>
|
||||
<ul>
|
||||
<li v-for="(t, i) in item.topTracks" :key="`t-${i}`">
|
||||
<span class="name">{{ t.name }} <span class="dim">— {{ t.artist }}</span></span>
|
||||
<a class="yt" :href="ytLink(t.name, t.artist)" target="_blank" rel="noopener">▶ YT</a>
|
||||
</li>
|
||||
<li v-if="!item.topTracks?.length" class="dim">inget kvar efter filtrering</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="item.mbid" class="foot">
|
||||
<button class="mini" :disabled="added.self" @click="grabArtist(item.mbid, 'self')">
|
||||
{{ stateLabel[added.self] ?? `⤓ Hämta ${item.name} till Lidarr` }}
|
||||
</button>
|
||||
<h4>Artister <span class="dim">(sådant du inte redan har)</span></h4>
|
||||
<div v-if="artistCards.length" class="grid">
|
||||
<MusicCard v-for="(a, i) in artistCards" :key="`a-${i}`" :item="a" />
|
||||
</div>
|
||||
<p v-else class="dim">inget kvar efter filtrering</p>
|
||||
|
||||
<h4>Låtar</h4>
|
||||
<div v-if="trackCards.length" class="grid">
|
||||
<MusicCard v-for="(t, i) in trackCards" :key="`t-${i}`" :item="t" />
|
||||
</div>
|
||||
<p v-else class="dim">inget kvar efter filtrering</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,17 +53,21 @@ async function grab() {
|
||||
<p v-if="item.motivation" class="motivation">{{ item.motivation }}</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button v-if="item.inLibrary || state === 'fanns'" class="have" disabled>✓ I biblioteket</button>
|
||||
<button v-else-if="state === 'done'" class="have" disabled>✓ Hämtas</button>
|
||||
<button v-else-if="item.kind === 'genre'" class="have" disabled>utforska nedan</button>
|
||||
<button v-else-if="!item.mbid" class="have" disabled title="hittades inte i Lidarrs metadata">går ej att hämta</button>
|
||||
<button v-else :disabled="state === 'busy'" @click="grab">
|
||||
{{ state === 'busy' ? 'Lägger till…' : state === 'fel' ? 'Försök igen' : '⤓ Hämta' }}
|
||||
</button>
|
||||
<template v-if="item.kind === 'genre'">
|
||||
<button @click="emit('detail', item)">🔍 Utforska</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button v-if="item.inLibrary || state === 'fanns'" class="have" disabled>✓ I biblioteket</button>
|
||||
<button v-else-if="state === 'done'" class="have" disabled>✓ Hämtas</button>
|
||||
<button v-else-if="!item.mbid" class="have" disabled title="hittades inte i Lidarrs metadata">går ej att hämta</button>
|
||||
<button v-else :disabled="state === 'busy'" @click="grab">
|
||||
{{ state === 'busy' ? 'Lägger till…' : state === 'fel' ? 'Försök igen' : '⤓ Hämta' }}
|
||||
</button>
|
||||
</template>
|
||||
<a class="yt" :href="ytUrl" target="_blank" rel="noopener" title="Öppna i YouTube Music">▶ YT</a>
|
||||
</div>
|
||||
<button v-if="item.kind === 'singer' || item.kind === 'genre'" class="more" @click="emit('detail', item)">
|
||||
{{ item.kind === 'genre' ? 'Toppartister & topplåtar →' : 'Topplåtar & liknande →' }}
|
||||
<button v-if="item.kind === 'singer'" class="more" @click="emit('detail', item)">
|
||||
Topplåtar & liknande →
|
||||
</button>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user