c045f6ad80
Build and Push Docker Image / build (push) Successful in 2m30s
Episodes without a title in the filename (e.g. "Lost - S02E21 - [DSNP WEBDL-1080p]") had their name set to "[DSNP WEBDL-1080p]" because the non-greedy (.+?) still matched into the bracket. Changed capture group to ([^\[]+?) so it refuses to match "[", falling back to the SxxExx identifier instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
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-...]"
|
|
// Some files have no episode title: "Lost (2004) - S02E21 - [DSNP WEBDL-1080p]..."
|
|
const epTitleMatch = fileName.match(/S\d{2}E\d{2}(?:-E\d{2})?\s*-\s*([^\[]+?)(?:\s*\[|$)/i);
|
|
const episodeTitle = epTitleMatch ? epTitleMatch[1].trim() || `S${seMatch[1]}E${seMatch[2]}` : `S${seMatch[1]}E${seMatch[2]}`;
|
|
|
|
return {
|
|
type: "Episode",
|
|
name: episodeTitle,
|
|
year,
|
|
seriesName,
|
|
seasonNumber,
|
|
episodeNumber,
|
|
imdbId: null,
|
|
tmdbId: null,
|
|
tvdbId,
|
|
container: ext,
|
|
};
|
|
}
|