Inbäddad mini-spelare: ▶ på korten spelar direkt i appen
All checks were successful
build-and-push / build (push) Successful in 14s
All checks were successful
build-and-push / build (push) Successful in 14s
- 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 } }] },
|
||||
|
||||
Reference in New Issue
Block a user