import fs from 'fs' import path from 'path' import type { Movie } from '@/types' import { parseMovieNfo } from './nfo' const HIDDEN_FILES = /^\./ const VIDEO_EXTENSIONS = new Set(['.mkv', '.mp4', '.avi', '.mov', '.m4v', '.wmv', '.ts', '.m2ts']) function fileApiUrl(libraryId: string, relativePath: string): string { return `/api/file?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(relativePath)}` } function thumbnailApiUrl(libraryId: string, relativePath: string): string { return `/api/thumbnail?libraryId=${encodeURIComponent(libraryId)}&path=${encodeURIComponent(relativePath)}` } /** * Finds the first file in a directory whose basename (without extension) * matches the given pattern (case-insensitive). */ function findFile(dir: string, pattern: RegExp): string | null { let entries: string[] try { entries = fs.readdirSync(dir) } catch { return null } return entries.find( (e) => !HIDDEN_FILES.test(e) && pattern.test(path.basename(e, path.extname(e))) ) ?? null } function findVideoFile(dir: string): string | null { let entries: string[] try { entries = fs.readdirSync(dir) } catch { return null } return entries.find( (e) => !HIDDEN_FILES.test(e) && VIDEO_EXTENSIONS.has(path.extname(e).toLowerCase()) ) ?? null } function findNfoFile(dir: string, dirName: string): string | null { // Try {dirName}.nfo first, then movie.nfo, then any .nfo const candidates = [`${dirName}.nfo`, 'movie.nfo'] let entries: string[] try { entries = fs.readdirSync(dir) } catch { return null } for (const candidate of candidates) { if (entries.find((e) => e.toLowerCase() === candidate.toLowerCase())) { return entries.find((e) => e.toLowerCase() === candidate.toLowerCase())! } } return entries.find((e) => path.extname(e).toLowerCase() === '.nfo') ?? null } export function scanMoviesLibrary(libraryRoot: string, libraryId: string): Movie[] { let dirs: string[] try { dirs = fs .readdirSync(libraryRoot, { withFileTypes: true }) .filter((d) => d.isDirectory() && !HIDDEN_FILES.test(d.name)) .map((d) => d.name) } catch { return [] } const movies: Movie[] = [] for (const dirName of dirs) { const moviePath = path.join(libraryRoot, dirName) const videoFile = findVideoFile(moviePath) if (!videoFile) continue const nfoFile = findNfoFile(moviePath, dirName) const nfo = nfoFile ? parseMovieNfo(path.join(moviePath, nfoFile)) : null const posterFile = findFile(moviePath, /^(poster|cover|folder)$/i) const backdropFile = findFile(moviePath, /^(backdrop|fanart|background)$/i) const id = encodeURIComponent(dirName) const videoRelPath = path.join(dirName, videoFile) movies.push({ id, title: nfo?.title ?? dirName, year: nfo?.year ?? null, plot: nfo?.plot ?? null, rating: nfo?.rating ?? null, genres: nfo?.genres ?? [], runtime: nfo?.runtime ?? null, posterUrl: posterFile ? thumbnailApiUrl(libraryId, path.join(dirName, posterFile)) : null, backdropUrl: backdropFile ? fileApiUrl(libraryId, path.join(dirName, backdropFile)) : null, videoPath: videoRelPath, }) } return movies.sort((a, b) => a.title.localeCompare(b.title)) }