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:
Garret Patti
2026-04-10 18:04:29 -04:00
parent 390ce8fcc6
commit 6f86750a99
20 changed files with 161 additions and 124 deletions

View File

@@ -85,7 +85,7 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
<VideoPlayerModal
url={videoUrl}
name={movie.title}
mediaKey={`${libraryId}:${movie.id}`}
itemKey={movie.item_key!}
onTagsChanged={onTagsChanged}
onClose={() => setPlaying(false)}
onPrev={onPrev}
@@ -288,7 +288,7 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--text-secondary)' }}>
Tags
</p>
<TagSelector mediaKey={`${libraryId}:${movie.id}`} onTagsChanged={onTagsChanged} />
<TagSelector itemKey={movie.item_key!} onTagsChanged={onTagsChanged} />
</div>
</div>
</div>

View File

@@ -57,7 +57,7 @@ export default function MoviesView({ libraryId }: Props) {
const filtered = movies.filter((movie) => {
if (search && !movie.title.toLowerCase().includes(search.toLowerCase())) return false
if (selectedTagIds.size > 0) {
const movieTags = assignments[`${libraryId}:${movie.id}`] ?? []
const movieTags = assignments[movie.item_key!] ?? []
if (![...selectedTagIds].every((id) => movieTags.includes(id))) return false
}
return true