Compare commits

..

5 Commits

Author SHA1 Message Date
152bc12427 Merge pull request 'more-ui-adjustments' (#29) from more-ui-adjustments into main
All checks were successful
Build and Push Docker Image / build (push) Successful in 58s
Reviewed-on: http://gitea.lan/gpatti/MediaLore/pulls/29
2026-04-18 04:38:33 +00:00
Garret Patti
345a05e42a fix TV show metadata refresh 2026-04-18 00:38:04 -04:00
Garret Patti
0de839393a fix tv navigation 2026-04-18 00:22:02 -04:00
Garret Patti
0ff3ed8ac9 add gameview series navigation 2026-04-18 00:14:18 -04:00
Garret Patti
b2e9df8ab8 add gameview navigation 2026-04-17 23:55:33 -04:00
9 changed files with 187 additions and 42 deletions

View File

@@ -120,7 +120,46 @@ export async function POST(request: NextRequest) {
status: nfo.status ?? null, 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') { if (itemType === 'tv_episode') {

View File

@@ -30,23 +30,30 @@ export default async function LibraryPage({ params, searchParams }: Props) {
return ( return (
<div> <div>
<div className="flex items-center gap-2 mb-6"> {library.type !== 'mixed' && (
<a href="/" className="text-sm transition-colors" style={{ color: 'var(--text-secondary)' }}> <div className="flex items-center gap-2 mb-6">
Libraries <a href="/" className="text-sm transition-colors" style={{ color: 'var(--text-secondary)' }}>
</a> Libraries
<span style={{ color: 'var(--text-secondary)' }}>/</span> </a>
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}> <span style={{ color: 'var(--text-secondary)' }}>/</span>
{library.name} <span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
</span> {library.name}
{session.role === 'admin' && ( </span>
<div className="ml-auto"> {session.role === 'admin' && (
<ScanLibraryButton libraryId={id} /> <div className="ml-auto">
</div> <ScanLibraryButton libraryId={id} />
)} </div>
</div> )}
</div>
)}
{library.type === 'mixed' && session.role === 'admin' && (
<div className="flex justify-end mb-2">
<ScanLibraryButton libraryId={id} />
</div>
)}
{library.type === 'games' && <GamesView libraryId={id} />} {library.type === 'games' && <GamesView libraryId={id} />}
{library.type === 'mixed' && <MixedView libraryId={id} initialPath={subpath ?? ''} />} {library.type === 'mixed' && <MixedView libraryId={id} libraryName={library.name} initialPath={subpath ?? ''} />}
{library.type === 'movies' && <MoviesView libraryId={id} />} {library.type === 'movies' && <MoviesView libraryId={id} />}
{library.type === 'tv' && <TvView libraryId={id} />} {library.type === 'tv' && <TvView libraryId={id} />}
</div> </div>

View File

@@ -30,12 +30,14 @@ interface Props {
game: Game game: Game
libraryId: string libraryId: string
onClose: () => void onClose: () => void
onPrev?: () => void
onNext?: () => void
onTagsChanged?: () => void onTagsChanged?: () => void
onCoverUploaded?: () => void onCoverUploaded?: () => void
onDeleted?: (gameId: string) => void onDeleted?: (gameId: string) => void
} }
export default function GameDetailModal({ game, libraryId, onClose, onTagsChanged, onCoverUploaded, onDeleted }: Props) { export default function GameDetailModal({ game, libraryId, onClose, onPrev, onNext, onTagsChanged, onCoverUploaded, onDeleted }: Props) {
const overlayRef = useRef<HTMLDivElement>(null) const overlayRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null) const menuRef = useRef<HTMLDivElement>(null)
const screenshotInputRef = useRef<HTMLInputElement>(null) const screenshotInputRef = useRef<HTMLInputElement>(null)
@@ -178,7 +180,7 @@ export default function GameDetailModal({ game, libraryId, onClose, onTagsChange
{/* ── Left pane — relative container for floating controls ── */} {/* ── Left pane — relative container for floating controls ── */}
<div className="flex-1 min-h-0 min-w-0 relative" onClick={() => onClose()}> <div className="flex-1 min-h-0 min-w-0 relative" onClick={() => onClose()}>
{/* Scrollable card area */} {/* Scrollable card area */}
<div className="h-full overflow-y-auto flex items-start justify-center p-4"> <div className="h-full overflow-y-auto flex items-center justify-center p-4">
<div <div
className="w-full max-w-lg rounded-2xl overflow-hidden shadow-2xl" className="w-full max-w-lg rounded-2xl overflow-hidden shadow-2xl"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }} style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
@@ -497,6 +499,28 @@ export default function GameDetailModal({ game, libraryId, onClose, onTagsChange
</button> </button>
</div> </div>
{/* Prev / Next */}
{onPrev && (
<button
onClick={(e) => { e.stopPropagation(); onPrev() }}
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"
>
</button>
)}
{onNext && (
<button
onClick={(e) => { e.stopPropagation(); onNext() }}
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"
>
</button>
)}
</div> </div>
{/* ── Tag panel — bottom half on mobile, right sidebar on desktop ── */} {/* ── Tag panel — bottom half on mobile, right sidebar on desktop ── */}

View File

@@ -72,7 +72,10 @@ export default function GamesView({ libraryId }: Props) {
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set()) const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
const [assignments, setAssignments] = useState<Record<string, string[]>>({}) const [assignments, setAssignments] = useState<Record<string, string[]>>({})
const [filterRefreshKey, setFilterRefreshKey] = useState(0) const [filterRefreshKey, setFilterRefreshKey] = useState(0)
const [showFilters, setShowFilters] = useState(true) const [showFilters, setShowFilters] = useState(
() => typeof window !== 'undefined' && window.innerWidth >= 768
)
const [selectedGameIndex, setSelectedGameIndex] = useState<number | null>(null)
const toggleTag = (tagId: string) => const toggleTag = (tagId: string) =>
setSelectedTagIds((prev) => { setSelectedTagIds((prev) => {
@@ -147,6 +150,9 @@ export default function GamesView({ libraryId }: Props) {
}) })
const filtersActive = search !== '' || selectedTagIds.size > 0 const filtersActive = search !== '' || selectedTagIds.size > 0
const filteredGames: Game[] = filtered.flatMap((item) =>
'games' in item ? item.games : [item as Game]
)
return ( return (
<> <>
@@ -220,7 +226,7 @@ export default function GamesView({ libraryId }: Props) {
<GameCard <GameCard
key={item.id} key={item.id}
game={item} game={item}
onClick={() => setSelected(item)} onClick={() => { setSelected(item); setSelectedGameIndex(filteredGames.indexOf(item)) }}
/> />
) )
)} )}
@@ -231,11 +237,18 @@ export default function GamesView({ libraryId }: Props) {
<GameDetailModal <GameDetailModal
game={selected} game={selected}
libraryId={libraryId} libraryId={libraryId}
onClose={() => setSelected(null)} onClose={() => { setSelected(null); setSelectedGameIndex(null) }}
onPrev={selectedGameIndex !== null && selectedGameIndex > 0
? () => { const g = filteredGames[selectedGameIndex - 1]; setSelected(g); setSelectedGameIndex(selectedGameIndex - 1) }
: undefined}
onNext={selectedGameIndex !== null && selectedGameIndex < filteredGames.length - 1
? () => { const g = filteredGames[selectedGameIndex + 1]; setSelected(g); setSelectedGameIndex(selectedGameIndex + 1) }
: undefined}
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments() }} onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments() }}
onCoverUploaded={() => fetchGames(true)} onCoverUploaded={() => fetchGames(true)}
onDeleted={() => { onDeleted={() => {
setSelected(null) setSelected(null)
setSelectedGameIndex(null)
fetchGames() fetchGames()
fetchAssignments() fetchAssignments()
}} }}

View File

@@ -11,6 +11,7 @@ import { isBrowserPlayable } from '@/lib/browser-media'
interface Props { interface Props {
libraryId: string libraryId: string
libraryName: string
initialPath: string initialPath: string
} }
@@ -21,7 +22,7 @@ type ModalState =
type TagPanelState = { entry: FileEntry; itemKey: string } | null type TagPanelState = { entry: FileEntry; itemKey: string } | null
export default function MixedView({ libraryId, initialPath }: Props) { export default function MixedView({ libraryId, libraryName, initialPath }: Props) {
const [currentPath, setCurrentPath] = useState(initialPath) const [currentPath, setCurrentPath] = useState(initialPath)
const [listing, setListing] = useState<DirectoryListing | null>(null) const [listing, setListing] = useState<DirectoryListing | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -33,7 +34,9 @@ export default function MixedView({ libraryId, initialPath }: Props) {
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set()) const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
const [assignments, setAssignments] = useState<Record<string, string[]>>({}) const [assignments, setAssignments] = useState<Record<string, string[]>>({})
const [filterRefreshKey, setFilterRefreshKey] = useState(0) const [filterRefreshKey, setFilterRefreshKey] = useState(0)
const [showFilters, setShowFilters] = useState(true) const [showFilters, setShowFilters] = useState(
() => typeof window !== 'undefined' && window.innerWidth >= 768
)
const [recursiveEntries, setRecursiveEntries] = useState<FileEntry[]>([]) const [recursiveEntries, setRecursiveEntries] = useState<FileEntry[]>([])
const [recursiveLoading, setRecursiveLoading] = useState(false) const [recursiveLoading, setRecursiveLoading] = useState(false)
const [recursiveLoaded, setRecursiveLoaded] = useState(false) const [recursiveLoaded, setRecursiveLoaded] = useState(false)
@@ -339,12 +342,20 @@ export default function MixedView({ libraryId, initialPath }: Props) {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-1 mb-6 flex-wrap text-sm"> <nav className="flex items-center gap-1 mb-6 flex-wrap text-sm">
<a
href="/"
className="transition-colors"
style={{ color: 'var(--text-secondary)' }}
>
Libraries
</a>
<span style={{ color: 'var(--border)' }}>/</span>
<button <button
onClick={() => loadPath('')} onClick={() => loadPath('')}
className="transition-colors" className="transition-colors"
style={{ color: breadcrumbs.length === 0 ? 'var(--text-primary)' : 'var(--text-secondary)' }} style={{ color: breadcrumbs.length === 0 ? 'var(--text-primary)' : 'var(--text-secondary)' }}
> >
Root {libraryName}
</button> </button>
{breadcrumbs.map((segment, i) => { {breadcrumbs.map((segment, i) => {
const isLast = i === breadcrumbs.length - 1 const isLast = i === breadcrumbs.length - 1

View File

@@ -202,7 +202,7 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onPrev, on
{/* ── Left pane — relative container for floating controls ── */} {/* ── Left pane — relative container for floating controls ── */}
<div className="flex-1 min-h-0 min-w-0 relative" onClick={() => onClose()}> <div className="flex-1 min-h-0 min-w-0 relative" onClick={() => onClose()}>
{/* Scrollable card area */} {/* Scrollable card area */}
<div className="h-full overflow-y-auto flex items-start justify-center p-4"> <div className="h-full overflow-y-auto flex items-center justify-center p-4">
<div <div
className="w-full max-w-lg rounded-2xl overflow-hidden shadow-2xl" className="w-full max-w-lg rounded-2xl overflow-hidden shadow-2xl"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }} style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}

View File

@@ -20,7 +20,9 @@ export default function MoviesView({ libraryId }: Props) {
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set()) const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
const [assignments, setAssignments] = useState<Record<string, string[]>>({}) const [assignments, setAssignments] = useState<Record<string, string[]>>({})
const [filterRefreshKey, setFilterRefreshKey] = useState(0) const [filterRefreshKey, setFilterRefreshKey] = useState(0)
const [showFilters, setShowFilters] = useState(true) const [showFilters, setShowFilters] = useState(
() => typeof window !== 'undefined' && window.innerWidth >= 768
)
const [doomScrollActive, setDoomScrollActive] = useState(false) const [doomScrollActive, setDoomScrollActive] = useState(false)
const [doomScrollItems, setDoomScrollItems] = useState<DoomScrollItem[]>([]) const [doomScrollItems, setDoomScrollItems] = useState<DoomScrollItem[]>([])

View File

@@ -32,7 +32,11 @@ export default function TvView({ libraryId }: Props) {
const [assignments, setAssignments] = useState<Record<string, string[]>>({}) const [assignments, setAssignments] = useState<Record<string, string[]>>({})
const [seriesEpisodeTags, setSeriesEpisodeTags] = useState<Record<string, string[]>>({}) const [seriesEpisodeTags, setSeriesEpisodeTags] = useState<Record<string, string[]>>({})
const [filterRefreshKey, setFilterRefreshKey] = useState(0) const [filterRefreshKey, setFilterRefreshKey] = useState(0)
const [showFilters, setShowFilters] = useState(true) const [showFilters, setShowFilters] = useState(
() => typeof window !== 'undefined' && window.innerWidth >= 768
)
const [selectedSeriesIndex, setSelectedSeriesIndex] = useState<number | null>(null)
const [selectedSeasonIndex, setSelectedSeasonIndex] = useState<number | null>(null)
const [tagPanel, setTagPanel] = useState<{ itemKey: string; title: string } | null>(null) const [tagPanel, setTagPanel] = useState<{ itemKey: string; title: string } | null>(null)
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const [confirming, setConfirming] = useState(false) const [confirming, setConfirming] = useState(false)
@@ -93,6 +97,7 @@ export default function TvView({ libraryId }: Props) {
useEffect(() => { fetchSeriesEpisodeTags() }, [fetchSeriesEpisodeTags]) useEffect(() => { fetchSeriesEpisodeTags() }, [fetchSeriesEpisodeTags])
const openSeries = (s: TvSeries) => { const openSeries = (s: TvSeries) => {
setSelectedSeriesIndex(filteredSeries.indexOf(s))
setSelectedSeries(s) setSelectedSeries(s)
setView('seasons') setView('seasons')
setLoading(true) setLoading(true)
@@ -102,16 +107,12 @@ export default function TvView({ libraryId }: Props) {
.then((data: TvSeason[]) => { .then((data: TvSeason[]) => {
setSeasons(data) setSeasons(data)
setLoading(false) setLoading(false)
// Flat series: a single synthetic season (id='.') means episodes live
// directly in the series folder — skip the seasons screen automatically.
if (data.length === 1 && data[0].id === '.') {
openSeason(data[0])
}
}) })
.catch(() => { setError('Failed to load seasons'); setLoading(false) }) .catch(() => { setError('Failed to load seasons'); setLoading(false) })
} }
const openSeason = (season: TvSeason) => { const openSeason = (season: TvSeason, index?: number) => {
setSelectedSeasonIndex(index ?? seasons.indexOf(season))
setSelectedSeason(season) setSelectedSeason(season)
setView('episodes') setView('episodes')
if (showTagPanel) { if (showTagPanel) {
@@ -143,6 +144,8 @@ export default function TvView({ libraryId }: Props) {
setView('series') setView('series')
setSelectedSeries(null) setSelectedSeries(null)
setSelectedSeason(null) setSelectedSeason(null)
setSelectedSeriesIndex(null)
setSelectedSeasonIndex(null)
setMenuOpen(false) setMenuOpen(false)
setConfirming(false) setConfirming(false)
setShowTagPanel(false) setShowTagPanel(false)
@@ -153,6 +156,7 @@ export default function TvView({ libraryId }: Props) {
const goToSeasons = () => { const goToSeasons = () => {
setView('seasons') setView('seasons')
setSelectedSeason(null) setSelectedSeason(null)
setSelectedSeasonIndex(null)
setConfirming(false) setConfirming(false)
if (showTagPanel && selectedSeries?.item_key) { if (showTagPanel && selectedSeries?.item_key) {
setTagPanelItemKey(selectedSeries.item_key) setTagPanelItemKey(selectedSeries.item_key)
@@ -180,11 +184,18 @@ export default function TvView({ libraryId }: Props) {
setRefreshingMeta(true) setRefreshingMeta(true)
setWarnRefresh(false) setWarnRefresh(false)
const itemKey = `${libraryId}:tv_series:${selectedSeries.id}` const itemKey = `${libraryId}:tv_series:${selectedSeries.id}`
const currentId = selectedSeries.id
fetch( 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' } { 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)) .finally(() => setRefreshingMeta(false))
} }
@@ -601,7 +612,7 @@ export default function TvView({ libraryId }: Props) {
> >
<div className={`flex h-full w-full ${showTagPanel ? 'flex-col md:flex-row' : ''}`}> <div className={`flex h-full w-full ${showTagPanel ? 'flex-col md:flex-row' : ''}`}>
<div className="flex-1 min-h-0 min-w-0 relative" onClick={goToSeries}> <div className="flex-1 min-h-0 min-w-0 relative" onClick={goToSeries}>
<div className="h-full overflow-y-auto flex items-start justify-center p-4"> <div className="h-full overflow-y-auto flex items-center justify-center p-4">
<div <div
className="w-full max-w-3xl rounded-2xl overflow-hidden shadow-2xl" className="w-full max-w-3xl rounded-2xl overflow-hidden shadow-2xl"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }} style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
@@ -878,7 +889,7 @@ export default function TvView({ libraryId }: Props) {
{seasons.map((season) => ( {seasons.map((season) => (
<button <button
key={season.id} key={season.id}
onClick={() => openSeason(season)} onClick={() => openSeason(season, seasons.indexOf(season))}
className="group text-left rounded-xl overflow-hidden border transition-all focus:outline-none focus-visible:ring-2" className="group text-left rounded-xl overflow-hidden border transition-all focus:outline-none focus-visible:ring-2"
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)' }} style={{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)' }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
@@ -990,6 +1001,42 @@ export default function TvView({ libraryId }: Props) {
</button> </button>
</div> </div>
{/* Prev — series in seasons view, season in episodes view */}
{(view === 'seasons'
? selectedSeriesIndex !== null && selectedSeriesIndex > 0
: selectedSeasonIndex !== null && selectedSeasonIndex > 0) && (
<button
onClick={(e) => {
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"
>
</button>
)}
{/* Next — series in seasons view, season in episodes view */}
{(view === 'seasons'
? selectedSeriesIndex !== null && selectedSeriesIndex < filteredSeries.length - 1
: selectedSeasonIndex !== null && selectedSeasonIndex < seasons.length - 1) && (
<button
onClick={(e) => {
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"
>
</button>
)}
</div> </div>
{/* Right tag panel */} {/* Right tag panel */}

View File

@@ -3,6 +3,7 @@ import path from 'path'
import type { TvSeries, TvSeason, TvEpisode } from '@/types' import type { TvSeries, TvSeason, TvEpisode } from '@/types'
import { getDb } from './db' import { getDb } from './db'
import { HIDDEN_FILES, VIDEO_EXTENSIONS, fileApiUrl, thumbnailApiUrl, findFile } from './media-utils' import { HIDDEN_FILES, VIDEO_EXTENSIONS, fileApiUrl, thumbnailApiUrl, findFile } from './media-utils'
import { parseTvShowNfo } from './nfo'
function isVideoFile(name: string): boolean { function isVideoFile(name: string): boolean {
return VIDEO_EXTENSIONS.has(path.extname(name).toLowerCase()) 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 posterFile = findFile(seriesPath, /^(poster|folder)$/i)
const backdropFile = findFile(seriesPath, /^(backdrop|fanart|background)$/i) const backdropFile = findFile(seriesPath, /^(backdrop|fanart|background)$/i)
const nfo = parseTvShowNfo(path.join(seriesPath, 'tvshow.nfo'))
const seasonDirs = readDirs(seriesPath) const seasonDirs = readDirs(seriesPath)
const seasonDirCount = seasonDirs.filter((sd) => { const seasonDirCount = seasonDirs.filter((sd) => {
@@ -67,11 +69,11 @@ export function scanTvLibrary(libraryRoot: string, libraryId: string): TvSeries[
series.push({ series.push({
id, id,
title: dirName, title: nfo?.title ?? dirName,
year: null, year: nfo?.year ?? null,
plot: null, plot: nfo?.plot ?? null,
genres: [], genres: nfo?.genres ?? [],
status: null, status: nfo?.status ?? null,
posterUrl: posterFile posterUrl: posterFile
? thumbnailApiUrl(libraryId, path.join(dirName, posterFile)) ? thumbnailApiUrl(libraryId, path.join(dirName, posterFile))
: null, : null,