feat: Add extraction and formatting for special edition information in filenames

This commit is contained in:
sHa
2025-12-26 20:56:22 +00:00
parent b21308c7b8
commit 691d1e7b2d
7 changed files with 101 additions and 8 deletions

View File

@@ -108,3 +108,46 @@ MOVIE_DB_DICT = {
"patterns": ["tvdbid", "tvdb", "tvdbid-", "tvdb-"], "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",
]

View File

@@ -78,6 +78,12 @@ class MediaExtractor:
("Default", "extract_movie_db"), ("Default", "extract_movie_db"),
], ],
}, },
"special_info": {
"sources": [
("Filename", "extract_special_info"),
("Default", "extract_special_info"),
],
},
"audio_langs": { "audio_langs": {
"sources": [ "sources": [
("MediaInfo", "extract_audio_langs"), ("MediaInfo", "extract_audio_langs"),
@@ -149,20 +155,28 @@ class MediaExtractor:
if extractor_name.lower() == source.lower(): if extractor_name.lower() == source.lower():
method = f"extract_{key}" method = f"extract_{key}"
if hasattr(extractor, method): 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 return None
# Fallback mode - try sources in order # Fallback mode - try sources in order
if key in self._data: 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: else:
# Try extractors in order for unconfigured keys # Try extractors in order for unconfigured keys
sources = [(name, f"extract_{key}") for name in ["MediaInfo", "Metadata", "Filename", "FileInfo"]] 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: for src, method in sources:
if src in self._extractors and hasattr(self._extractors[src], method): if src in self._extractors and hasattr(self._extractors[src], method):
val = getattr(self._extractors[src], method)() val = getattr(self._extractors[src], method)()
if val is not None: if condition(val):
return val return val
return None return None

View File

@@ -22,6 +22,9 @@ class DefaultExtractor:
def extract_movie_db(self): def extract_movie_db(self):
return None return None
def extract_special_info(self):
return []
def extract_audio_langs(self): def extract_audio_langs(self):
return None return None

View File

@@ -1,7 +1,7 @@
import re import re
from pathlib import Path from pathlib import Path
from collections import Counter 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 import langcodes
@@ -192,6 +192,30 @@ class FilenameExtractor:
return None 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: def extract_audio_langs(self) -> str:
"""Extract audio languages from filename""" """Extract audio languages from filename"""
# Look for language patterns in brackets and outside brackets # Look for language patterns in brackets and outside brackets

View File

@@ -21,7 +21,7 @@ class MediaInfoExtractor:
self.audio_tracks = [] self.audio_tracks = []
self.sub_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""" """Get frame class from video height using FRAME_CLASSES constant"""
for frame_class, info in FRAME_CLASSES.items(): for frame_class, info in FRAME_CLASSES.items():
if height == info['nominal_height']: if height == info['nominal_height']:

View File

@@ -267,7 +267,7 @@ class MediaFormatter:
"label": "Order", "label": "Order",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],
"value": self.extractor.get("order", "Filename") or "Not extracted", "value": self.extractor.get("order", "Filename") or "Not extracted",
"display_formatters": [TextFormatter.grey], "display_formatters": [TextFormatter.yellow],
}, },
{ {
"label": "Movie title", "label": "Movie title",
@@ -307,6 +307,14 @@ class MediaFormatter:
or "Not extracted", or "Not extracted",
"display_formatters": [TextFormatter.grey], "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": "Movie DB",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],

View File

@@ -15,6 +15,7 @@ class ProposedNameFormatter:
self.__frame_class = extractor.get("frame_class") or None self.__frame_class = extractor.get("frame_class") or None
self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else "" self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else ""
self.__audio_langs = extractor.get("audio_langs") or None 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" self.__extension = extractor.get("extension") or "ext"
def __str__(self) -> str: def __str__(self) -> str:
@@ -22,7 +23,7 @@ class ProposedNameFormatter:
return self.rename_line() return self.rename_line()
def rename_line(self) -> str: 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: def rename_line_formatted(self) -> str:
"""Format the proposed name for display with color""" """Format the proposed name for display with color"""