feat: Add extraction and formatting for special edition information in filenames
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user