- {selectedSeriesIndex !== null && selectedSeriesIndex > 0 && (
-
- )}
+
{selectedSeries.title}
- {selectedSeriesIndex !== null && selectedSeriesIndex < filteredSeries.length - 1 && (
-
- )}
{/* Kebab menu */}
+
+ {/* Prev — series in seasons view, season in episodes view */}
+ {(view === 'seasons'
+ ? selectedSeriesIndex !== null && selectedSeriesIndex > 0
+ : selectedSeasonIndex !== null && selectedSeasonIndex > 0) && (
+
{
+ e.stopPropagation()
+ if (view === 'seasons') openSeries(filteredSeries[selectedSeriesIndex! - 1])
+ else openSeason(seasons[selectedSeasonIndex! - 1], selectedSeasonIndex! - 1)
+ }}
+ className="absolute left-2 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full flex items-center justify-center text-lg transition-opacity hover:opacity-100 opacity-70"
+ style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
+ aria-label="Previous"
+ >
+ ‹
+
+ )}
+
+ {/* Next — series in seasons view, season in episodes view */}
+ {(view === 'seasons'
+ ? selectedSeriesIndex !== null && selectedSeriesIndex < filteredSeries.length - 1
+ : selectedSeasonIndex !== null && selectedSeasonIndex < seasons.length - 1) && (
+
{
+ e.stopPropagation()
+ if (view === 'seasons') openSeries(filteredSeries[selectedSeriesIndex! + 1])
+ else openSeason(seasons[selectedSeasonIndex! + 1], selectedSeasonIndex! + 1)
+ }}
+ className="absolute right-2 top-1/2 -translate-y-1/2 z-10 w-12 h-12 rounded-full flex items-center justify-center text-lg transition-opacity hover:opacity-100 opacity-70"
+ style={{ backgroundColor: 'rgba(0,0,0,0.4)', color: '#fff' }}
+ aria-label="Next"
+ >
+ ›
+
+ )}
{/* Right tag panel */}
From 345a05e42afffb5b98448085c6bb5f12dcf9c0f5 Mon Sep 17 00:00:00 2001
From: Garret Patti <42485635+garretpatti@users.noreply.github.com>
Date: Sat, 18 Apr 2026 00:38:04 -0400
Subject: [PATCH 4/4] fix TV show metadata refresh
---
src/app/api/nfo-refresh/route.ts | 41 +++++++++++++++++++++++++++++++-
src/components/tv/TvView.tsx | 11 +++++++--
src/lib/tv.ts | 12 ++++++----
3 files changed, 56 insertions(+), 8 deletions(-)
diff --git a/src/app/api/nfo-refresh/route.ts b/src/app/api/nfo-refresh/route.ts
index 576f4a1..dba57f3 100644
--- a/src/app/api/nfo-refresh/route.ts
+++ b/src/app/api/nfo-refresh/route.ts
@@ -120,7 +120,46 @@ export async function POST(request: NextRequest) {
status: nfo.status ?? null,
}),
})
- return NextResponse.json({ updated: true, title: nfo.title, year: nfo.year })
+
+ // Optionally also refresh every episode NFO in this series
+ let episodesUpdated = 0
+ const includeEpisodes = searchParams.get('includeEpisodes') === 'true'
+ if (includeEpisodes) {
+ type EpRow = { item_key: string; file_path: string | null; metadata: string | null }
+ const episodeRows = db
+ .prepare(`SELECT item_key, file_path, metadata FROM media_items WHERE item_type = 'tv_episode' AND item_key LIKE ?`)
+ .all(`${libraryId}:tv_episode:${encodedDirName}:%`) as EpRow[]
+
+ const updateEp = db.prepare(`
+ UPDATE media_items SET title = @title, plot = @plot, metadata = @metadata WHERE item_key = @item_key
+ `)
+
+ db.transaction(() => {
+ for (const ep of episodeRows) {
+ if (!ep.file_path) continue
+ const epDir = path.join(libraryRoot, path.dirname(ep.file_path))
+ const baseName = path.basename(ep.file_path, path.extname(ep.file_path))
+ const epNfo = parseEpisodeNfo(path.join(epDir, `${baseName}.nfo`))
+ if (!epNfo) continue
+ const epMeta = ep.metadata ? JSON.parse(ep.metadata) : {}
+ updateEp.run({
+ item_key: ep.item_key,
+ title: epNfo.title ?? null,
+ plot: epNfo.plot ?? null,
+ metadata: JSON.stringify({
+ ...epMeta,
+ episodeNumber: epNfo.episode ?? epMeta.episodeNumber ?? null,
+ seasonNumber: epNfo.season ?? epMeta.seasonNumber ?? null,
+ aired: epNfo.aired ?? null,
+ rating: epNfo.rating ?? null,
+ }),
+ })
+ episodesUpdated++
+ }
+ })()
+ }
+
+ return NextResponse.json({ updated: true, title: nfo.title, year: nfo.year, episodesUpdated })
}
if (itemType === 'tv_episode') {
diff --git a/src/components/tv/TvView.tsx b/src/components/tv/TvView.tsx
index 36ccbfe..72e3f11 100644
--- a/src/components/tv/TvView.tsx
+++ b/src/components/tv/TvView.tsx
@@ -184,11 +184,18 @@ export default function TvView({ libraryId }: Props) {
setRefreshingMeta(true)
setWarnRefresh(false)
const itemKey = `${libraryId}:tv_series:${selectedSeries.id}`
+ const currentId = selectedSeries.id
fetch(
- `/api/nfo-refresh?libraryId=${encodeURIComponent(libraryId)}&itemType=tv_series&itemKey=${encodeURIComponent(itemKey)}`,
+ `/api/nfo-refresh?libraryId=${encodeURIComponent(libraryId)}&itemType=tv_series&itemKey=${encodeURIComponent(itemKey)}&includeEpisodes=true`,
{ method: 'POST' }
)
- .then(() => fetchSeries())
+ .then(() => fetch(`/api/tv?libraryId=${encodeURIComponent(libraryId)}`))
+ .then((r) => r.json())
+ .then((data: TvSeries[]) => {
+ setSeries(data)
+ const updated = data.find((s) => s.id === currentId)
+ if (updated) setSelectedSeries(updated)
+ })
.finally(() => setRefreshingMeta(false))
}
diff --git a/src/lib/tv.ts b/src/lib/tv.ts
index 00cb0f1..bb67b65 100644
--- a/src/lib/tv.ts
+++ b/src/lib/tv.ts
@@ -3,6 +3,7 @@ import path from 'path'
import type { TvSeries, TvSeason, TvEpisode } from '@/types'
import { getDb } from './db'
import { HIDDEN_FILES, VIDEO_EXTENSIONS, fileApiUrl, thumbnailApiUrl, findFile } from './media-utils'
+import { parseTvShowNfo } from './nfo'
function isVideoFile(name: string): boolean {
return VIDEO_EXTENSIONS.has(path.extname(name).toLowerCase())
@@ -52,6 +53,7 @@ export function scanTvLibrary(libraryRoot: string, libraryId: string): TvSeries[
const posterFile = findFile(seriesPath, /^(poster|folder)$/i)
const backdropFile = findFile(seriesPath, /^(backdrop|fanart|background)$/i)
+ const nfo = parseTvShowNfo(path.join(seriesPath, 'tvshow.nfo'))
const seasonDirs = readDirs(seriesPath)
const seasonDirCount = seasonDirs.filter((sd) => {
@@ -67,11 +69,11 @@ export function scanTvLibrary(libraryRoot: string, libraryId: string): TvSeries[
series.push({
id,
- title: dirName,
- year: null,
- plot: null,
- genres: [],
- status: null,
+ title: nfo?.title ?? dirName,
+ year: nfo?.year ?? null,
+ plot: nfo?.plot ?? null,
+ genres: nfo?.genres ?? [],
+ status: nfo?.status ?? null,
posterUrl: posterFile
? thumbnailApiUrl(libraryId, path.join(dirName, posterFile))
: null,