From 691d1e7b2d7600f3fe7e66d7618bfb782aaa1d9e Mon Sep 17 00:00:00 2001 From: sHa Date: Fri, 26 Dec 2025 20:56:22 +0000 Subject: [PATCH] feat: Add extraction and formatting for special edition information in filenames --- renamer/constants.py | 43 +++++++++++++++++++ renamer/extractor.py | 22 ++++++++-- renamer/extractors/default_extractor.py | 3 ++ renamer/extractors/filename_extractor.py | 26 ++++++++++- renamer/extractors/mediainfo_extractor.py | 2 +- renamer/formatters/media_formatter.py | 10 ++++- renamer/formatters/proposed_name_formatter.py | 3 +- 7 files changed, 101 insertions(+), 8 deletions(-) diff --git a/renamer/constants.py b/renamer/constants.py index 7ed711f..406982d 100644 --- a/renamer/constants.py +++ b/renamer/constants.py @@ -108,3 +108,46 @@ MOVIE_DB_DICT = { "patterns": ["tvdbid", "tvdb", "tvdbid-", "tvdb-"], }, } + +SPECIAL_EDITIONS = [ + "Theatrical Cut", + "Director's Cut", + "Director Cut", + "Extended Edition", + "Ultimate Extended Edition", + "Special Edition", + "Collector's Edition", + "Criterion Collection", + "Fundamental Collection", + "Anniversary Edition", + "Redux", + "Final Cut", + "Alternate Cut", + "International Cut", + "Restored Version", + "Remastered", + "Unrated", + "Uncensored", + "Definitive Edition", + "Platinum Edition", + "Gold Edition", + "Diamond Edition", + "Steelbook Edition", + "Limited Edition", + "Deluxe Edition", + "Premium Edition", + "Complete Edition", + "Restored Edition", + "4K Restoration", + "HD Remaster", + "Director's Definitive Cut", + "Extended Director's Cut", + "Ultimate Director's Cut", + "Original Cut", + "Cinematic Cut", + "Roadshow Cut", + "Premiere Cut", + "Festival Cut", + "Workprint", + "Rough Cut", +] diff --git a/renamer/extractor.py b/renamer/extractor.py index 6e73377..4456a72 100644 --- a/renamer/extractor.py +++ b/renamer/extractor.py @@ -78,6 +78,12 @@ class MediaExtractor: ("Default", "extract_movie_db"), ], }, + "special_info": { + "sources": [ + ("Filename", "extract_special_info"), + ("Default", "extract_special_info"), + ], + }, "audio_langs": { "sources": [ ("MediaInfo", "extract_audio_langs"), @@ -149,20 +155,28 @@ class MediaExtractor: if extractor_name.lower() == source.lower(): method = f"extract_{key}" if hasattr(extractor, method): - return getattr(extractor, method)() + val = getattr(extractor, method)() + # Apply condition if specified + if key in self._data and "condition" in self._data[key]: + condition = self._data[key]["condition"] + return val if condition(val) else None + return val return None # Fallback mode - try sources in order if key in self._data: - sources = self._data[key]["sources"] + data = self._data[key] + sources = data["sources"] + condition = data.get("condition", lambda x: x is not None) else: # Try extractors in order for unconfigured keys sources = [(name, f"extract_{key}") for name in ["MediaInfo", "Metadata", "Filename", "FileInfo"]] + condition = lambda x: x is not None - # Try each source in order until a non-None value is found + # Try each source in order until a valid value is found for src, method in sources: if src in self._extractors and hasattr(self._extractors[src], method): val = getattr(self._extractors[src], method)() - if val is not None: + if condition(val): return val return None diff --git a/renamer/extractors/default_extractor.py b/renamer/extractors/default_extractor.py index b20d933..dddc252 100644 --- a/renamer/extractors/default_extractor.py +++ b/renamer/extractors/default_extractor.py @@ -22,6 +22,9 @@ class DefaultExtractor: def extract_movie_db(self): return None + def extract_special_info(self): + return [] + def extract_audio_langs(self): return None diff --git a/renamer/extractors/filename_extractor.py b/renamer/extractors/filename_extractor.py index 2c6eb01..105feac 100644 --- a/renamer/extractors/filename_extractor.py +++ b/renamer/extractors/filename_extractor.py @@ -1,7 +1,7 @@ import re from pathlib import Path from collections import Counter -from ..constants import SOURCE_DICT, FRAME_CLASSES, MOVIE_DB_DICT +from ..constants import SOURCE_DICT, FRAME_CLASSES, MOVIE_DB_DICT, SPECIAL_EDITIONS import langcodes @@ -192,6 +192,30 @@ class FilenameExtractor: return None + def extract_special_info(self) -> list[str]: + """Extract special edition information from filename""" + # Look for special edition indicators in brackets or as standalone text + special_info = [] + + for edition in SPECIAL_EDITIONS: + # Check in brackets: [Theatrical Cut], [Director's Cut], etc. + bracket_pattern = r'\[([^\]]+)\]' + brackets = re.findall(bracket_pattern, self.file_name) + for bracket in brackets: + # Check if bracket contains comma-separated items + items = [item.strip() for item in bracket.split(',')] + for item in items: + if edition.lower() == item.lower().strip(): + if edition not in special_info: + special_info.append(edition) + + # Check as standalone text (case-insensitive) + if re.search(r'\b' + re.escape(edition) + r'\b', self.file_name, re.IGNORECASE): + if edition not in special_info: + special_info.append(edition) + + return special_info + def extract_audio_langs(self) -> str: """Extract audio languages from filename""" # Look for language patterns in brackets and outside brackets diff --git a/renamer/extractors/mediainfo_extractor.py b/renamer/extractors/mediainfo_extractor.py index 8393f38..5dd42d4 100644 --- a/renamer/extractors/mediainfo_extractor.py +++ b/renamer/extractors/mediainfo_extractor.py @@ -21,7 +21,7 @@ class MediaInfoExtractor: self.audio_tracks = [] self.sub_tracks = [] - def _get_frame_class_from_height(self, height: int) -> str: + def _get_frame_class_from_height(self, height: int) -> str | None: """Get frame class from video height using FRAME_CLASSES constant""" for frame_class, info in FRAME_CLASSES.items(): if height == info['nominal_height']: diff --git a/renamer/formatters/media_formatter.py b/renamer/formatters/media_formatter.py index b3f1d00..566e601 100644 --- a/renamer/formatters/media_formatter.py +++ b/renamer/formatters/media_formatter.py @@ -267,7 +267,7 @@ class MediaFormatter: "label": "Order", "label_formatters": [TextFormatter.bold], "value": self.extractor.get("order", "Filename") or "Not extracted", - "display_formatters": [TextFormatter.grey], + "display_formatters": [TextFormatter.yellow], }, { "label": "Movie title", @@ -307,6 +307,14 @@ class MediaFormatter: or "Not extracted", "display_formatters": [TextFormatter.grey], }, + { + "label": "Special info", + "label_formatters": [TextFormatter.bold], + "value": self.extractor.get("special_info", "Filename") + or "Not extracted", + "value_formatters": [lambda x: ", ".join(x) if isinstance(x, list) else x, TextFormatter.blue], + "display_formatters": [TextFormatter.grey], + }, { "label": "Movie DB", "label_formatters": [TextFormatter.bold], diff --git a/renamer/formatters/proposed_name_formatter.py b/renamer/formatters/proposed_name_formatter.py index 7793bc3..442cfc2 100644 --- a/renamer/formatters/proposed_name_formatter.py +++ b/renamer/formatters/proposed_name_formatter.py @@ -15,6 +15,7 @@ class ProposedNameFormatter: self.__frame_class = extractor.get("frame_class") or None self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else "" self.__audio_langs = extractor.get("audio_langs") or None + self.__special_info = f" [{', '.join(extractor.get('special_info'))}]" if extractor.get("special_info") else "" self.__extension = extractor.get("extension") or "ext" def __str__(self) -> str: @@ -22,7 +23,7 @@ class ProposedNameFormatter: return self.rename_line() def rename_line(self) -> str: - return f"{self.__order}{self.__title} {self.__year}{self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}].{self.__extension}" + return f"{self.__order}{self.__title} {self.__year}{self.__special_info}{self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}].{self.__extension}" def rename_line_formatted(self) -> str: """Format the proposed name for display with color"""