Milstolpe 2+3: ikon, sök (artist/album/låt) och Hämta-till-Lidarr
All checks were successful
build-and-push / build (push) Successful in 16s
All checks were successful
build-and-push / build (push) Successful in 16s
- Ikon: SVG-favicon (indigo-gradient + balknot), används i topbar, login och som favicon - GET /api/search: artister/album via Lidarrs metadata-API (funkar även när musicbrainz.org är nere), låtar via MusicBrainz recording-sök; 503 med svenskt felmeddelande när källan inte svarar - POST /api/add: slår upp via lidarr:<mbid>, artist läggs bevakad med searchForMissingAlbums, album bevakat med searchForNewAlbum (artisten följer med obevakad), låtar mappas till album/artist; 'already' när det redan finns - Söksida med Jellyseerr-lik kortgrid: poster/fallback, typ-badge, Hämta-knapp med busy/klar/finns-lägen; sökfältet i topbaren aktivt - 7 nya tester (mappning + rutter med stubbat Lidarr), totalt 11 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup
This commit is contained in:
@@ -6,6 +6,9 @@ import Fastify from 'fastify'
|
||||
import fastifyCookie from '@fastify/cookie'
|
||||
import fastifySession from '@fastify/session'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import { lidarrClient, LidarrError } from './lidarr.js'
|
||||
import { mbClient } from './musicbrainz.js'
|
||||
import { mapLidarrSearch } from './mapping.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
@@ -18,7 +21,9 @@ function passwordOk(given, expected) {
|
||||
return a.length === b.length && timingSafeEqual(a, b)
|
||||
}
|
||||
|
||||
export function buildApp(config) {
|
||||
export function buildApp(config, deps = {}) {
|
||||
const lidarr = deps.lidarr ?? lidarrClient(config)
|
||||
const mb = deps.musicbrainz ?? mbClient()
|
||||
const app = Fastify({ logger: true, trustProxy: true })
|
||||
|
||||
app.register(fastifyCookie)
|
||||
@@ -64,6 +69,77 @@ export function buildApp(config) {
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
app.get('/api/search', async (req, reply) => {
|
||||
const q = (req.query.q ?? '').trim()
|
||||
const type = req.query.type ?? 'artist'
|
||||
if (!q) return []
|
||||
try {
|
||||
if (type === 'track') return await mb.searchTracks(q)
|
||||
return mapLidarrSearch(await lidarr.search(q), type)
|
||||
} catch (err) {
|
||||
req.log.warn({ err }, 'sökningen misslyckades')
|
||||
const source = type === 'track' ? 'MusicBrainz' : 'Lidarr'
|
||||
return reply.code(503).send({ error: `${source} svarar inte just nu — prova igen om en stund` })
|
||||
}
|
||||
})
|
||||
|
||||
// "Hämta": lägg in artist/album i Lidarr, bevakad + sök direkt.
|
||||
// Låtar mappas till sitt album (eller artisten om albumet saknas).
|
||||
app.post('/api/add', async (req, reply) => {
|
||||
const { kind, mbid, artistMbid, albumMbid } = req.body ?? {}
|
||||
|
||||
async function lookup(wantedKind, id) {
|
||||
const results = await lidarr.search(`lidarr:${id}`)
|
||||
return (results ?? []).find((r) =>
|
||||
wantedKind === 'artist' ? r.artist?.foreignArtistId === id : r.album?.foreignAlbumId === id,
|
||||
) ?? (results ?? [])[0]
|
||||
}
|
||||
|
||||
try {
|
||||
let addKind = kind
|
||||
let addId = mbid
|
||||
if (kind === 'track') {
|
||||
addKind = albumMbid ? 'album' : 'artist'
|
||||
addId = albumMbid ?? artistMbid
|
||||
if (!addId) return reply.code(400).send({ error: 'låten saknar kopplat album/artist-id' })
|
||||
}
|
||||
|
||||
const hit = await lookup(addKind, addId)
|
||||
if (!hit) return reply.code(404).send({ error: 'hittades inte i Lidarrs metadata' })
|
||||
|
||||
const profile = {
|
||||
qualityProfileId: config.lidarr.qualityProfileId,
|
||||
metadataProfileId: config.lidarr.metadataProfileId,
|
||||
rootFolderPath: config.lidarr.rootFolder,
|
||||
}
|
||||
|
||||
if (addKind === 'artist') {
|
||||
if (hit.artist.id) return { ok: true, already: true }
|
||||
await lidarr.addArtist({
|
||||
...hit.artist,
|
||||
...profile,
|
||||
monitored: true,
|
||||
addOptions: { monitor: 'all', searchForMissingAlbums: true },
|
||||
})
|
||||
} else {
|
||||
if (hit.album.id) return { ok: true, already: true }
|
||||
await lidarr.addAlbum({
|
||||
...hit.album,
|
||||
monitored: true,
|
||||
artist: { ...hit.album.artist, ...profile, monitored: false },
|
||||
addOptions: { searchForNewAlbum: true },
|
||||
})
|
||||
}
|
||||
return { ok: true, already: false }
|
||||
} catch (err) {
|
||||
if (err instanceof LidarrError && err.status === 400 && /already/i.test(err.message)) {
|
||||
return { ok: true, already: true }
|
||||
}
|
||||
req.log.error({ err }, 'kunde inte lägga till i Lidarr')
|
||||
return reply.code(502).send({ error: 'kunde inte lägga till i Lidarr — se loggen' })
|
||||
}
|
||||
})
|
||||
|
||||
// byggd frontend (finns bara i containern / efter npm run build)
|
||||
const publicDir = path.resolve(__dirname, '../public')
|
||||
if (existsSync(publicDir)) {
|
||||
|
||||
Reference in New Issue
Block a user