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:
@@ -64,7 +64,7 @@ export async function DELETE(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Failed to delete movie directory' }, { status: 500 })
|
||||
}
|
||||
|
||||
removeAllAssignmentsForItem(`${libraryId}:${movieId}`)
|
||||
removeAllAssignmentsForItem(`${libraryId}:movie:${movieId}`)
|
||||
|
||||
return new NextResponse(null, { status: 204 })
|
||||
}
|
||||
|
||||
@@ -2,27 +2,27 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getResolvedTagsForItem, addTagToItem, removeTagFromItem } from '@/lib/tags'
|
||||
import { requireLibraryAccess } from '@/lib/auth'
|
||||
|
||||
function extractLibraryId(mediaKey: string): string | null {
|
||||
const colonIdx = mediaKey.indexOf(':')
|
||||
function extractLibraryId(itemKey: string): string | null {
|
||||
const colonIdx = itemKey.indexOf(':')
|
||||
if (colonIdx === -1) return null
|
||||
return mediaKey.slice(0, colonIdx)
|
||||
return itemKey.slice(0, colonIdx)
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const mediaKey = searchParams.get('mediaKey')
|
||||
if (!mediaKey) {
|
||||
return NextResponse.json({ error: 'mediaKey is required' }, { status: 400 })
|
||||
const itemKey = searchParams.get('itemKey')
|
||||
if (!itemKey) {
|
||||
return NextResponse.json({ error: 'itemKey is required' }, { status: 400 })
|
||||
}
|
||||
const libraryId = extractLibraryId(mediaKey)
|
||||
const libraryId = extractLibraryId(itemKey)
|
||||
if (!libraryId) {
|
||||
return NextResponse.json({ error: 'Invalid mediaKey' }, { status: 400 })
|
||||
return NextResponse.json({ error: 'Invalid itemKey' }, { status: 400 })
|
||||
}
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
return NextResponse.json(getResolvedTagsForItem(mediaKey))
|
||||
return NextResponse.json(getResolvedTagsForItem(itemKey))
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: (err as Error).message }, { status: 500 })
|
||||
}
|
||||
@@ -30,18 +30,18 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { mediaKey, tagId } = await request.json()
|
||||
if (!mediaKey || !tagId) {
|
||||
return NextResponse.json({ error: 'mediaKey and tagId are required' }, { status: 400 })
|
||||
const { itemKey, tagId } = await request.json()
|
||||
if (!itemKey || !tagId) {
|
||||
return NextResponse.json({ error: 'itemKey and tagId are required' }, { status: 400 })
|
||||
}
|
||||
const libraryId = extractLibraryId(mediaKey)
|
||||
const libraryId = extractLibraryId(itemKey)
|
||||
if (!libraryId) {
|
||||
return NextResponse.json({ error: 'Invalid mediaKey' }, { status: 400 })
|
||||
return NextResponse.json({ error: 'Invalid itemKey' }, { status: 400 })
|
||||
}
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
addTagToItem(mediaKey, tagId)
|
||||
addTagToItem(itemKey, tagId)
|
||||
return new NextResponse(null, { status: 204 })
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: (err as Error).message }, { status: 400 })
|
||||
@@ -51,19 +51,19 @@ export async function POST(request: NextRequest) {
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const mediaKey = searchParams.get('mediaKey')
|
||||
const itemKey = searchParams.get('itemKey')
|
||||
const tagId = searchParams.get('tagId')
|
||||
if (!mediaKey || !tagId) {
|
||||
return NextResponse.json({ error: 'mediaKey and tagId are required' }, { status: 400 })
|
||||
if (!itemKey || !tagId) {
|
||||
return NextResponse.json({ error: 'itemKey and tagId are required' }, { status: 400 })
|
||||
}
|
||||
const libraryId = extractLibraryId(mediaKey)
|
||||
const libraryId = extractLibraryId(itemKey)
|
||||
if (!libraryId) {
|
||||
return NextResponse.json({ error: 'Invalid mediaKey' }, { status: 400 })
|
||||
return NextResponse.json({ error: 'Invalid itemKey' }, { status: 400 })
|
||||
}
|
||||
const auth = await requireLibraryAccess(request, libraryId)
|
||||
if (auth instanceof NextResponse) return auth
|
||||
|
||||
removeTagFromItem(mediaKey, tagId)
|
||||
removeTagFromItem(itemKey, tagId)
|
||||
return new NextResponse(null, { status: 204 })
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: (err as Error).message }, { status: 400 })
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function DELETE(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Failed to delete series directory' }, { status: 500 })
|
||||
}
|
||||
|
||||
removeAllAssignmentsForItem(`${libraryId}:${seriesId}`)
|
||||
removeAllAssignmentsForItem(`${libraryId}:tv_series:${seriesId}`)
|
||||
|
||||
return new NextResponse(null, { status: 204 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user