1
0

feat: matchedAlbum on item detail, onLoan collection filter

This commit is contained in:
2026-09-03 21:25:47 +02:00
parent d63bc4bc12
commit f8f240f00a
4 changed files with 126 additions and 4 deletions

View File

@@ -62,3 +62,37 @@ export function resolveRipStatusBatch(
return albumKeys.has(key) ? 'ripped' : 'not_ripped'
})
}
export interface MatchedAlbum {
id: number
subsonicId: string
lastPlayedAt: string | null
}
/** Album behind an item's rip match — link wins, else confident fuzzy match. Independent of rip_override. */
export function findMatchedAlbum(db: DB, userId: number, itemId: number): MatchedAlbum | null {
const item = db
.prepare('SELECT id, title, artist FROM collection_items WHERE id = ? AND user_id = ?')
.get(itemId, userId) as { id: number; title: string; artist: string } | undefined
if (!item) return null
const link = db
.prepare(
`SELECT da.id, da.subsonic_id, da.last_played_at FROM match_links ml
JOIN digital_albums da ON da.id = ml.album_id WHERE ml.item_id = ?`
)
.get(itemId) as { id: number; subsonic_id: string; last_played_at: string | null } | undefined
if (link) {
return { id: link.id, subsonicId: link.subsonic_id, lastPlayedAt: link.last_played_at }
}
const album = db
.prepare(
`SELECT id, subsonic_id, last_played_at, title, artist FROM digital_albums WHERE user_id = ?`
)
.all(userId) as { id: number; subsonic_id: string; last_played_at: string | null; title: string; artist: string }[]
const match = album.find((a) => isConfidentMatch(item, a))
return match
? { id: match.id, subsonicId: match.subsonic_id, lastPlayedAt: match.last_played_at }
: null
}

View File

@@ -1,7 +1,7 @@
import { FastifyInstance } from 'fastify'
import type { DB } from '../db.js'
import { cacheArtwork } from '../artwork.js'
import { resolveRipStatus, resolveRipStatusBatch, type RipStatus } from '../ripstatus.js'
import { resolveRipStatus, resolveRipStatusBatch, findMatchedAlbum, type RipStatus } from '../ripstatus.js'
import { requireAuth } from './authRoutes.js'
import { discogsClientFor, discogsErrorStatus } from './lookupRoutes.js'
@@ -146,7 +146,12 @@ export async function registerCollectionRoutes(app: FastifyInstance): Promise<vo
notRipped: items.filter((i) => i.ripStatus === 'not_ripped').length,
}
const { format, ripped, q } = request.query as { format?: string; ripped?: string; q?: string }
const { format, ripped, q, onLoan } = request.query as {
format?: string
ripped?: string
q?: string
onLoan?: string
}
let filtered = items
if (format) filtered = filtered.filter((i) => i.formats.some((f: string) => f.includes(format)))
if (ripped === 'ripped' || ripped === 'not_ripped') {
@@ -158,6 +163,15 @@ export async function registerCollectionRoutes(app: FastifyInstance): Promise<vo
(i) => i.title.toLowerCase().includes(needle) || i.artist.toLowerCase().includes(needle)
)
}
if (onLoan === 'true' || onLoan === 'false') {
const want = onLoan === 'true'
filtered = filtered.filter((i) => {
const has = !!db
.prepare('SELECT id FROM loans WHERE item_id = ? AND returned_at IS NULL')
.get(i.id)
return has === want
})
}
return { items: filtered, counts }
})
@@ -165,7 +179,7 @@ export async function registerCollectionRoutes(app: FastifyInstance): Promise<vo
const db = request.server.db
const row = getItem(db, request.user!.id, Number((request.params as { id: string }).id))
if (!row) return reply.code(404).send({ error: 'not_found' })
return rowToItem(db, row)
return { ...rowToItem(db, row), matchedAlbum: findMatchedAlbum(db, request.user!.id, row.id) }
})
app.delete('/api/collection/:id', { preHandler: [requireAuth] }, async (request, reply) => {