From 9874d383eab4dece2ae1c0912552df4b25221230 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 1 Aug 2026 04:38:33 +0200 Subject: [PATCH] =?UTF-8?q?Inb=C3=A4ddad=20mini-spelare:=20=E2=96=B6=20p?= =?UTF-8?q?=C3=A5=20korten=20spelar=20direkt=20i=20appen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/play?q=: slår upp spelbart videoId via YouTube Data API (kategori musik, YOUTUBE_API_KEY i stackens .env) - MiniPlayer: fast spelare nere till höger (YouTube-embed, autoplay), ligger kvar medan man bläddrar; stängs med ✕ - MusicCard: ▶-knapp på artist/album/låt/sångare (genrer behåller sina två knappar); YT-länken heter nu 'YT ↗' - 2 nya tester, totalt 31 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup --- backend/src/app.js | 27 +++++++++++ backend/src/config.js | 3 ++ backend/test/search.test.js | 32 +++++++++++++ frontend/src/App.vue | 2 + frontend/src/components/MiniPlayer.vue | 64 ++++++++++++++++++++++++++ frontend/src/components/MusicCard.vue | 50 ++++++++++++++++---- frontend/src/player.js | 4 ++ 7 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/MiniPlayer.vue create mode 100644 frontend/src/player.js diff --git a/backend/src/app.js b/backend/src/app.js index 85375f5..f64308e 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -235,6 +235,33 @@ export function buildApp(config, deps = {}) { } }) + // Slår upp ett spelbart video-id för mini-spelaren (YouTube Data API, + // kategori 10 = musik) + app.get('/api/play', async (req, reply) => { + const q = (req.query.q ?? '').trim() + if (!q) return reply.code(400).send({ error: 'sökterm saknas' }) + const key = config.youtube?.apiKey + if (!key) return reply.code(500).send({ error: 'YOUTUBE_API_KEY saknas i stackens .env' }) + try { + const url = + `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video` + + `&videoCategoryId=10&maxResults=1&q=${encodeURIComponent(q)}&key=${encodeURIComponent(key)}` + const res = await (deps.playFetch ?? fetch)(url) + if (!res.ok) return reply.code(502).send({ error: `YouTube svarade ${res.status}` }) + const js = await res.json() + const hit = js.items?.[0] + if (!hit) return reply.code(404).send({ error: 'ingen video hittades' }) + return { + videoId: hit.id.videoId, + title: hit.snippet?.title ?? q, + channel: hit.snippet?.channelTitle ?? '', + } + } catch (err) { + req.log.warn({ err }, 'play-uppslag misslyckades') + return reply.code(502).send({ error: 'kunde inte nå YouTube' }) + } + }) + app.get('/api/suggest/items', async (req) => { const type = SUGGEST_TYPES.includes(req.query.type) ? req.query.type : 'mix' return suggest.allItems(type) diff --git a/backend/src/config.js b/backend/src/config.js index 74d3d63..cd82081 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -20,6 +20,9 @@ export function loadConfig(env = process.env) { apiKey: env.GEMINI_API_KEY ?? '', model: env.GEMINI_MODEL ?? 'gemini-2.5-flash', }, + youtube: { + apiKey: env.YOUTUBE_API_KEY ?? '', + }, youtubarr: { url: env.YOUTUBARR_URL ?? 'http://youtubarr', dataDir: env.YOUTUBARR_DATA ?? '/youtubarr-data', diff --git a/backend/test/search.test.js b/backend/test/search.test.js index 9d8b846..85ec32d 100644 --- a/backend/test/search.test.js +++ b/backend/test/search.test.js @@ -130,6 +130,38 @@ test('POST /api/add med låt går via albumet', async () => { await app.close() }) +test('GET /api/play slår upp videoId via YouTube Data API', async () => { + const playConfig = { ...config, youtube: { apiKey: 'yt-nyckel' } } + const app = buildApp(playConfig, { + lidarr: {}, + musicbrainz: {}, + playFetch: async (url) => { + assert.match(url, /videoCategoryId=10/) + assert.match(url, /q=Powerwolf%20Sanctified/) + return { + ok: true, + json: async () => ({ + items: [{ id: { videoId: 'abc123' }, snippet: { title: 'Sanctified with Dynamite', channelTitle: 'Powerwolf - Topic' } }], + }), + } + }, + }) + const cookie = await loggedIn(app) + const res = await app.inject({ method: 'GET', url: '/api/play?q=Powerwolf%20Sanctified', headers: { cookie } }) + assert.equal(res.statusCode, 200) + assert.equal(res.json().videoId, 'abc123') + await app.close() +}) + +test('GET /api/play utan nyckel ger begripligt fel', async () => { + const app = buildApp({ ...config, youtube: { apiKey: '' } }, { lidarr: {}, musicbrainz: {} }) + const cookie = await loggedIn(app) + const res = await app.inject({ method: 'GET', url: '/api/play?q=x', headers: { cookie } }) + assert.equal(res.statusCode, 500) + assert.match(res.json().error, /YOUTUBE_API_KEY/) + await app.close() +}) + test('POST /api/add svarar already för befintlig artist', async () => { const app = buildApp(config, { lidarr: { search: async () => [{ artist: { ...powerwolf, id: 12 } }] }, diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 315e151..eb1893a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,6 +1,7 @@ + + + + diff --git a/frontend/src/components/MusicCard.vue b/frontend/src/components/MusicCard.vue index fba07fd..87cb9ff 100644 --- a/frontend/src/components/MusicCard.vue +++ b/frontend/src/components/MusicCard.vue @@ -1,17 +1,32 @@