Unify media_key and item_key — use item_key everywhere
media_key was a lossy shortening of item_key (libraryId:lastSegment) that introduced a real collision bug: two TV episodes from different series with the same filename would share the same media_key and each other's tags. - DB migration converts existing media_tags rows from short format to full item_key by joining against media_items; ambiguous/orphaned rows are dropped - media_tags column renamed media_key → item_key - Removed itemKeyToMediaKey() from scanner; reconcileAndPrune now passes item_key directly to reKeyMediaItem - DB reader functions (tv, movies, games) now expose item_key on returned entities; frontend components use entity.item_key instead of constructing the short libraryId:id form - MixedView now constructs the full mixed_file: item_key format - Tag API renamed mediaKey param → itemKey throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,9 +32,9 @@ function initDb(db: Database.Database): void {
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS tags_name_category ON tags(name, category_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_tags (
|
||||
media_key TEXT NOT NULL,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (media_key, tag_id)
|
||||
item_key TEXT NOT NULL,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (item_key, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS libraries (
|
||||
@@ -101,6 +101,7 @@ function initDb(db: Database.Database): void {
|
||||
migrateLibrariesType(db)
|
||||
migrateMediaItemsSchema(db)
|
||||
migrateMediaItemsFingerprint(db)
|
||||
migrateMediaTagsToItemKey(db)
|
||||
seedAppSettings(db)
|
||||
}
|
||||
|
||||
@@ -177,6 +178,56 @@ function migrateMediaItemsFingerprint(db: Database.Database): void {
|
||||
}
|
||||
}
|
||||
|
||||
function migrateMediaTagsToItemKey(db: Database.Database): void {
|
||||
const row = db
|
||||
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='media_tags'")
|
||||
.get() as { sql: string } | undefined
|
||||
if (!row || !row.sql.includes('media_key')) return // Already migrated or table doesn't exist
|
||||
|
||||
// Create replacement table with item_key column
|
||||
db.exec(`
|
||||
CREATE TABLE media_tags_new (
|
||||
item_key TEXT NOT NULL,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (item_key, tag_id)
|
||||
)
|
||||
`)
|
||||
|
||||
// Build reverse mapping: short media_key → full item_key
|
||||
// Uses same logic as the old itemKeyToMediaKey: libraryId + lastSegment
|
||||
const items = db
|
||||
.prepare('SELECT item_key FROM media_items')
|
||||
.all() as { item_key: string }[]
|
||||
|
||||
const shortToFull: Record<string, string[]> = {}
|
||||
for (const { item_key } of items) {
|
||||
const firstColon = item_key.indexOf(':')
|
||||
const lastColon = item_key.lastIndexOf(':')
|
||||
const libraryId = item_key.slice(0, firstColon)
|
||||
const shortId = item_key.slice(lastColon + 1)
|
||||
const mediaKey = `${libraryId}:${shortId}`
|
||||
;(shortToFull[mediaKey] ??= []).push(item_key)
|
||||
}
|
||||
|
||||
const tagRows = db
|
||||
.prepare('SELECT media_key, tag_id FROM media_tags')
|
||||
.all() as { media_key: string; tag_id: string }[]
|
||||
|
||||
const insert = db.prepare('INSERT OR IGNORE INTO media_tags_new (item_key, tag_id) VALUES (?, ?)')
|
||||
db.transaction(() => {
|
||||
for (const { media_key, tag_id } of tagRows) {
|
||||
const candidates = shortToFull[media_key]
|
||||
if (!candidates || candidates.length !== 1) continue // orphaned or ambiguous collision
|
||||
insert.run(candidates[0], tag_id)
|
||||
}
|
||||
})()
|
||||
|
||||
db.exec(`
|
||||
DROP TABLE media_tags;
|
||||
ALTER TABLE media_tags_new RENAME TO media_tags;
|
||||
`)
|
||||
}
|
||||
|
||||
function migrateLibrariesType(db: Database.Database): void {
|
||||
const row = db
|
||||
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='libraries'")
|
||||
|
||||
Reference in New Issue
Block a user