Files
filename-tool/src/lib/FileNameVideoMetaExtractor.ts
T
2026-01-09 22:33:03 +01:00

301 lines
12 KiB
TypeScript

import {FileNameCleaner} from "./tools/FileNameCleaner.js";
import {StringArrayOperation} from "./tools/StringArrayOperation.js";
import {SerieFilesNameAnalysisTool} from "./tools/video/SerieFilesNamaAnalysisTool.js";
import path from 'path';
/**
* Result of video file metadata extraction
*/
export interface VideoFileMetadata {
originalTitle?: string;
season?: string;
episode?: string;
increment?: string;
movieYear?: string;
videoType?: 'tvshow' | 'movie';
extra?: string;
}
const MIN_TITLE_LENGTH = 3;
export class FileNameVideoMetaExtractor {
fileNameCleaner = new FileNameCleaner();
stringArrayOperation = new StringArrayOperation();
serieFilesNameAnalysisTool: SerieFilesNameAnalysisTool;
constructor(
private watchFolderList: string[],
private episodePatterns:string[],
private seasonAndEpisodePatterns:string[],
private seasonPatterns:string[],
private extraEpKeyWords:string[],
private keywordsArray:string[],
private substringArray:string[],
private soloEp,
) {
this.serieFilesNameAnalysisTool = new SerieFilesNameAnalysisTool(
episodePatterns,
seasonAndEpisodePatterns,
seasonPatterns,
extraEpKeyWords,
keywordsArray,
substringArray,
soloEp
);
}
/**
* Returns the name of the folder at a specified parent level from the file path,
* or null if the file is not under any of the media root folders defined in watchFolderList,
* or if no media root folder is defined, calculates based on the file path directly.
* @param filePath The file path to check.
* @param parent The parent level to adjust the depth check (1 for parent folder, 2 for grandparent folder, etc.).
* @return The name of the folder at the specified parent level or null.
*/
getParentFolder(filePath: string, parent: number): string | null {
if (parent < 0 || !Number.isInteger(parent)) {
throw new Error('Parent parameter must be a non-negative integer.');
}
const normalizedFilePath = path.normalize(filePath);
const folderList = this.watchFolderList;
if (folderList) {
for (let folder of folderList) {
const mediaRootFolder = path.normalize(folder);
if (normalizedFilePath.startsWith(mediaRootFolder)) {
const pathRelative = path.relative(mediaRootFolder, normalizedFilePath);
const pathParts = pathRelative.split(path.sep);
pathParts.reverse(); // Reverse the array to make it easier to access the parent levels.
if (pathParts.length >= parent) {
return pathParts[parent]; // Adjusted for zero-indexed arrays.
}
return null; // The specified parent level is too deep.
}
}
return null; // File path is not under any of the media root folders.
}
// General solution for any parent level when no watchFolderList is defined.
let targetFolder = normalizedFilePath;
for (let i = 0; i < parent; i++) {
targetFolder = path.dirname(targetFolder);
}
if (targetFolder === path.sep || !targetFolder) {
return null; // Reached the root or an invalid state.
}
return path.basename(targetFolder);
}
/**
* Title cleaning
*/
cleanTitle(simpleTitle: string): string {
if (!simpleTitle) {
return null;
}
simpleTitle = this.fileNameCleaner.cleanSpecialChars(simpleTitle);
simpleTitle = this.serieFilesNameAnalysisTool.removeKeywordsFromFilename(simpleTitle);
simpleTitle = this.fileNameCleaner.cleanTags(simpleTitle);
simpleTitle = this.serieFilesNameAnalysisTool.removeBeyondHyphen(simpleTitle);
simpleTitle = this.fileNameCleaner.removeTagSeparator(simpleTitle);
simpleTitle = this.fileNameCleaner.removeYear(simpleTitle);
simpleTitle = this.serieFilesNameAnalysisTool.removeSFV(simpleTitle);
simpleTitle = this.serieFilesNameAnalysisTool.removeSeasonAndEpisode(simpleTitle);
simpleTitle = this.fileNameCleaner.cleanDot(simpleTitle);
//simpleTitle = this.fileNameCleaner.removeNumbers(simpleTitle);
simpleTitle = this.fileNameCleaner.removeHyphenAtTheEndAndStart(simpleTitle);
simpleTitle = this.fileNameCleaner.cleanSpace(simpleTitle);
simpleTitle = this.fileNameCleaner.convertFirstLetterToUpperCase(simpleTitle);
simpleTitle = this.fileNameCleaner.sanitizeFilename(simpleTitle);
return simpleTitle;
}
computeTitleFromFileName(fileName: string): string {
return this.cleanTitle(fileName);
}
computeTitleFromParentFolder(filePath: string): string {
return this.cleanTitle(this.getParentFolder(filePath, 1));
}
computeTitleFromGrandParentFolder(filePath: string): string {
return this.cleanTitle(this.getParentFolder(filePath, 2));
}
computeTitleFromFilePath(filePath: string): string | undefined {
let filename = this.fileNameCleaner.getFilenameFromPath(filePath);
let titleFromFileName = this.computeTitleFromFileName(filename);
let titleFromParentFolder = this.computeTitleFromParentFolder(filePath);
let titleFromGrandParentFolder = this.computeTitleFromGrandParentFolder(filePath);
let simpleTitle = this.stringArrayOperation.longestString([titleFromFileName, titleFromParentFolder, titleFromGrandParentFolder]);
let selectedTitle;
if (titleFromFileName && titleFromFileName.length > MIN_TITLE_LENGTH) {
selectedTitle = titleFromFileName;
} else if (titleFromParentFolder && titleFromParentFolder.length > MIN_TITLE_LENGTH) {
selectedTitle = titleFromParentFolder;
} else if (titleFromGrandParentFolder && titleFromGrandParentFolder.length > MIN_TITLE_LENGTH) {
selectedTitle = titleFromGrandParentFolder;
} else {
selectedTitle = simpleTitle;
}
if (selectedTitle && selectedTitle.length > MIN_TITLE_LENGTH) {
return selectedTitle;
}
return undefined;
}
/**
* Season Ep cleaning
*/
computeSeasonFromParentFolder(filePath: string): number {
let parentFolder = this.getParentFolder(filePath, 1);
if (!parentFolder) {
return -1;
}
let parentFolderName = path.basename(path.dirname(filePath));
parentFolderName = this.serieFilesNameAnalysisTool.removeKeywordsFromFilename(parentFolderName);
parentFolderName = this.fileNameCleaner.cleanTags(parentFolderName);
parentFolderName = this.fileNameCleaner.cleanSpecialChars(parentFolderName);
parentFolderName = this.fileNameCleaner.cleanDot(parentFolderName);
parentFolderName = this.fileNameCleaner.cleanSpace(parentFolderName);
return this.serieFilesNameAnalysisTool.findSeasonFromParentFolder(parentFolderName);
}
computeSeasonAndEpisodeFromFilename(filename: string): [number, number] { // season , episode
filename = this.serieFilesNameAnalysisTool.removeKeywordsFromFilename(filename);
filename = this.serieFilesNameAnalysisTool.removeSFV(filename);
filename = this.fileNameCleaner.cleanSpecialChars(filename);
filename = this.fileNameCleaner.cleanSpace(filename);
return this.serieFilesNameAnalysisTool.findSeasonAndEpisode(filename);
}
computeSeasonAndEpisodeFromFilePath(filePath: string): { season?: string; episode?: string; increment?: string } {
const result: { season?: string; episode?: string; increment?: string } = {};
let filename = this.fileNameCleaner.getFilenameFromPath(filePath);
const seasonFromParentFolder = this.computeSeasonFromParentFolder(filePath);
let [sea, ep] = this.computeSeasonAndEpisodeFromFilename(filename);
//season and ep 0 can exist (but < 0 is not possible)
// first trust file then trust parent folder
if (sea >= 0) {
result.season = "" + sea;
} else if (seasonFromParentFolder >= 0) {
sea = seasonFromParentFolder;
result.season = "" + sea;
}
if (ep >= 0) {
result.episode = "" + ep;
}
if (ep >= 0) {
if (sea < 0) {
sea = 1;
//assume season 1
result.season = "1";
}
//max ep is 9999
result.increment = "" + (sea * 10000 + ep);
}
return result;
}
/**
* Movie Year cleaning
*/
computeMovieYearFromFilename(filename: string): number {
//year are most probably in a tag so no cleaning eg "(2020) or [2020] or {2020} or 2020"
return this.serieFilesNameAnalysisTool.findMovieYear(filename);
}
computeMovieYearFromParentFolder(filePath: string): number {
let parentFolder = this.getParentFolder(filePath, 1);
if (!parentFolder) {
return null;
}
let parentFolderName = path.basename(path.dirname(filePath));
parentFolderName = this.serieFilesNameAnalysisTool.removeKeywordsFromFilename(parentFolderName);
parentFolderName = this.fileNameCleaner.cleanSpecialChars(parentFolderName);
parentFolderName = this.fileNameCleaner.cleanDot(parentFolderName);
parentFolderName = this.fileNameCleaner.cleanSpace(parentFolderName);
return this.serieFilesNameAnalysisTool.findMovieYear(parentFolderName);
}
computeMovieYearFromFilePath(filePath: string): string | undefined {
let filename = this.fileNameCleaner.getFilenameFromPath(filePath);
let movieYear = this.computeMovieYearFromFilename(filename);
let movieYearFromParentFolder = this.computeMovieYearFromParentFolder(filePath);
//first trust file then trust parent folder
if (movieYear > 0) {
return "" + movieYear;
} else if (movieYearFromParentFolder > 0) {
return "" + movieYearFromParentFolder;
}
return undefined;
}
/**
* Extract metadata from a video file path
* @returns Plain object with extracted metadata
*/
public extractVideoFileMetadata(filePath: string): VideoFileMetadata {
const result: VideoFileMetadata = {};
// Extract title
const title = this.computeTitleFromFilePath(filePath);
if (title) {
result.originalTitle = title;
}
// Extract movie year
const movieYear = this.computeMovieYearFromFilePath(filePath);
if (movieYear) {
result.movieYear = movieYear;
}
// Extract season and episode
const seasonEpisode = this.computeSeasonAndEpisodeFromFilePath(filePath);
if (seasonEpisode.season) result.season = seasonEpisode.season;
if (seasonEpisode.episode) result.episode = seasonEpisode.episode;
if (seasonEpisode.increment) result.increment = seasonEpisode.increment;
// Check for extra flag
if (this.serieFilesNameAnalysisTool.isExtra(filePath)) {
result.extra = "true";
}
// Compute video type
const season = result.season ? parseInt(result.season) : -1;
const episode = result.episode ? parseInt(result.episode) : -1;
const year = result.movieYear ? parseInt(result.movieYear) : -1;
if (season > 0 || episode > 0) {
result.videoType = "tvshow";
} else if (year > 1800 && year < 2100) {
result.videoType = "movie";
}
return result;
}
/**
* @deprecated Use extractVideoFileMetadata instead - returns plain object
*/
public processVideoFile(filePath: string, metadata: any): void {
const result = this.extractVideoFileMetadata(filePath);
// Write to metadata object for backwards compatibility
if (result.originalTitle) metadata.at("originalTitle").set(result.originalTitle);
if (result.movieYear) metadata.at("movieYear").set(result.movieYear);
if (result.season) metadata.at("season").set(result.season);
if (result.episode) metadata.at("episode").set(result.episode);
if (result.increment) metadata.at("increment").set(result.increment);
if (result.extra) metadata.at("extra").set(result.extra);
if (result.videoType) metadata.at("videoType").set(result.videoType);
}
}