301 lines
12 KiB
TypeScript
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);
|
|
}
|
|
}
|