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-"],
|
"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"),
|
("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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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']:
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
Reference in New Issue
Block a user