fix search/filter bugs in game and TV libraries
- Game series: filter now checks child games for both search and tag matches instead of always passing series through - TV episodes: tag selector no longer closes after picking a tag - TV episodes: filter panel now filters episodes within a season view - TV series list: series now appear when any of their episodes match the active tag filter (via new /api/tv/series-episode-tags endpoint backed by media_items) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
||||
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
||||
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)
|
||||
@@ -66,6 +67,15 @@ export default function TvView({ libraryId }: Props) {
|
||||
|
||||
useEffect(() => { fetchAssignments() }, [fetchAssignments])
|
||||
|
||||
const fetchSeriesEpisodeTags = useCallback(() => {
|
||||
fetch(`/api/tv/series-episode-tags?libraryId=${encodeURIComponent(libraryId)}`)
|
||||
.then((r) => r.json())
|
||||
.then(setSeriesEpisodeTags)
|
||||
.catch(() => {})
|
||||
}, [libraryId])
|
||||
|
||||
useEffect(() => { fetchSeriesEpisodeTags() }, [fetchSeriesEpisodeTags])
|
||||
|
||||
const openSeries = (s: TvSeries) => {
|
||||
setSelectedSeries(s)
|
||||
setView('seasons')
|
||||
@@ -188,8 +198,21 @@ 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 tags = assignments[`${libraryId}:${s.id}`] ?? []
|
||||
if (![...selectedTagIds].every((id) => tags.includes(id))) return false
|
||||
const seriesTags = assignments[`${libraryId}:${s.id}`] ?? []
|
||||
const episodeTags = seriesEpisodeTags[s.id] ?? []
|
||||
const allTags = seriesTags.length === 0 ? episodeTags
|
||||
: episodeTags.length === 0 ? seriesTags
|
||||
: [...new Set([...seriesTags, ...episodeTags])]
|
||||
if (![...selectedTagIds].every((id) => allTags.includes(id))) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
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}`] ?? []
|
||||
if (![...selectedTagIds].every((id) => epTags.includes(id))) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -203,7 +226,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
url={videoUrl}
|
||||
name={playingEpisode.title}
|
||||
mediaKey={`${libraryId}:${playingEpisode.id}`}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments() }}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments(); fetchSeriesEpisodeTags() }}
|
||||
onClose={() => setPlayingEpisodeIndex(null)}
|
||||
onPrev={playingEpisodeIndex > 0 ? () => setPlayingEpisodeIndex((i) => (i !== null ? i - 1 : null)) : undefined}
|
||||
onNext={playingEpisodeIndex < episodes.length - 1 ? () => setPlayingEpisodeIndex((i) => (i !== null ? i + 1 : null)) : undefined}
|
||||
@@ -510,11 +533,11 @@ export default function TvView({ libraryId }: Props) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{episodes.map((ep, idx) => (
|
||||
{filteredEpisodes.map((ep) => (
|
||||
<EpisodeCard
|
||||
key={ep.id}
|
||||
episode={ep}
|
||||
onClick={() => setPlayingEpisodeIndex(idx)}
|
||||
onClick={() => setPlayingEpisodeIndex(episodes.indexOf(ep))}
|
||||
onTag={() => setTagPanel({ mediaKey: `${libraryId}:${ep.id}`, title: ep.title })}
|
||||
/>
|
||||
))}
|
||||
@@ -555,7 +578,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
<div className="px-5 py-4">
|
||||
<TagSelector
|
||||
mediaKey={tagPanel.mediaKey}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments(); setTagPanel(null) }}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments(); fetchSeriesEpisodeTags() }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user