initial commit
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user