feat: rip status resolution with override and match links
This commit is contained in:
31
server/src/ripstatus.ts
Normal file
31
server/src/ripstatus.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { DB } from './db.js'
|
||||||
|
import { isConfidentMatch } from './matcher.js'
|
||||||
|
|
||||||
|
export type RipStatus = 'ripped' | 'not_ripped'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolution order per spec:
|
||||||
|
* 1. manual rip_override (non-null) wins
|
||||||
|
* 2. else a stored match_link means ripped
|
||||||
|
* 3. else confident fuzzy match against the user's digital_albums
|
||||||
|
*/
|
||||||
|
export function resolveRipStatus(db: DB, userId: number, itemId: number): RipStatus {
|
||||||
|
const item = db
|
||||||
|
.prepare('SELECT id, title, artist, rip_override FROM collection_items WHERE id = ? AND user_id = ?')
|
||||||
|
.get(itemId, userId) as
|
||||||
|
| { id: number; title: string; artist: string; rip_override: number | null }
|
||||||
|
| undefined
|
||||||
|
if (!item) return 'not_ripped'
|
||||||
|
|
||||||
|
if (item.rip_override !== null && item.rip_override !== undefined) {
|
||||||
|
return item.rip_override === 1 ? 'ripped' : 'not_ripped'
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = db.prepare('SELECT album_id FROM match_links WHERE item_id = ?').get(itemId)
|
||||||
|
if (link) return 'ripped'
|
||||||
|
|
||||||
|
const albums = db
|
||||||
|
.prepare('SELECT title, artist FROM digital_albums WHERE user_id = ?')
|
||||||
|
.all(userId) as { title: string; artist: string }[]
|
||||||
|
return albums.some((a) => isConfidentMatch(item, a)) ? 'ripped' : 'not_ripped'
|
||||||
|
}
|
||||||
58
server/test/ripstatus.test.ts
Normal file
58
server/test/ripstatus.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { openDatabase, type DB } from '../src/db.js'
|
||||||
|
import { resolveRipStatus } from '../src/ripstatus.js'
|
||||||
|
|
||||||
|
describe('resolveRipStatus', () => {
|
||||||
|
let db: DB
|
||||||
|
let itemId: number
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
db = openDatabase(':memory:')
|
||||||
|
db.prepare("INSERT INTO users (id, username, password_hash, is_admin) VALUES (1, 'sam', 'x', 1)").run()
|
||||||
|
db.prepare(
|
||||||
|
"INSERT INTO collection_items (id, user_id, discogs_release_id, title, artist) VALUES (10, 1, 100, 'Motion', 'The Cinematic Orchestra')"
|
||||||
|
).run()
|
||||||
|
itemId = 10
|
||||||
|
})
|
||||||
|
|
||||||
|
function addAlbum(id: number, title: string, artist: string) {
|
||||||
|
db.prepare('INSERT INTO digital_albums (id, user_id, subsonic_id, title, artist) VALUES (?, 1, ?, ?, ?)').run(
|
||||||
|
id,
|
||||||
|
`sub-${id}`,
|
||||||
|
title,
|
||||||
|
artist
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('no album and no override → not_ripped', () => {
|
||||||
|
expect(resolveRipStatus(db, 1, itemId)).toBe('not_ripped')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('confident auto-match → ripped', () => {
|
||||||
|
addAlbum(1, 'Motion!', 'Cinematic Orchestra')
|
||||||
|
expect(resolveRipStatus(db, 1, itemId)).toBe('ripped')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('same title different artist → not_ripped', () => {
|
||||||
|
addAlbum(1, 'Motion', 'Massive Attack')
|
||||||
|
expect(resolveRipStatus(db, 1, itemId)).toBe('not_ripped')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('match link → ripped even without fuzzy match', () => {
|
||||||
|
addAlbum(1, 'Motion (Remastered)', 'The Cinematic Orchestra')
|
||||||
|
db.prepare('INSERT INTO match_links (user_id, item_id, album_id) VALUES (1, 10, 1)').run()
|
||||||
|
expect(resolveRipStatus(db, 1, itemId)).toBe('ripped')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('manual override wins over everything (both directions)', () => {
|
||||||
|
addAlbum(1, 'Motion', 'The Cinematic Orchestra')
|
||||||
|
db.prepare('UPDATE collection_items SET rip_override = 0 WHERE id = 10').run()
|
||||||
|
expect(resolveRipStatus(db, 1, itemId)).toBe('not_ripped')
|
||||||
|
|
||||||
|
db.prepare('UPDATE collection_items SET rip_override = 1 WHERE id = 10').run()
|
||||||
|
expect(resolveRipStatus(db, 1, itemId)).toBe('ripped')
|
||||||
|
|
||||||
|
db.prepare('UPDATE collection_items SET rip_override = NULL WHERE id = 10').run()
|
||||||
|
expect(resolveRipStatus(db, 1, itemId)).toBe('ripped')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user