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:
@@ -31,7 +31,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
const [seriesEpisodeTags, setSeriesEpisodeTags] = useState<Record<string, string[]>>({})
|
||||
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
||||
const [showFilters, setShowFilters] = useState(true)
|
||||
const [tagPanel, setTagPanel] = useState<{ mediaKey: string; title: string } | null>(null)
|
||||
const [tagPanel, setTagPanel] = useState<{ itemKey: string; title: string } | null>(null)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
@@ -229,7 +229,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
const filteredSeries = series.filter((s) => {
|
||||
if (search && !s.title.toLowerCase().includes(search.toLowerCase())) return false
|
||||
if (selectedTagIds.size > 0) {
|
||||
const seriesTags = assignments[`${libraryId}:${s.id}`] ?? []
|
||||
const seriesTags = assignments[s.item_key!] ?? []
|
||||
const episodeTags = seriesEpisodeTags[s.id] ?? []
|
||||
const allTags = seriesTags.length === 0 ? episodeTags
|
||||
: episodeTags.length === 0 ? seriesTags
|
||||
@@ -242,7 +242,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
const filteredEpisodes = episodes.filter((ep) => {
|
||||
if (search && !ep.title.toLowerCase().includes(search.toLowerCase())) return false
|
||||
if (selectedTagIds.size > 0) {
|
||||
const epTags = assignments[`${libraryId}:${ep.id}`] ?? []
|
||||
const epTags = assignments[ep.item_key!] ?? []
|
||||
if (![...selectedTagIds].every((id) => epTags.includes(id))) return false
|
||||
}
|
||||
return true
|
||||
@@ -256,7 +256,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
<VideoPlayerModal
|
||||
url={videoUrl}
|
||||
name={playingEpisode.title}
|
||||
mediaKey={`${libraryId}:${playingEpisode.id}`}
|
||||
itemKey={playingEpisode.item_key!}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments(); fetchSeriesEpisodeTags() }}
|
||||
onClose={() => setPlayingEpisodeIndex(null)}
|
||||
onPrev={playingEpisodeIndex > 0 ? () => setPlayingEpisodeIndex((i) => (i !== null ? i - 1 : null)) : undefined}
|
||||
@@ -391,7 +391,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
<div className="absolute inset-0 flex items-center justify-center text-4xl">📺</div>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setTagPanel({ mediaKey: `${libraryId}:${s.id}`, title: s.title }) }}
|
||||
onClick={(e) => { e.stopPropagation(); setTagPanel({ itemKey: s.item_key!, title: s.title }) }}
|
||||
className="absolute top-2 left-2 w-6 h-6 rounded-full items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity hidden group-hover:flex"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.55)', color: '#fff' }}
|
||||
aria-label={`Tag ${s.title}`}
|
||||
@@ -579,7 +579,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
key={ep.id}
|
||||
episode={ep}
|
||||
onClick={() => setPlayingEpisodeIndex(episodes.indexOf(ep))}
|
||||
onTag={() => setTagPanel({ mediaKey: `${libraryId}:${ep.id}`, title: ep.title })}
|
||||
onTag={() => setTagPanel({ itemKey: ep.item_key!, title: ep.title })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -618,7 +618,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<TagSelector
|
||||
mediaKey={tagPanel.mediaKey}
|
||||
itemKey={tagPanel.itemKey}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments(); fetchSeriesEpisodeTags() }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user