diff --git a/src/app/api/movies/route.ts b/src/app/api/movies/route.ts index 12ba6e9..cbb2ee6 100644 --- a/src/app/api/movies/route.ts +++ b/src/app/api/movies/route.ts @@ -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 }) } diff --git a/src/app/api/tags/assignments/route.ts b/src/app/api/tags/assignments/route.ts index 887ddd7..1b6ddb5 100644 --- a/src/app/api/tags/assignments/route.ts +++ b/src/app/api/tags/assignments/route.ts @@ -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 }) diff --git a/src/app/api/tv/route.ts b/src/app/api/tv/route.ts index a297ce9..b063d0b 100644 --- a/src/app/api/tv/route.ts +++ b/src/app/api/tv/route.ts @@ -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 }) } diff --git a/src/components/DoomScrollView.tsx b/src/components/DoomScrollView.tsx index e9c4c38..af00d73 100644 --- a/src/components/DoomScrollView.tsx +++ b/src/components/DoomScrollView.tsx @@ -7,7 +7,7 @@ export interface DoomScrollItem { url: string name: string mediaType: 'video' | 'image' - mediaKey?: string + itemKey?: string } interface Props { diff --git a/src/components/games/GameDetailModal.tsx b/src/components/games/GameDetailModal.tsx index 30709ef..8d41fcb 100644 --- a/src/components/games/GameDetailModal.tsx +++ b/src/components/games/GameDetailModal.tsx @@ -142,7 +142,7 @@ export default function GameDetailModal({ game, libraryId, onClose, onTagsChange

Tags

- + diff --git a/src/components/games/GamesView.tsx b/src/components/games/GamesView.tsx index 9379235..a3150a2 100644 --- a/src/components/games/GamesView.tsx +++ b/src/components/games/GamesView.tsx @@ -81,7 +81,7 @@ export default function GamesView({ libraryId }: Props) { if (!searchMatch) return false if (selectedTagIds.size > 0) { return item.games.some((g) => { - const gameTags = assignments[`${libraryId}:${g.id}`] ?? [] + const gameTags = assignments[g.item_key!] ?? [] return [...selectedTagIds].every((id) => gameTags.includes(id)) }) } @@ -89,7 +89,7 @@ export default function GamesView({ libraryId }: Props) { } if (search && !item.title.toLowerCase().includes(search.toLowerCase())) return false if (selectedTagIds.size > 0) { - const gameTags = assignments[`${libraryId}:${item.id}`] ?? [] + const gameTags = assignments[item.item_key!] ?? [] if (![...selectedTagIds].every((id) => gameTags.includes(id))) return false } return true diff --git a/src/components/mixed/ImageLightbox.tsx b/src/components/mixed/ImageLightbox.tsx index cf27d0a..ebbc7f6 100644 --- a/src/components/mixed/ImageLightbox.tsx +++ b/src/components/mixed/ImageLightbox.tsx @@ -9,14 +9,14 @@ interface Props { onClose: () => void onPrev?: () => void onNext?: () => void - mediaKey?: string + itemKey?: string onTagsChanged?: () => void } -export default function ImageLightbox({ url, name, onClose, onPrev, onNext, mediaKey, onTagsChanged }: Props) { +export default function ImageLightbox({ url, name, onClose, onPrev, onNext, itemKey, onTagsChanged }: Props) { const overlayRef = useRef(null) const [showTags, setShowTags] = useState( - () => !!mediaKey && typeof window !== 'undefined' && window.innerWidth >= 1280 + () => !!itemKey && typeof window !== 'undefined' && window.innerWidth >= 1280 ) useEffect(() => { @@ -50,7 +50,7 @@ export default function ImageLightbox({ url, name, onClose, onPrev, onNext, medi {name}
- {mediaKey && ( + {itemKey && (
) : ( diff --git a/src/components/mixed/MixedView.tsx b/src/components/mixed/MixedView.tsx index 3d92a37..783bfe4 100644 --- a/src/components/mixed/MixedView.tsx +++ b/src/components/mixed/MixedView.tsx @@ -14,11 +14,11 @@ interface Props { } type ModalState = - | { type: 'video'; url: string; name: string; mediaKey: string; mediaIndex: number } - | { type: 'image'; url: string; name: string; mediaKey: string; mediaIndex: number } + | { type: 'video'; url: string; name: string; itemKey: string; mediaIndex: number } + | { type: 'image'; url: string; name: string; itemKey: string; mediaIndex: number } | null -type TagPanelState = { entry: FileEntry; mediaKey: string } | null +type TagPanelState = { entry: FileEntry; itemKey: string } | null export default function MixedView({ libraryId, initialPath }: Props) { const [currentPath, setCurrentPath] = useState(initialPath) @@ -100,11 +100,11 @@ export default function MixedView({ libraryId, initialPath }: Props) { fetchRecursive() }, [filtersActive, fetchRecursive]) - const mediaKeyFor = (entry: FileEntry) => { + const itemKeyFor = (entry: FileEntry) => { // In recursive mode entry.name is already the full relative path from the library root - if (filtersActive) return `${libraryId}:${encodeURIComponent(entry.name)}` + if (filtersActive) return `${libraryId}:mixed_file:${encodeURIComponent(entry.name)}` const rel = currentPath ? `${currentPath}/${entry.name}` : entry.name - return `${libraryId}:${encodeURIComponent(rel)}` + return `${libraryId}:mixed_file:${encodeURIComponent(rel)}` } const sourceEntries = filtersActive ? recursiveEntries : (listing?.entries ?? []) @@ -112,7 +112,7 @@ export default function MixedView({ libraryId, initialPath }: Props) { const filteredEntries = sourceEntries.filter((entry) => { if (search && !entry.name.toLowerCase().includes(search.toLowerCase())) return false if (selectedTagIds.size > 0 && entry.type !== 'directory') { - const entryTags = assignments[mediaKeyFor(entry)] ?? [] + const entryTags = assignments[itemKeyFor(entry)] ?? [] if (![...selectedTagIds].every((id) => entryTags.includes(id))) return false } return true @@ -124,11 +124,11 @@ export default function MixedView({ libraryId, initialPath }: Props) { const openMediaEntry = (entry: FileEntry, idx: number) => { if (!entry.url) return - const mediaKey = mediaKeyFor(entry) + const itemKey = itemKeyFor(entry) if (entry.mediaType === 'video') { - setModal({ type: 'video', url: entry.url, name: entry.name, mediaKey, mediaIndex: idx }) + setModal({ type: 'video', url: entry.url, name: entry.name, itemKey, mediaIndex: idx }) } else if (entry.mediaType === 'image') { - setModal({ type: 'image', url: entry.url, name: entry.name, mediaKey, mediaIndex: idx }) + setModal({ type: 'image', url: entry.url, name: entry.name, itemKey, mediaIndex: idx }) } } @@ -155,7 +155,7 @@ export default function MixedView({ libraryId, initialPath }: Props) { } const handleTagEntry = (entry: FileEntry) => { - setTagPanel({ entry, mediaKey: mediaKeyFor(entry) }) + setTagPanel({ entry, itemKey: itemKeyFor(entry) }) } const navigateUp = () => { @@ -326,7 +326,7 @@ export default function MixedView({ libraryId, initialPath }: Props) { { setFilterRefreshKey((k) => k + 1); fetchAssignments() }} onClose={() => setModal(null)} onPrev={modal.mediaIndex > 0 ? () => navigateModal(-1) : undefined} @@ -337,7 +337,7 @@ export default function MixedView({ libraryId, initialPath }: Props) { { setFilterRefreshKey((k) => k + 1); fetchAssignments() }} onClose={() => setModal(null)} onPrev={modal.mediaIndex > 0 ? () => navigateModal(-1) : undefined} @@ -378,7 +378,7 @@ export default function MixedView({ libraryId, initialPath }: Props) {
{ setFilterRefreshKey((k) => k + 1); fetchAssignments() }} />
diff --git a/src/components/mixed/VideoPlayerModal.tsx b/src/components/mixed/VideoPlayerModal.tsx index eb36aa0..495aa5a 100644 --- a/src/components/mixed/VideoPlayerModal.tsx +++ b/src/components/mixed/VideoPlayerModal.tsx @@ -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(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}
- {mediaKey && ( + {itemKey && (
) : ( diff --git a/src/components/movies/MovieDetailModal.tsx b/src/components/movies/MovieDetailModal.tsx index 2abe176..7baa477 100644 --- a/src/components/movies/MovieDetailModal.tsx +++ b/src/components/movies/MovieDetailModal.tsx @@ -85,7 +85,7 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on setPlaying(false)} onPrev={onPrev} @@ -288,7 +288,7 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on

Tags

- + diff --git a/src/components/movies/MoviesView.tsx b/src/components/movies/MoviesView.tsx index f4ac72b..bb28d68 100644 --- a/src/components/movies/MoviesView.tsx +++ b/src/components/movies/MoviesView.tsx @@ -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 diff --git a/src/components/tags/TagSelector.tsx b/src/components/tags/TagSelector.tsx index 4f03cc9..527b4e4 100644 --- a/src/components/tags/TagSelector.tsx +++ b/src/components/tags/TagSelector.tsx @@ -5,7 +5,7 @@ import type { Tag, TagCategory } from '@/types' import TagBadge from './TagBadge' interface Props { - mediaKey: string + itemKey: string onTagsChanged?: () => void } @@ -14,7 +14,7 @@ interface AllTags { tags: Tag[] } -export default function TagSelector({ mediaKey, onTagsChanged }: Props) { +export default function TagSelector({ itemKey, onTagsChanged }: Props) { const [assigned, setAssigned] = useState<{ tags: Tag[]; categories: TagCategory[] }>({ tags: [], categories: [], @@ -39,10 +39,10 @@ export default function TagSelector({ mediaKey, onTagsChanged }: Props) { const [savingCategory, setSavingCategory] = useState(false) const fetchAssigned = useCallback(() => { - return fetch(`/api/tags/assignments?mediaKey=${encodeURIComponent(mediaKey)}`) + return fetch(`/api/tags/assignments?itemKey=${encodeURIComponent(itemKey)}`) .then((r) => r.json()) .then((data) => setAssigned(data)) - }, [mediaKey]) + }, [itemKey]) const fetchAll = useCallback(() => { return Promise.all([ @@ -66,14 +66,14 @@ export default function TagSelector({ mediaKey, onTagsChanged }: Props) { try { if (isAssigned(tag.id)) { await fetch( - `/api/tags/assignments?mediaKey=${encodeURIComponent(mediaKey)}&tagId=${encodeURIComponent(tag.id)}`, + `/api/tags/assignments?itemKey=${encodeURIComponent(itemKey)}&tagId=${encodeURIComponent(tag.id)}`, { method: 'DELETE' } ) } else { await fetch('/api/tags/assignments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mediaKey, tagId: tag.id }), + body: JSON.stringify({ itemKey, tagId: tag.id }), }) } await fetchAssigned() @@ -106,7 +106,7 @@ export default function TagSelector({ mediaKey, onTagsChanged }: Props) { fetch('/api/tags/assignments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mediaKey, tagId: newTag.id }), + body: JSON.stringify({ itemKey, tagId: newTag.id }), }), fetchAll(), ]) diff --git a/src/components/tv/TvView.tsx b/src/components/tv/TvView.tsx index c5fb257..87cfafe 100644 --- a/src/components/tv/TvView.tsx +++ b/src/components/tv/TvView.tsx @@ -31,7 +31,7 @@ export default function TvView({ libraryId }: Props) { const [seriesEpisodeTags, setSeriesEpisodeTags] = useState>({}) 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) { { 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) {
📺
)}