import path from "node:path"; const VIDEO_EXTENSIONS = new Set(["mkv", "mp4", "avi", "m4v", "ts", "wmv"]); export interface ParsedPath { type: "Movie" | "Episode"; name: string; year: number | null; seriesName: string | null; seasonNumber: number | null; episodeNumber: number | null; imdbId: string | null; tmdbId: string | null; tvdbId: string | null; container: string; } function extractYear(str: string): number | null { const m = str.match(/\((\d{4})\)/); return m ? parseInt(m[1], 10) : null; } function extractId(str: string, prefix: string): string | null { const re = new RegExp(`\\[${prefix}-([^\\]]+)\\]`, "i"); const m = str.match(re); return m ? m[1] : null; } /** Strip trailing `(YYYY)` and any bracketed provider-id tokens from a folder name, return bare title. */ function titleFromFolder(folderName: string): string { return folderName .replace(/\s*\(\d{4}\).*$/, "") .trim(); } export function parsePath(filePath: string, moviesRoot: string, tvRoot: string): ParsedPath | null { const ext = path.extname(filePath).replace(/^\./, "").toLowerCase(); if (!VIDEO_EXTENSIONS.has(ext)) return null; // Normalise roots so comparisons work regardless of trailing slash. const moviesPrefix = moviesRoot.replace(/\/+$/, "") + "/"; const tvPrefix = tvRoot.replace(/\/+$/, "") + "/"; const fileName = path.basename(filePath, `.${ext}`); if (filePath.startsWith(moviesPrefix)) { return parseMovie(filePath, moviesPrefix, fileName, ext); } if (filePath.startsWith(tvPrefix)) { return parseEpisode(filePath, tvPrefix, fileName, ext); } return null; } function parseMovie(filePath: string, moviesPrefix: string, fileName: string, ext: string): ParsedPath | null { // Path: /movies/{Folder}/{filename}.ext — folder is the immediate child of moviesRoot. const relative = filePath.slice(moviesPrefix.length); // e.g. "Hot Fuzz (2007)/Hot Fuzz (2007) [imdbid-...].mkv" const folderName = relative.split("/")[0]; const name = titleFromFolder(folderName); const year = extractYear(folderName); // IDs can appear in filename or folder name. const searchStr = `${folderName} ${fileName}`; const imdbId = extractId(searchStr, "imdbid"); const tmdbId = extractId(searchStr, "tmdbid"); return { type: "Movie", name, year, seriesName: null, seasonNumber: null, episodeNumber: null, imdbId, tmdbId, tvdbId: null, container: ext, }; } function parseEpisode(filePath: string, tvPrefix: string, fileName: string, ext: string): ParsedPath | null { // Path: /tv/{Series Folder}/Season NN/{filename}.ext const relative = filePath.slice(tvPrefix.length); const parts = relative.split("/"); if (parts.length < 2) return null; const seriesFolder = parts[0]; // e.g. "Arrow (2012) [tvdbid-257655]" const seriesName = titleFromFolder(seriesFolder); const year = extractYear(seriesFolder); const tvdbId = extractId(seriesFolder, "tvdbid"); // Season/episode from S01E01 (or S02E01-E13 for multi-episode). const seMatch = fileName.match(/S(\d{2})E(\d{2})/i); if (!seMatch) return null; const seasonNumber = parseInt(seMatch[1], 10); const episodeNumber = parseInt(seMatch[2], 10); // Episode title: everything after "- S01E01... -" up to the first "[". // e.g. "Breaking Bad (2008) - S05E03 - Hazard Pay [WEBDL-...]" const epTitleMatch = fileName.match(/S\d{2}E\d{2}(?:-E\d{2})?\s*-\s*(.+?)(?:\s*\[|$)/i); const episodeTitle = epTitleMatch ? epTitleMatch[1].trim() : fileName; return { type: "Episode", name: episodeTitle, year, seriesName, seasonNumber, episodeNumber, imdbId: null, tmdbId: null, tvdbId, container: ext, }; }