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

@@ -10,19 +10,19 @@ interface Props {
onClose: () => void
onPrev?: () => void
onNext?: () => void
mediaKey?: string
itemKey?: string
onTagsChanged?: () => void
context?: 'mixed' | 'movies' | 'tv'
}
export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, mediaKey, onTagsChanged, context = 'mixed' }: Props) {
export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, itemKey, onTagsChanged, context = 'mixed' }: Props) {
const settings = useUserSettings()
const autoPlay = context === 'mixed' ? settings.mixedAutoplay : context === 'movies' ? settings.moviesAutoplay : settings.tvAutoplay
const loop = context === 'mixed' ? settings.mixedLoop : context === 'movies' ? settings.moviesLoop : settings.tvLoop
const muted = context === 'mixed' ? settings.mixedMuted : context === 'movies' ? settings.moviesMuted : settings.tvMuted
const overlayRef = useRef<HTMLDivElement>(null)
const [showTags, setShowTags] = useState(
() => !!mediaKey && typeof window !== 'undefined' && window.innerWidth >= 1280
() => !!itemKey && typeof window !== 'undefined' && window.innerWidth >= 1280
)
useEffect(() => {
@@ -56,7 +56,7 @@ export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, m
{name}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
{mediaKey && (
{itemKey && (
<button
onClick={(e) => { e.stopPropagation(); setShowTags((v) => !v) }}
className="w-8 h-8 rounded-full flex items-center justify-center text-sm transition-colors"
@@ -134,7 +134,7 @@ export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, m
<p className="text-xs font-semibold uppercase tracking-wider mb-3" style={{ color: 'var(--text-secondary)' }}>
Tags
</p>
<TagSelector mediaKey={mediaKey!} onTagsChanged={onTagsChanged} />
<TagSelector itemKey={itemKey!} onTagsChanged={onTagsChanged} />
</div>
</div>
) : (