diff --git a/src/lib/FileNameVideoMetaExtractor.ts b/src/lib/FileNameVideoMetaExtractor.ts index 8f0c013..e308882 100644 --- a/src/lib/FileNameVideoMetaExtractor.ts +++ b/src/lib/FileNameVideoMetaExtractor.ts @@ -126,21 +126,63 @@ export class FileNameVideoMetaExtractor { return this.cleanTitle(this.getParentFolder(filePath, 2)); } + /** + * Check if a filename contains a season/episode pattern (indicates TV show) + */ + private filenameHasSeasonEpisodePattern(filename: string): boolean { + const cleanedFilename = this.serieFilesNameAnalysisTool.removeKeywordsFromFilename(filename); + const cleanedFilename2 = this.serieFilesNameAnalysisTool.removeSFV(cleanedFilename); + const cleanedFilename3 = this.fileNameCleaner.cleanSpecialChars(cleanedFilename2); + const cleanedFilename4 = this.fileNameCleaner.cleanSpace(cleanedFilename3); + const [season, episode] = this.serieFilesNameAnalysisTool.findSeasonAndEpisode(cleanedFilename4); + return season >= 0 || episode >= 0; + } + 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]); + + // Check if filename contains season/episode pattern (indicates TV show) + const isTvShow = this.filenameHasSeasonEpisodePattern(filename); + 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; + + if (isTvShow) { + // For TV shows, prefer the longer title between filename and folder + // This handles cases like "S01E01-The Fool.mkv" where "The Fool" is the episode name + // but "Lord of Mysteries" in the parent folder is the actual series title + const filenameTitleValid = titleFromFileName && titleFromFileName.length > MIN_TITLE_LENGTH; + const folderTitleValid = titleFromParentFolder && titleFromParentFolder.length > MIN_TITLE_LENGTH; + + if (filenameTitleValid && folderTitleValid) { + // Both valid - prefer the longer one (series names are typically longer than episode names) + // Use strict > to avoid preferring folder when lengths are equal (e.g., "Dexter" vs "Series") + selectedTitle = titleFromParentFolder.length > titleFromFileName.length + ? titleFromParentFolder + : titleFromFileName; + } else if (folderTitleValid) { + selectedTitle = titleFromParentFolder; + } else if (filenameTitleValid) { + selectedTitle = titleFromFileName; + } else if (titleFromGrandParentFolder && titleFromGrandParentFolder.length > MIN_TITLE_LENGTH) { + selectedTitle = titleFromGrandParentFolder; + } else { + selectedTitle = simpleTitle; + } } else { - selectedTitle = simpleTitle; + // For non-TV shows (movies, etc.), use original logic: prefer filename + 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) { diff --git a/src/tests/FileNameVideoMetaExtractor.test.ts b/src/tests/FileNameVideoMetaExtractor.test.ts index e64843c..ce21516 100644 --- a/src/tests/FileNameVideoMetaExtractor.test.ts +++ b/src/tests/FileNameVideoMetaExtractor.test.ts @@ -164,6 +164,14 @@ describe('FileNameMetaExtractor', () => { describe('Edge cases - title from folder (skipped)', () => { // These tests are skipped because the parser doesn't extract title from folder name // when the filename doesn't contain the show title + // Pattern: FolderWithTitle/S01E01-EpisodeName.mkv should extract title from folder + + it('Lord of Mysteries - title from folder, filename is S01E01-EpisodeName pattern', async () => { + // Real-world case: folder contains title + quality info, filename is S01E01-EpisodeName format + // Expected: extract "Lord of Mysteries" from folder, not "The Fool" from filename + await titleSeasonEpTester('Lord of Mysteries', '1', '1', + '/data/watch/test-files/Anime/Lord of Mysteries S01 1080p Dual Audio WEBRip DD+ x265-EMBER/S01E01-The Fool [8A4E8B1F].mkv'); + }); it.skip('handles files named with season in folder', async () => { await titleSeasonEpTester('MythBusters', '2', '1', @@ -180,7 +188,7 @@ describe('FileNameMetaExtractor', () => { '/data/watch/test-files/Anime/Le Depart/13 it a new start.mkv'); }); - it.skip('Akudama Drive - title from folder', async () => { + it('Akudama Drive - title from folder', async () => { await titleSeasonEpTester('Akudama Drive', '1', '2', '/data/watch/test-files/Anime/Akudama Drive/S01E02-RESERVOIR DOGS [F1D64557].mkv'); }); @@ -210,8 +218,8 @@ describe('FileNameMetaExtractor', () => { '/data/watch/test-files/Anime/K-Project/[CBM]_K_-_02_-_Kitten_[1080p-FLAC]_[A83DF131].mkv'); }); - it.skip('Made in Abyss - title from folder', async () => { - await titleSeasonEpTester('Made In Abyss', '1', '2', + it('Made in Abyss - title from folder', async () => { + await titleSeasonEpTester('Made in Abyss', '1', '2', '/data/watch/test-files/Anime/Made in Abyss/S01E02-Resurrection Festival [AE07AD68].mkv'); });