Refactor extractors and formatters for improved structure and functionality
- Converted static methods to instance methods in FileInfoExtractor and FilenameExtractor for better encapsulation. - Enhanced MediaInfoExtractor to initialize with file path and extract media information upon instantiation. - Updated MetadataExtractor to handle metadata extraction with improved error handling and added methods for meta type detection. - Introduced ColorFormatter for consistent text formatting across the application. - Refactored MediaFormatter to utilize the new extractor structure and improve output formatting. - Removed redundant utility functions and replaced them with direct calls in extractors. - Added ProposedNameFormatter for better handling of proposed filename formatting. - Updated extension handling to use MEDIA_TYPES for descriptions instead of VIDEO_EXT_DESCRIPTIONS.
This commit is contained in:
@@ -4,12 +4,14 @@ from textual.containers import Horizontal, Container, ScrollableContainer, Verti
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
import concurrent.futures
|
||||
|
||||
from .constants import VIDEO_EXTENSIONS
|
||||
from .utils import get_media_tracks
|
||||
from .constants import MEDIA_TYPES
|
||||
from .screens import OpenScreen
|
||||
from .extractor import MediaExtractor
|
||||
from .formatters.media_formatter import MediaFormatter
|
||||
from .formatters.proposed_name_formatter import ProposedNameFormatter
|
||||
from .formatters.color_formatter import ColorFormatter
|
||||
|
||||
|
||||
class RenamerApp(App):
|
||||
@@ -42,7 +44,9 @@ class RenamerApp(App):
|
||||
with Vertical():
|
||||
yield LoadingIndicator(id="loading")
|
||||
with ScrollableContainer(id="details_container"):
|
||||
yield Static("Select a file to view details", id="details", markup=True)
|
||||
yield Static(
|
||||
"Select a file to view details", id="details", markup=True
|
||||
)
|
||||
yield Static("", id="proposed", markup=True)
|
||||
yield Footer()
|
||||
|
||||
@@ -58,7 +62,6 @@ class RenamerApp(App):
|
||||
return
|
||||
tree = self.query_one("#file_tree", Tree)
|
||||
tree.clear()
|
||||
tree.root.add(".", data=self.scan_dir)
|
||||
self.build_tree(self.scan_dir, tree.root)
|
||||
tree.root.expand()
|
||||
self.set_focus(tree)
|
||||
@@ -68,9 +71,13 @@ class RenamerApp(App):
|
||||
for item in sorted(path.iterdir()):
|
||||
try:
|
||||
if item.is_dir():
|
||||
if item.name.startswith(".") or item.name == "lost+found":
|
||||
continue
|
||||
subnode = node.add(item.name, data=item)
|
||||
self.build_tree(item, subnode)
|
||||
elif item.is_file() and item.suffix.lower() in VIDEO_EXTENSIONS:
|
||||
elif item.is_file() and item.suffix.lower() in {
|
||||
f".{ext}" for ext in MEDIA_TYPES
|
||||
}:
|
||||
node.add(item.name, data=item)
|
||||
except PermissionError:
|
||||
pass
|
||||
@@ -100,52 +107,46 @@ class RenamerApp(App):
|
||||
proposed.update("")
|
||||
elif node.data.is_file():
|
||||
self._start_loading_animation()
|
||||
threading.Thread(target=self._extract_and_show_details, args=(node.data,)).start()
|
||||
threading.Thread(
|
||||
target=self._extract_and_show_details, args=(node.data,)
|
||||
).start()
|
||||
|
||||
def _extract_and_show_details(self, file_path: Path):
|
||||
time.sleep(1) # Minimum delay to show loading
|
||||
# Initialize extractors and formatters
|
||||
extractor = MediaExtractor()
|
||||
formatter = MediaFormatter()
|
||||
try:
|
||||
# Initialize extractors and formatters
|
||||
extractor = MediaExtractor(file_path)
|
||||
formatter = MediaFormatter()
|
||||
name_formatter = ProposedNameFormatter(extractor)
|
||||
|
||||
# Extract all data
|
||||
rename_data = extractor.extract_all(file_path)
|
||||
# Update UI
|
||||
self.call_later(
|
||||
self._update_details,
|
||||
formatter.format_file_info_panel(extractor),
|
||||
name_formatter.format_display_string(),
|
||||
)
|
||||
except Exception as e:
|
||||
self.call_later(
|
||||
self._update_details,
|
||||
ColorFormatter.red(f"Error extracting details: {str(e)}"),
|
||||
"",
|
||||
)
|
||||
|
||||
# Get media tracks info
|
||||
tracks_text = get_media_tracks(file_path)
|
||||
if not tracks_text:
|
||||
tracks_text = "[grey]No track info available[/grey]"
|
||||
|
||||
# Format file info
|
||||
full_info = formatter.format_file_info(file_path, rename_data)
|
||||
full_info += f"\n\n{tracks_text}"
|
||||
|
||||
# Format proposed name
|
||||
ext_name = file_path.suffix.lower().lstrip('.')
|
||||
proposed_name = formatter.format_proposed_name(rename_data, ext_name)
|
||||
|
||||
# Format rename lines
|
||||
rename_lines = formatter.format_rename_lines(rename_data, proposed_name)
|
||||
full_info += f"\n\n" + "\n".join(rename_lines[:-1])
|
||||
|
||||
# Update UI
|
||||
self.call_later(self._update_details, full_info, proposed_name)
|
||||
|
||||
def _update_details(self, full_info: str, proposed_name: str):
|
||||
def _update_details(self, full_info: str, display_string: str):
|
||||
self._stop_loading_animation()
|
||||
details = self.query_one("#details", Static)
|
||||
details.update(full_info)
|
||||
|
||||
proposed = self.query_one("#proposed", Static)
|
||||
proposed.update(f"[bold yellow]Proposed filename: {proposed_name}[/bold yellow]")
|
||||
proposed.update(display_string)
|
||||
|
||||
def action_quit(self):
|
||||
async def action_quit(self):
|
||||
self.exit()
|
||||
|
||||
def action_open(self):
|
||||
async def action_open(self):
|
||||
self.push_screen(OpenScreen())
|
||||
|
||||
def action_scan(self):
|
||||
async def action_scan(self):
|
||||
if self.scan_dir:
|
||||
self.scan_files()
|
||||
|
||||
@@ -153,7 +154,12 @@ class RenamerApp(App):
|
||||
if event.key == "right":
|
||||
tree = self.query_one("#file_tree", Tree)
|
||||
node = tree.cursor_node
|
||||
if node and node.data and isinstance(node.data, Path) and node.data.is_dir():
|
||||
if (
|
||||
node
|
||||
and node.data
|
||||
and isinstance(node.data, Path)
|
||||
and node.data.is_dir()
|
||||
):
|
||||
if not node.is_expanded:
|
||||
node.expand()
|
||||
tree.cursor_line = node.line + 1
|
||||
|
||||
@@ -1,72 +1,83 @@
|
||||
VIDEO_EXTENSIONS = {'.mkv', '.avi', '.mov', '.mp4', '.wmv', '.flv', '.webm', '.m4v', '.3gp', '.ogv'}
|
||||
|
||||
VIDEO_EXT_DESCRIPTIONS = {
|
||||
'mkv': 'Matroska multimedia container',
|
||||
'avi': 'Audio Video Interleave',
|
||||
'mov': 'QuickTime movie',
|
||||
'mp4': 'MPEG-4 video container',
|
||||
'wmv': 'Windows Media Video',
|
||||
'flv': 'Flash Video',
|
||||
'webm': 'WebM multimedia',
|
||||
'm4v': 'MPEG-4 video',
|
||||
'3gp': '3GPP multimedia',
|
||||
'ogv': 'Ogg Video',
|
||||
}
|
||||
|
||||
META_DESCRIPTIONS = {
|
||||
'MP4': 'MPEG-4 video container',
|
||||
'Matroska': 'Matroska multimedia container',
|
||||
'AVI': 'Audio Video Interleave',
|
||||
'QuickTime': 'QuickTime movie',
|
||||
'ASF': 'Windows Media',
|
||||
'FLV': 'Flash Video',
|
||||
'WebM': 'WebM multimedia',
|
||||
'Ogg': 'Ogg multimedia',
|
||||
MEDIA_TYPES = {
|
||||
"mkv": {
|
||||
"description": "Matroska multimedia container",
|
||||
"meta_type": "Matroska",
|
||||
"mime": "video/x-matroska",
|
||||
},
|
||||
"avi": {
|
||||
"description": "Audio Video Interleave",
|
||||
"meta_type": "AVI",
|
||||
"mime": "video/x-msvideo",
|
||||
},
|
||||
"mov": {
|
||||
"description": "QuickTime movie",
|
||||
"meta_type": "QuickTime",
|
||||
"mime": "video/quicktime",
|
||||
},
|
||||
"mp4": {
|
||||
"description": "MPEG-4 video container",
|
||||
"meta_type": "MP4",
|
||||
"mime": "video/mp4",
|
||||
},
|
||||
"wmv": {
|
||||
"description": "Windows Media Video",
|
||||
"meta_type": "ASF",
|
||||
"mime": "video/x-ms-wmv",
|
||||
},
|
||||
"flv": {"description": "Flash Video", "meta_type": "FLV", "mime": "video/x-flv"},
|
||||
"webm": {
|
||||
"description": "WebM multimedia",
|
||||
"meta_type": "WebM",
|
||||
"mime": "video/webm",
|
||||
},
|
||||
"m4v": {"description": "MPEG-4 video", "meta_type": "MP4", "mime": "video/mp4"},
|
||||
"3gp": {"description": "3GPP multimedia", "meta_type": "MP4", "mime": "video/3gpp"},
|
||||
"ogv": {"description": "Ogg Video", "meta_type": "Ogg", "mime": "video/ogg"},
|
||||
}
|
||||
|
||||
SOURCE_DICT = {
|
||||
'WEB-DL': ['WEB-DL', 'WEBRip', 'WEB-Rip', 'WEB'],
|
||||
'BDRip': ['BDRip', 'BD-Rip', 'BDRIP'],
|
||||
'BDRemux': ['BDRemux', 'BD-Remux', 'BDREMUX'],
|
||||
'DVDRip': ['DVDRip', 'DVD-Rip', 'DVDRIP'],
|
||||
'HDTV': ['HDTV'],
|
||||
'BluRay': ['BluRay', 'BLURAY', 'Blu-ray'],
|
||||
"WEB-DL": ["WEB-DL", "WEBRip", "WEB-Rip", "WEB"],
|
||||
"BDRip": ["BDRip", "BD-Rip", "BDRIP"],
|
||||
"BDRemux": ["BDRemux", "BD-Remux", "BDREMUX"],
|
||||
"DVDRip": ["DVDRip", "DVD-Rip", "DVDRIP"],
|
||||
"HDTV": ["HDTV"],
|
||||
"BluRay": ["BluRay", "BLURAY", "Blu-ray"],
|
||||
}
|
||||
|
||||
FRAME_CLASSES = {
|
||||
'480p': {
|
||||
'nominal_height': 480,
|
||||
'typical_widths': [640, 704, 720],
|
||||
'description': 'Standard Definition (SD) - DVD quality'
|
||||
"480p": {
|
||||
"nominal_height": 480,
|
||||
"typical_widths": [640, 704, 720],
|
||||
"description": "Standard Definition (SD) - DVD quality",
|
||||
},
|
||||
'576p': {
|
||||
'nominal_height': 576,
|
||||
'typical_widths': [720, 768],
|
||||
'description': 'PAL Standard Definition (SD) - European DVD quality'
|
||||
"576p": {
|
||||
"nominal_height": 576,
|
||||
"typical_widths": [720, 768],
|
||||
"description": "PAL Standard Definition (SD) - European DVD quality",
|
||||
},
|
||||
'720p': {
|
||||
'nominal_height': 720,
|
||||
'typical_widths': [1280],
|
||||
'description': 'High Definition (HD) - 720p HD'
|
||||
"720p": {
|
||||
"nominal_height": 720,
|
||||
"typical_widths": [1280],
|
||||
"description": "High Definition (HD) - 720p HD",
|
||||
},
|
||||
'1080p': {
|
||||
'nominal_height': 1080,
|
||||
'typical_widths': [1920],
|
||||
'description': 'Full High Definition (FHD) - 1080p HD'
|
||||
"1080p": {
|
||||
"nominal_height": 1080,
|
||||
"typical_widths": [1920],
|
||||
"description": "Full High Definition (FHD) - 1080p HD",
|
||||
},
|
||||
'1440p': {
|
||||
'nominal_height': 1440,
|
||||
'typical_widths': [2560],
|
||||
'description': 'Quad High Definition (QHD) - 1440p 2K'
|
||||
"1440p": {
|
||||
"nominal_height": 1440,
|
||||
"typical_widths": [2560],
|
||||
"description": "Quad High Definition (QHD) - 1440p 2K",
|
||||
},
|
||||
'2160p': {
|
||||
'nominal_height': 2160,
|
||||
'typical_widths': [3840],
|
||||
'description': 'Ultra High Definition (UHD) - 2160p 4K'
|
||||
"2160p": {
|
||||
"nominal_height": 2160,
|
||||
"typical_widths": [3840],
|
||||
"description": "Ultra High Definition (UHD) - 2160p 4K",
|
||||
},
|
||||
"4320p": {
|
||||
"nominal_height": 4320,
|
||||
"typical_widths": [7680],
|
||||
"description": "Ultra High Definition (UHD) - 4320p 8K",
|
||||
},
|
||||
'4320p': {
|
||||
'nominal_height': 4320,
|
||||
'typical_widths': [7680],
|
||||
'description': 'Ultra High Definition (UHD) - 4320p 8K'
|
||||
}
|
||||
}
|
||||
@@ -2,70 +2,107 @@ from pathlib import Path
|
||||
from .extractors.filename_extractor import FilenameExtractor
|
||||
from .extractors.metadata_extractor import MetadataExtractor
|
||||
from .extractors.mediainfo_extractor import MediaInfoExtractor
|
||||
from .extractors.fileinfo_extractor import FileInfoExtractor
|
||||
|
||||
|
||||
class MediaExtractor:
|
||||
"""Class to extract various metadata from media files using specialized extractors"""
|
||||
|
||||
def __init__(self):
|
||||
self.mediainfo_extractor = MediaInfoExtractor()
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
self.filename_extractor = FilenameExtractor(file_path)
|
||||
self.metadata_extractor = MetadataExtractor(file_path)
|
||||
self.mediainfo_extractor = MediaInfoExtractor(file_path)
|
||||
self.fileinfo_extractor = FileInfoExtractor(file_path)
|
||||
|
||||
def extract_title(self, file_path: Path) -> str | None:
|
||||
"""Extract movie title from metadata or filename"""
|
||||
# Try metadata first
|
||||
title = MetadataExtractor.extract_title(file_path)
|
||||
if title:
|
||||
return title
|
||||
# Fallback to filename
|
||||
return FilenameExtractor.extract_title(file_path)
|
||||
|
||||
def extract_year(self, file_path: Path) -> str | None:
|
||||
"""Extract year from filename"""
|
||||
return FilenameExtractor.extract_year(file_path)
|
||||
|
||||
def extract_source(self, file_path: Path) -> str | None:
|
||||
"""Extract video source from filename"""
|
||||
return FilenameExtractor.extract_source(file_path)
|
||||
|
||||
def extract_frame_class(self, file_path: Path) -> str | None:
|
||||
"""Extract frame class from media info or filename"""
|
||||
# Try media info first
|
||||
frame_class = self.mediainfo_extractor.extract_frame_class(file_path)
|
||||
if frame_class:
|
||||
return frame_class
|
||||
# Fallback to filename
|
||||
return FilenameExtractor.extract_frame_class(file_path)
|
||||
|
||||
def extract_resolution(self, file_path: Path) -> str | None:
|
||||
"""Extract actual video resolution (WIDTHxHEIGHT) from media info"""
|
||||
return self.mediainfo_extractor.extract_resolution(file_path)
|
||||
|
||||
def extract_aspect_ratio(self, file_path: Path) -> str | None:
|
||||
"""Extract video aspect ratio from media info"""
|
||||
return self.mediainfo_extractor.extract_aspect_ratio(file_path)
|
||||
|
||||
def extract_hdr(self, file_path: Path) -> str | None:
|
||||
"""Extract HDR info from media info"""
|
||||
return self.mediainfo_extractor.extract_hdr(file_path)
|
||||
|
||||
def extract_audio_langs(self, file_path: Path) -> str:
|
||||
"""Extract audio languages from media info"""
|
||||
return self.mediainfo_extractor.extract_audio_langs(file_path)
|
||||
|
||||
def extract_metadata(self, file_path: Path) -> dict:
|
||||
"""Extract general metadata"""
|
||||
return MetadataExtractor.extract_all_metadata(file_path)
|
||||
|
||||
def extract_all(self, file_path: Path) -> dict:
|
||||
"""Extract all rename-related data"""
|
||||
return {
|
||||
'title': self.extract_title(file_path),
|
||||
'year': self.extract_year(file_path),
|
||||
'source': self.extract_source(file_path),
|
||||
'frame_class': self.extract_frame_class(file_path),
|
||||
'resolution': self.extract_resolution(file_path),
|
||||
'aspect_ratio': self.extract_aspect_ratio(file_path),
|
||||
'hdr': self.extract_hdr(file_path),
|
||||
'audio_langs': self.extract_audio_langs(file_path),
|
||||
'metadata': self.extract_metadata(file_path)
|
||||
# Define sources for each data type
|
||||
self._sources = {
|
||||
'title': [
|
||||
('metadata', lambda: self.metadata_extractor.extract_title()),
|
||||
('filename', lambda: self.filename_extractor.extract_title())
|
||||
],
|
||||
'year': [
|
||||
('filename', lambda: self.filename_extractor.extract_year())
|
||||
],
|
||||
'source': [
|
||||
('filename', lambda: self.filename_extractor.extract_source())
|
||||
],
|
||||
'frame_class': [
|
||||
('mediainfo', lambda: self.mediainfo_extractor.extract_frame_class()),
|
||||
('filename', lambda: self.filename_extractor.extract_frame_class())
|
||||
],
|
||||
'resolution': [
|
||||
('mediainfo', lambda: self.mediainfo_extractor.extract_resolution())
|
||||
],
|
||||
'aspect_ratio': [
|
||||
('mediainfo', lambda: self.mediainfo_extractor.extract_aspect_ratio())
|
||||
],
|
||||
'hdr': [
|
||||
('mediainfo', lambda: self.mediainfo_extractor.extract_hdr())
|
||||
],
|
||||
'audio_langs': [
|
||||
('mediainfo', lambda: self.mediainfo_extractor.extract_audio_langs())
|
||||
],
|
||||
'metadata': [
|
||||
('metadata', lambda: self.metadata_extractor.extract_all_metadata())
|
||||
],
|
||||
'meta_type': [
|
||||
('metadata', lambda: self.metadata_extractor.extract_meta_type())
|
||||
],
|
||||
'meta_description': [
|
||||
('metadata', lambda: self.metadata_extractor.extract_meta_description())
|
||||
],
|
||||
'file_size': [
|
||||
('fileinfo', lambda: self.fileinfo_extractor.extract_size())
|
||||
],
|
||||
'modification_time': [
|
||||
('fileinfo', lambda: self.fileinfo_extractor.extract_modification_time())
|
||||
],
|
||||
'file_name': [
|
||||
('fileinfo', lambda: self.fileinfo_extractor.extract_file_name())
|
||||
],
|
||||
'file_path': [
|
||||
('fileinfo', lambda: self.fileinfo_extractor.extract_file_path())
|
||||
],
|
||||
'extension': [
|
||||
('fileinfo', lambda: self.fileinfo_extractor.extract_extension())
|
||||
],
|
||||
'tracks': [
|
||||
('mediainfo', lambda: self.mediainfo_extractor.extract_tracks())
|
||||
]
|
||||
}
|
||||
|
||||
# Conditions for when a value is considered valid
|
||||
self._conditions = {
|
||||
'title': lambda x: x is not None,
|
||||
'year': lambda x: x is not None,
|
||||
'source': lambda x: x is not None,
|
||||
'frame_class': lambda x: x and x != 'Unclassified',
|
||||
'resolution': lambda x: x is not None,
|
||||
'aspect_ratio': lambda x: x is not None,
|
||||
'hdr': lambda x: x is not None,
|
||||
'audio_langs': lambda x: x is not None,
|
||||
'metadata': lambda x: x is not None,
|
||||
'tracks': lambda x: x != ""
|
||||
}
|
||||
|
||||
def get(self, key: str, source: str | None = None):
|
||||
"""Get extracted data by key, optionally from specific source"""
|
||||
if key not in self._sources:
|
||||
raise ValueError(f"Unknown key: {key}")
|
||||
|
||||
condition = self._conditions.get(key, lambda x: x is not None)
|
||||
|
||||
if source:
|
||||
for src, func in self._sources[key]:
|
||||
if src == source:
|
||||
val = func()
|
||||
return val if condition(val) else None
|
||||
raise ValueError(f"No such source '{source}' for key '{key}'")
|
||||
else:
|
||||
# Use fallback: return first valid value
|
||||
for src, func in self._sources[key]:
|
||||
val = func()
|
||||
if condition(val):
|
||||
return val
|
||||
return None
|
||||
@@ -4,22 +4,29 @@ from pathlib import Path
|
||||
class FileInfoExtractor:
|
||||
"""Class to extract file information"""
|
||||
|
||||
@staticmethod
|
||||
def extract_size(file_path: Path) -> int:
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
self._size = file_path.stat().st_size
|
||||
self._modification_time = file_path.stat().st_mtime
|
||||
self._file_name = file_path.name
|
||||
self._file_path = str(file_path)
|
||||
|
||||
def extract_size(self) -> int:
|
||||
"""Extract file size in bytes"""
|
||||
return file_path.stat().st_size
|
||||
return self._size
|
||||
|
||||
@staticmethod
|
||||
def extract_modification_time(file_path: Path) -> float:
|
||||
def extract_modification_time(self) -> float:
|
||||
"""Extract file modification time"""
|
||||
return file_path.stat().st_mtime
|
||||
return self._modification_time
|
||||
|
||||
@staticmethod
|
||||
def extract_file_name(file_path: Path) -> str:
|
||||
def extract_file_name(self) -> str:
|
||||
"""Extract file name"""
|
||||
return file_path.name
|
||||
return self._file_name
|
||||
|
||||
@staticmethod
|
||||
def extract_file_path(file_path: Path) -> str:
|
||||
def extract_file_path(self) -> str:
|
||||
"""Extract full file path as string"""
|
||||
return str(file_path)
|
||||
return self._file_path
|
||||
|
||||
def extract_extension(self) -> str:
|
||||
"""Extract file extension without the dot"""
|
||||
return self.file_path.suffix.lower().lstrip('.')
|
||||
@@ -6,40 +6,37 @@ from ..constants import SOURCE_DICT, FRAME_CLASSES
|
||||
class FilenameExtractor:
|
||||
"""Class to extract information from filename"""
|
||||
|
||||
@staticmethod
|
||||
def _get_frame_class_from_height(height: int) -> str:
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
self.file_name = file_path.name
|
||||
|
||||
def _get_frame_class_from_height(self, height: int) -> str:
|
||||
"""Get frame class from video height using FRAME_CLASSES constant"""
|
||||
for frame_class, info in FRAME_CLASSES.items():
|
||||
if height == info['nominal_height']:
|
||||
return frame_class
|
||||
return 'Unclassified'
|
||||
|
||||
@staticmethod
|
||||
def extract_title(file_path: Path) -> str | None:
|
||||
def extract_title(self) -> str | None:
|
||||
"""Extract movie title from filename"""
|
||||
file_name = file_path.name
|
||||
temp_name = re.sub(r'\s*\(\d{4}\)\s*|\s*\d{4}\s*|\.\d{4}\.', '', file_name)
|
||||
temp_name = re.sub(r'\s*\(\d{4}\)\s*|\s*\d{4}\s*|\.\d{4}\.', '', self.file_name)
|
||||
|
||||
# Find and remove source
|
||||
source = FilenameExtractor.extract_source(file_path)
|
||||
source = self.extract_source()
|
||||
if source:
|
||||
for alias in SOURCE_DICT[source]:
|
||||
temp_name = re.sub(r'\b' + re.escape(alias) + r'\b', '', temp_name, flags=re.IGNORECASE)
|
||||
|
||||
return temp_name.rsplit('.', 1)[0].strip()
|
||||
|
||||
@staticmethod
|
||||
def extract_year(file_path: Path) -> str | None:
|
||||
def extract_year(self) -> str | None:
|
||||
"""Extract year from filename"""
|
||||
file_name = file_path.name
|
||||
year_match = re.search(r'\((\d{4})\)|(\d{4})', file_name)
|
||||
year_match = re.search(r'\((\d{4})\)|(\d{4})', self.file_name)
|
||||
return (year_match.group(1) or year_match.group(2)) if year_match else None
|
||||
|
||||
@staticmethod
|
||||
def extract_source(file_path: Path) -> str | None:
|
||||
def extract_source(self) -> str | None:
|
||||
"""Extract video source from filename"""
|
||||
file_name = file_path.name
|
||||
temp_name = re.sub(r'\s*\(\d{4}\)\s*|\s*\d{4}\s*|\.\d{4}\.', '', file_name)
|
||||
temp_name = re.sub(r'\s*\(\d{4}\)\s*|\s*\d{4}\s*|\.\d{4}\.', '', self.file_name)
|
||||
|
||||
for src, aliases in SOURCE_DICT.items():
|
||||
for alias in aliases:
|
||||
@@ -47,12 +44,10 @@ class FilenameExtractor:
|
||||
return src
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def extract_frame_class(file_path: Path) -> str | None:
|
||||
def extract_frame_class(self) -> str | None:
|
||||
"""Extract frame class from filename (480p, 720p, 1080p, 2160p, etc.)"""
|
||||
file_name = file_path.name
|
||||
match = re.search(r'(\d{3,4})[pi]', file_name, re.IGNORECASE)
|
||||
match = re.search(r'(\d{3,4})[pi]', self.file_name, re.IGNORECASE)
|
||||
if match:
|
||||
height = int(match.group(1))
|
||||
return FilenameExtractor._get_frame_class_from_height(height)
|
||||
return self._get_frame_class_from_height(height)
|
||||
return 'Unclassified'
|
||||
@@ -2,17 +2,24 @@ from pathlib import Path
|
||||
from pymediainfo import MediaInfo
|
||||
from collections import Counter
|
||||
from ..constants import FRAME_CLASSES
|
||||
from ..formatters.color_formatter import ColorFormatter
|
||||
|
||||
|
||||
class MediaInfoExtractor:
|
||||
"""Class to extract information from MediaInfo"""
|
||||
|
||||
def __init__(self):
|
||||
self.lang_map = {
|
||||
'en': 'eng', 'fr': 'fre', 'de': 'ger', 'uk': 'ukr', 'ru': 'rus',
|
||||
'es': 'spa', 'it': 'ita', 'pt': 'por', 'ja': 'jpn', 'ko': 'kor',
|
||||
'zh': 'chi', 'und': 'und'
|
||||
}
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
try:
|
||||
self.media_info = MediaInfo.parse(file_path)
|
||||
self.video_tracks = [t for t in self.media_info.tracks if t.track_type == 'Video']
|
||||
self.audio_tracks = [t for t in self.media_info.tracks if t.track_type == 'Audio']
|
||||
self.sub_tracks = [t for t in self.media_info.tracks if t.track_type == 'Text']
|
||||
except Exception:
|
||||
self.media_info = None
|
||||
self.video_tracks = []
|
||||
self.audio_tracks = []
|
||||
self.sub_tracks = []
|
||||
|
||||
def _get_frame_class_from_height(self, height: int) -> str:
|
||||
"""Get frame class from video height using FRAME_CLASSES constant"""
|
||||
@@ -21,82 +28,113 @@ class MediaInfoExtractor:
|
||||
return frame_class
|
||||
return 'Unclassified'
|
||||
|
||||
def extract_frame_class(self, file_path: Path) -> str | None:
|
||||
def extract_frame_class(self) -> str | None:
|
||||
"""Extract frame class from media info (480p, 720p, 1080p, etc.)"""
|
||||
try:
|
||||
media_info = MediaInfo.parse(file_path)
|
||||
video_tracks = [t for t in media_info.tracks if t.track_type == 'Video']
|
||||
if video_tracks:
|
||||
height = getattr(video_tracks[0], 'height', None)
|
||||
if height:
|
||||
return self._get_frame_class_from_height(height)
|
||||
except:
|
||||
pass
|
||||
if not self.video_tracks:
|
||||
return 'Unclassified'
|
||||
height = getattr(self.video_tracks[0], 'height', None)
|
||||
if height:
|
||||
return self._get_frame_class_from_height(height)
|
||||
return 'Unclassified'
|
||||
|
||||
def extract_resolution(self, file_path: Path) -> str | None:
|
||||
def extract_resolution(self) -> str | None:
|
||||
"""Extract actual video resolution (WIDTHxHEIGHT) from media info"""
|
||||
try:
|
||||
media_info = MediaInfo.parse(file_path)
|
||||
video_tracks = [t for t in media_info.tracks if t.track_type == 'Video']
|
||||
if video_tracks:
|
||||
width = getattr(video_tracks[0], 'width', None)
|
||||
height = getattr(video_tracks[0], 'height', None)
|
||||
if width and height:
|
||||
return f"{width}x{height}"
|
||||
except:
|
||||
pass
|
||||
if not self.video_tracks:
|
||||
return None
|
||||
width = getattr(self.video_tracks[0], 'width', None)
|
||||
height = getattr(self.video_tracks[0], 'height', None)
|
||||
if width and height:
|
||||
return f"{width}x{height}"
|
||||
return None
|
||||
|
||||
def extract_aspect_ratio(self, file_path: Path) -> str | None:
|
||||
def extract_aspect_ratio(self) -> str | None:
|
||||
"""Extract video aspect ratio from media info"""
|
||||
try:
|
||||
media_info = MediaInfo.parse(file_path)
|
||||
video_tracks = [t for t in media_info.tracks if t.track_type == 'Video']
|
||||
if video_tracks:
|
||||
aspect_ratio = getattr(video_tracks[0], 'display_aspect_ratio', None)
|
||||
if aspect_ratio:
|
||||
return str(aspect_ratio)
|
||||
except:
|
||||
pass
|
||||
if not self.video_tracks:
|
||||
return None
|
||||
aspect_ratio = getattr(self.video_tracks[0], 'display_aspect_ratio', None)
|
||||
if aspect_ratio:
|
||||
return str(aspect_ratio)
|
||||
return None
|
||||
|
||||
def extract_hdr(self, file_path: Path) -> str | None:
|
||||
def extract_hdr(self) -> str | None:
|
||||
"""Extract HDR info from media info"""
|
||||
try:
|
||||
media_info = MediaInfo.parse(file_path)
|
||||
video_tracks = [t for t in media_info.tracks if t.track_type == 'Video']
|
||||
if video_tracks:
|
||||
profile = getattr(video_tracks[0], 'format_profile', '')
|
||||
if 'HDR' in profile.upper():
|
||||
return 'HDR'
|
||||
except:
|
||||
pass
|
||||
if not self.video_tracks:
|
||||
return None
|
||||
profile = getattr(self.video_tracks[0], 'format_profile', '')
|
||||
if 'HDR' in profile.upper():
|
||||
return 'HDR'
|
||||
return None
|
||||
|
||||
def extract_audio_langs(self, file_path: Path) -> str:
|
||||
def extract_audio_langs(self) -> str:
|
||||
"""Extract audio languages from media info"""
|
||||
try:
|
||||
media_info = MediaInfo.parse(file_path)
|
||||
audio_tracks = [t for t in media_info.tracks if t.track_type == 'Audio']
|
||||
langs = [getattr(a, 'language', 'und').lower()[:3] for a in audio_tracks]
|
||||
langs = [self.lang_map.get(lang, lang) for lang in langs]
|
||||
lang_counts = Counter(langs)
|
||||
audio_langs = [f"{count}{lang}" if count > 1 else lang for lang, count in lang_counts.items()]
|
||||
return ','.join(audio_langs)
|
||||
except:
|
||||
if not self.audio_tracks:
|
||||
return ''
|
||||
lang_map = {
|
||||
'en': 'eng', 'fr': 'fre', 'de': 'ger', 'uk': 'ukr', 'ru': 'rus',
|
||||
'es': 'spa', 'it': 'ita', 'pt': 'por', 'ja': 'jpn', 'ko': 'kor',
|
||||
'zh': 'chi', 'und': 'und'
|
||||
}
|
||||
langs = [getattr(a, 'language', 'und').lower()[:3] for a in self.audio_tracks]
|
||||
langs = [lang_map.get(lang, lang) for lang in langs]
|
||||
lang_counts = Counter(langs)
|
||||
audio_langs = [f"{count}{lang}" if count > 1 else lang for lang, count in lang_counts.items()]
|
||||
return ','.join(audio_langs)
|
||||
|
||||
def extract_video_dimensions(self, file_path: Path) -> tuple[int, int] | None:
|
||||
def extract_video_dimensions(self) -> tuple[int, int] | None:
|
||||
"""Extract video width and height"""
|
||||
try:
|
||||
media_info = MediaInfo.parse(file_path)
|
||||
video_tracks = [t for t in media_info.tracks if t.track_type == 'Video']
|
||||
if video_tracks:
|
||||
width = getattr(video_tracks[0], 'width', None)
|
||||
height = getattr(video_tracks[0], 'height', None)
|
||||
if width and height:
|
||||
return width, height
|
||||
except:
|
||||
pass
|
||||
if not self.video_tracks:
|
||||
return None
|
||||
width = getattr(self.video_tracks[0], 'width', None)
|
||||
height = getattr(self.video_tracks[0], 'height', None)
|
||||
if width and height:
|
||||
return width, height
|
||||
return None
|
||||
|
||||
def extract_tracks(self) -> str:
|
||||
"""Extract compact media track information"""
|
||||
tracks_info = []
|
||||
try:
|
||||
# Video tracks
|
||||
for i, v in enumerate(self.video_tracks[:2]): # Up to 2 videos
|
||||
codec = getattr(v, 'format', None) or getattr(v, 'codec', None) or 'unknown'
|
||||
width = getattr(v, 'width', None) or '?'
|
||||
height = getattr(v, 'height', None) or '?'
|
||||
bitrate = getattr(v, 'bit_rate', None)
|
||||
fps = getattr(v, 'frame_rate', None)
|
||||
profile = getattr(v, 'format_profile', None)
|
||||
|
||||
video_str = f"{codec} {width}x{height}"
|
||||
if bitrate:
|
||||
video_str += f" {bitrate}bps"
|
||||
if fps:
|
||||
video_str += f" {fps}fps"
|
||||
if profile:
|
||||
video_str += f" ({profile})"
|
||||
|
||||
tracks_info.append(ColorFormatter.green(f"Video {i+1}: {video_str}"))
|
||||
|
||||
# Audio tracks
|
||||
for i, a in enumerate(self.audio_tracks[:3]): # Up to 3 audios
|
||||
codec = getattr(a, 'format', None) or getattr(a, 'codec', None) or 'unknown'
|
||||
channels = getattr(a, 'channel_s', None) or '?'
|
||||
lang = getattr(a, 'language', None) or 'und'
|
||||
bitrate = getattr(a, 'bit_rate', None)
|
||||
|
||||
audio_str = f"{codec} {channels}ch {lang}"
|
||||
if bitrate:
|
||||
audio_str += f" {bitrate}bps"
|
||||
|
||||
tracks_info.append(ColorFormatter.yellow(f"Audio {i+1}: {audio_str}"))
|
||||
|
||||
# Subtitle tracks
|
||||
for i, s in enumerate(self.sub_tracks[:3]): # Up to 3 subs
|
||||
lang = getattr(s, 'language', None) or 'und'
|
||||
format = getattr(s, 'format', None) or getattr(s, 'codec', None) or 'unknown'
|
||||
|
||||
sub_str = f"{lang} ({format})"
|
||||
tracks_info.append(ColorFormatter.magenta(f"Sub {i+1}: {sub_str}"))
|
||||
|
||||
except Exception as e:
|
||||
tracks_info.append(ColorFormatter.red(f"Track info error: {str(e)}"))
|
||||
|
||||
return "\n".join(tracks_info) if tracks_info else ""
|
||||
@@ -1,48 +1,63 @@
|
||||
import mutagen
|
||||
from pathlib import Path
|
||||
from ..constants import MEDIA_TYPES
|
||||
|
||||
|
||||
class MetadataExtractor:
|
||||
"""Class to extract information from file metadata"""
|
||||
|
||||
@staticmethod
|
||||
def extract_title(file_path: Path) -> str | None:
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
try:
|
||||
self.info = mutagen.File(file_path) # type: ignore
|
||||
except Exception:
|
||||
self.info = None
|
||||
|
||||
def extract_title(self) -> str | None:
|
||||
"""Extract title from metadata"""
|
||||
try:
|
||||
info = mutagen.File(file_path)
|
||||
if info:
|
||||
return getattr(info, 'title', None) or getattr(info, 'get', lambda x, default=None: default)('title', [None])[0]
|
||||
except:
|
||||
pass
|
||||
if self.info:
|
||||
return getattr(self.info, 'title', None) or getattr(self.info, 'get', lambda x, default=None: default)('title', [None])[0] # type: ignore
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def extract_duration(file_path: Path) -> float | None:
|
||||
def extract_duration(self) -> float | None:
|
||||
"""Extract duration from metadata"""
|
||||
try:
|
||||
info = mutagen.File(file_path)
|
||||
if info:
|
||||
return getattr(info, 'length', None)
|
||||
except:
|
||||
pass
|
||||
if self.info:
|
||||
return getattr(self.info, 'length', None)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def extract_artist(file_path: Path) -> str | None:
|
||||
def extract_artist(self) -> str | None:
|
||||
"""Extract artist from metadata"""
|
||||
try:
|
||||
info = mutagen.File(file_path)
|
||||
if info:
|
||||
return getattr(info, 'artist', None) or getattr(info, 'get', lambda x, default=None: default)('artist', [None])[0]
|
||||
except:
|
||||
pass
|
||||
if self.info:
|
||||
return getattr(self.info, 'artist', None) or getattr(self.info, 'get', lambda x, default=None: default)('artist', [None])[0] # type: ignore
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def extract_all_metadata(file_path: Path) -> dict:
|
||||
def extract_all_metadata(self) -> dict:
|
||||
"""Extract all metadata"""
|
||||
return {
|
||||
'title': MetadataExtractor.extract_title(file_path),
|
||||
'duration': MetadataExtractor.extract_duration(file_path),
|
||||
'artist': MetadataExtractor.extract_artist(file_path)
|
||||
'title': self.extract_title(),
|
||||
'duration': self.extract_duration(),
|
||||
'artist': self.extract_artist()
|
||||
}
|
||||
|
||||
def extract_meta_type(self) -> str:
|
||||
"""Extract meta type from metadata"""
|
||||
if self.info:
|
||||
return type(self.info).__name__
|
||||
return self._detect_by_mime()
|
||||
|
||||
def extract_meta_description(self) -> str:
|
||||
"""Extract meta description"""
|
||||
meta_type = self.extract_meta_type()
|
||||
return {info['meta_type']: info['description'] for info in MEDIA_TYPES.values()}.get(meta_type, f'Unknown type {meta_type}')
|
||||
|
||||
def _detect_by_mime(self) -> str:
|
||||
"""Detect meta type by MIME"""
|
||||
try:
|
||||
import magic
|
||||
mime = magic.from_file(str(self.file_path), mime=True)
|
||||
for ext, info in MEDIA_TYPES.items():
|
||||
if info['mime'] == mime:
|
||||
return info['meta_type']
|
||||
return 'Unknown'
|
||||
except Exception:
|
||||
return 'Unknown'
|
||||
54
renamer/formatters/color_formatter.py
Normal file
54
renamer/formatters/color_formatter.py
Normal file
@@ -0,0 +1,54 @@
|
||||
class ColorFormatter:
|
||||
"""Class for formatting text with colors using Textual markup"""
|
||||
|
||||
@staticmethod
|
||||
def bold_blue(text: str) -> str:
|
||||
return f"[bold blue]{text}[/bold blue]"
|
||||
|
||||
@staticmethod
|
||||
def bold_green(text: str) -> str:
|
||||
return f"[bold green]{text}[/bold green]"
|
||||
|
||||
@staticmethod
|
||||
def bold_cyan(text: str) -> str:
|
||||
return f"[bold cyan]{text}[/bold cyan]"
|
||||
|
||||
@staticmethod
|
||||
def bold_magenta(text: str) -> str:
|
||||
return f"[bold magenta]{text}[/bold magenta]"
|
||||
|
||||
@staticmethod
|
||||
def bold_yellow(text: str) -> str:
|
||||
return f"[bold yellow]{text}[/bold yellow]"
|
||||
|
||||
@staticmethod
|
||||
def bold_red(text: str) -> str:
|
||||
return f"[bold red]{text}[/bold red]"
|
||||
|
||||
@staticmethod
|
||||
def green(text: str) -> str:
|
||||
return f"[green]{text}[/green]"
|
||||
|
||||
@staticmethod
|
||||
def yellow(text: str) -> str:
|
||||
return f"[yellow]{text}[/yellow]"
|
||||
|
||||
@staticmethod
|
||||
def magenta(text: str) -> str:
|
||||
return f"[magenta]{text}[/magenta]"
|
||||
|
||||
@staticmethod
|
||||
def cyan(text: str) -> str:
|
||||
return f"[cyan]{text}[/cyan]"
|
||||
|
||||
@staticmethod
|
||||
def red(text: str) -> str:
|
||||
return f"[red]{text}[/red]"
|
||||
|
||||
@staticmethod
|
||||
def grey(text: str) -> str:
|
||||
return f"[grey]{text}[/grey]"
|
||||
|
||||
@staticmethod
|
||||
def dim(text: str) -> str:
|
||||
return f"[dim]{text}[/dim]"
|
||||
@@ -8,3 +8,8 @@ class DateFormatter:
|
||||
def format_modification_date(mtime: float) -> str:
|
||||
"""Format file modification time"""
|
||||
return datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
@staticmethod
|
||||
def format_year(year: float | None) -> str:
|
||||
"""Format year from float to string"""
|
||||
return f"({year})" if year else ""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from ..constants import VIDEO_EXT_DESCRIPTIONS
|
||||
from ..constants import MEDIA_TYPES
|
||||
|
||||
|
||||
class ExtensionExtractor:
|
||||
@@ -13,4 +13,4 @@ class ExtensionExtractor:
|
||||
@staticmethod
|
||||
def get_extension_description(ext_name: str) -> str:
|
||||
"""Get description for extension"""
|
||||
return VIDEO_EXT_DESCRIPTIONS.get(ext_name, f'Unknown extension .{ext_name}')
|
||||
return MEDIA_TYPES.get(ext_name, {}).get('description', f'Unknown extension .{ext_name}')
|
||||
@@ -1,6 +1,6 @@
|
||||
from pathlib import Path
|
||||
from ..constants import VIDEO_EXT_DESCRIPTIONS
|
||||
from ..utils import detect_file_type
|
||||
from ..constants import MEDIA_TYPES
|
||||
from .color_formatter import ColorFormatter
|
||||
|
||||
|
||||
class ExtensionFormatter:
|
||||
@@ -9,21 +9,7 @@ class ExtensionFormatter:
|
||||
@staticmethod
|
||||
def check_extension_match(ext_name: str, meta_type: str) -> bool:
|
||||
"""Check if file extension matches detected type"""
|
||||
if ext_name.upper() == meta_type:
|
||||
return True
|
||||
elif ext_name == 'mkv' and meta_type == 'Matroska':
|
||||
return True
|
||||
elif ext_name == 'avi' and meta_type == 'AVI':
|
||||
return True
|
||||
elif ext_name == 'mov' and meta_type == 'QuickTime':
|
||||
return True
|
||||
elif ext_name == 'wmv' and meta_type == 'ASF':
|
||||
return True
|
||||
elif ext_name == 'flv' and meta_type == 'FLV':
|
||||
return True
|
||||
elif ext_name == 'webm' and meta_type == 'WebM':
|
||||
return True
|
||||
elif ext_name == 'ogv' and meta_type == 'Ogg':
|
||||
if ext_name in MEDIA_TYPES and MEDIA_TYPES[ext_name]['meta_type'] == meta_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -31,8 +17,8 @@ class ExtensionFormatter:
|
||||
def format_extension_info(ext_name: str, ext_desc: str, meta_type: str, meta_desc: str, match: bool) -> str:
|
||||
"""Format extension information with match status"""
|
||||
if match:
|
||||
return f"[bold green]Extension:[/bold green] {ext_name} - [grey]{ext_desc}[/grey]"
|
||||
return f"{ColorFormatter.bold_green('Extension:')} {ext_name} - {ColorFormatter.grey(ext_desc)}"
|
||||
else:
|
||||
return (f"[bold yellow]Extension:[/bold yellow] {ext_name} - [grey]{ext_desc}[/grey]\n"
|
||||
f"[bold red]Meta extension:[/bold red] {meta_type} - [grey]{meta_desc}[/grey]\n"
|
||||
"[bold red]Warning: Extensions do not match![/bold red]")
|
||||
return (f"{ColorFormatter.bold_yellow('Extension:')} {ext_name} - {ColorFormatter.grey(ext_desc)}\n"
|
||||
f"{ColorFormatter.bold_red('Meta extension:')} {meta_type} - {ColorFormatter.grey(meta_desc)}\n"
|
||||
f"{ColorFormatter.bold_red('Warning: Extensions do not match!')}")
|
||||
@@ -3,122 +3,200 @@ from .size_formatter import SizeFormatter
|
||||
from .date_formatter import DateFormatter
|
||||
from .extension_extractor import ExtensionExtractor
|
||||
from .extension_formatter import ExtensionFormatter
|
||||
from ..utils import detect_file_type
|
||||
from .color_formatter import ColorFormatter
|
||||
|
||||
|
||||
class MediaFormatter:
|
||||
"""Class to format media data for display"""
|
||||
|
||||
def format_file_info_panel(self, file_path: Path, rename_data: dict) -> str:
|
||||
def format_file_info_panel(self, extractor) -> str:
|
||||
"""Format file information for the file info panel"""
|
||||
output = []
|
||||
|
||||
# Panel title
|
||||
output.append("[bold blue]FILE INFO[/bold blue]")
|
||||
output.append("") # Empty line for spacing
|
||||
|
||||
# Get file stats
|
||||
size_full = SizeFormatter.format_size_full(file_path.stat().st_size)
|
||||
date_formatted = DateFormatter.format_modification_date(file_path.stat().st_mtime)
|
||||
data = [
|
||||
{
|
||||
"label": "Path",
|
||||
"value": extractor.get("file_path"),
|
||||
"format_func": ColorFormatter.bold_blue,
|
||||
},
|
||||
{
|
||||
"label": "Size",
|
||||
"value": SizeFormatter.format_size_full(extractor.get("file_size")),
|
||||
"format_func": ColorFormatter.bold_green,
|
||||
},
|
||||
{
|
||||
"label": "File",
|
||||
"value": extractor.get("file_name"),
|
||||
"format_func": ColorFormatter.bold_cyan,
|
||||
},
|
||||
{
|
||||
"label": "Modified",
|
||||
"value": DateFormatter.format_modification_date(
|
||||
extractor.get("modification_time")
|
||||
),
|
||||
"format_func": ColorFormatter.bold_magenta,
|
||||
},
|
||||
]
|
||||
|
||||
# Get extension info
|
||||
ext_name = ExtensionExtractor.get_extension_name(file_path)
|
||||
ext_name = ExtensionExtractor.get_extension_name(
|
||||
Path(extractor.get("file_path"))
|
||||
)
|
||||
ext_desc = ExtensionExtractor.get_extension_description(ext_name)
|
||||
meta_type, meta_desc = detect_file_type(file_path)
|
||||
meta_type = extractor.get("meta_type")
|
||||
meta_desc = extractor.get("meta_description")
|
||||
match = ExtensionFormatter.check_extension_match(ext_name, meta_type)
|
||||
ext_info = ExtensionFormatter.format_extension_info(ext_name, ext_desc, meta_type, meta_desc, match)
|
||||
ext_info = ExtensionFormatter.format_extension_info(
|
||||
ext_name, ext_desc, meta_type, meta_desc, match
|
||||
)
|
||||
|
||||
file_name = file_path.name
|
||||
output = [ColorFormatter.bold_blue("FILE INFO"), ""]
|
||||
output.extend(
|
||||
item["format_func"](f"{item['label']}: {item['value']}") for item in data
|
||||
)
|
||||
output.append(ext_info)
|
||||
|
||||
output.append(f"[bold blue]Path:[/bold blue] {str(file_path)}")
|
||||
output.append(f"[bold green]Size:[/bold green] {size_full}")
|
||||
output.append(f"[bold cyan]File:[/bold cyan] {file_name}")
|
||||
output.append(f"[bold magenta]Modified:[/bold magenta] {date_formatted}")
|
||||
output.append(f"{ext_info}")
|
||||
# Add tracks info
|
||||
tracks_text = extractor.get('tracks')
|
||||
if not tracks_text:
|
||||
tracks_text = ColorFormatter.grey("No track info available")
|
||||
output.append("")
|
||||
output.append(tracks_text)
|
||||
|
||||
# Add rename lines
|
||||
rename_lines = self.format_rename_lines(extractor)
|
||||
output.append("")
|
||||
output.extend(rename_lines)
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def format_filename_extraction_panel(self, rename_data: dict) -> str:
|
||||
def format_filename_extraction_panel(self, extractor) -> str:
|
||||
"""Format filename extraction data for the filename panel"""
|
||||
output = []
|
||||
output.append("[bold yellow]FILENAME EXTRACTION[/bold yellow]")
|
||||
output.append("") # Empty line for spacing
|
||||
output.append(f"[yellow]Title:[/yellow] {rename_data.get('title', 'Not found')}")
|
||||
output.append(f"[yellow]Year:[/yellow] {rename_data.get('year', 'Not found')}")
|
||||
output.append(f"[yellow]Source:[/yellow] {rename_data.get('source', 'Not found')}")
|
||||
output.append(f"[yellow]Frame Class:[/yellow] {rename_data.get('frame_class', 'Not found')}")
|
||||
data = [
|
||||
{
|
||||
"label": "Title",
|
||||
"value": extractor.get("title") or "Not found",
|
||||
"format_func": ColorFormatter.yellow,
|
||||
},
|
||||
{
|
||||
"label": "Year",
|
||||
"value": extractor.get("year") or "Not found",
|
||||
"format_func": ColorFormatter.yellow,
|
||||
},
|
||||
{
|
||||
"label": "Source",
|
||||
"value": extractor.get("source") or "Not found",
|
||||
"format_func": ColorFormatter.yellow,
|
||||
},
|
||||
{
|
||||
"label": "Frame Class",
|
||||
"value": extractor.get("frame_class") or "Not found",
|
||||
"format_func": ColorFormatter.yellow,
|
||||
},
|
||||
]
|
||||
|
||||
output = [ColorFormatter.bold_yellow("FILENAME EXTRACTION"), ""]
|
||||
output.extend(
|
||||
item["format_func"](f"{item['label']}: {item['value']}") for item in data
|
||||
)
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def format_metadata_extraction_panel(self, rename_data: dict) -> str:
|
||||
def format_metadata_extraction_panel(self, extractor) -> str:
|
||||
"""Format metadata extraction data for the metadata panel"""
|
||||
output = []
|
||||
output.append("[bold cyan]METADATA EXTRACTION[/bold cyan]")
|
||||
output.append("") # Empty line for spacing
|
||||
metadata = rename_data.get('metadata', {})
|
||||
if metadata.get('duration'):
|
||||
output.append(f"[cyan]Duration:[/cyan] {metadata['duration']:.1f} seconds")
|
||||
if metadata.get('title'):
|
||||
output.append(f"[cyan]Title:[/cyan] {metadata['title']}")
|
||||
if metadata.get('artist'):
|
||||
output.append(f"[cyan]Artist:[/cyan] {metadata['artist']}")
|
||||
if not any(key in metadata for key in ['duration', 'title', 'artist']):
|
||||
output.append("[dim]No metadata found[/dim]")
|
||||
return "\n".join(output) if output else "[dim]No metadata found[/dim]"
|
||||
metadata = extractor.get("metadata") or {}
|
||||
data = []
|
||||
if metadata.get("duration"):
|
||||
data.append(
|
||||
{
|
||||
"label": "Duration",
|
||||
"value": f"{metadata['duration']:.1f} seconds",
|
||||
"format_func": ColorFormatter.cyan,
|
||||
}
|
||||
)
|
||||
if metadata.get("title"):
|
||||
data.append(
|
||||
{
|
||||
"label": "Title",
|
||||
"value": metadata["title"],
|
||||
"format_func": ColorFormatter.cyan,
|
||||
}
|
||||
)
|
||||
if metadata.get("artist"):
|
||||
data.append(
|
||||
{
|
||||
"label": "Artist",
|
||||
"value": metadata["artist"],
|
||||
"format_func": ColorFormatter.cyan,
|
||||
}
|
||||
)
|
||||
|
||||
output = [ColorFormatter.bold_cyan("METADATA EXTRACTION"), ""]
|
||||
if data:
|
||||
output.extend(
|
||||
item["format_func"](f"{item['label']}: {item['value']}")
|
||||
for item in data
|
||||
)
|
||||
else:
|
||||
output.append(ColorFormatter.dim("No metadata found"))
|
||||
|
||||
def format_mediainfo_extraction_panel(self, rename_data: dict) -> str:
|
||||
"""Format media info extraction data for the mediainfo panel"""
|
||||
output = []
|
||||
output.append("[bold green]MEDIA INFO EXTRACTION[/bold green]")
|
||||
output.append("") # Empty line for spacing
|
||||
output.append(f"[green]Resolution:[/green] {rename_data.get('resolution', 'Not found')}")
|
||||
output.append(f"[green]Aspect Ratio:[/green] {rename_data.get('aspect_ratio', 'Not found')}")
|
||||
output.append(f"[green]HDR:[/green] {rename_data.get('hdr', 'Not found')}")
|
||||
output.append(f"[green]Audio Languages:[/green] {rename_data.get('audio_langs', 'Not found')}")
|
||||
return "\n".join(output)
|
||||
|
||||
def format_proposed_name(self, rename_data: dict, ext_name: str) -> str:
|
||||
"""Format the proposed filename"""
|
||||
proposed_parts = []
|
||||
if rename_data['title']:
|
||||
proposed_parts.append(rename_data['title'])
|
||||
if rename_data['year']:
|
||||
proposed_parts.append(f"({rename_data['year']})")
|
||||
if rename_data['source']:
|
||||
proposed_parts.append(rename_data['source'])
|
||||
def format_mediainfo_extraction_panel(self, extractor) -> str:
|
||||
"""Format media info extraction data for the mediainfo panel"""
|
||||
data = [
|
||||
{
|
||||
"label": "Resolution",
|
||||
"value": extractor.get("resolution") or "Not found",
|
||||
"format_func": ColorFormatter.green,
|
||||
},
|
||||
{
|
||||
"label": "Aspect Ratio",
|
||||
"value": extractor.get("aspect_ratio") or "Not found",
|
||||
"format_func": ColorFormatter.green,
|
||||
},
|
||||
{
|
||||
"label": "HDR",
|
||||
"value": extractor.get("hdr") or "Not found",
|
||||
"format_func": ColorFormatter.green,
|
||||
},
|
||||
{
|
||||
"label": "Audio Languages",
|
||||
"value": extractor.get("audio_langs") or "Not found",
|
||||
"format_func": ColorFormatter.green,
|
||||
},
|
||||
]
|
||||
|
||||
tags = []
|
||||
if rename_data['frame_class'] and rename_data['frame_class'] != 'Unclassified':
|
||||
tags.append(rename_data['frame_class'])
|
||||
if rename_data['hdr']:
|
||||
tags.append(rename_data['hdr'])
|
||||
if rename_data['audio_langs']:
|
||||
tags.append(rename_data['audio_langs'])
|
||||
if tags:
|
||||
proposed_parts.append(f"[{','.join(tags)}]")
|
||||
output = [ColorFormatter.bold_green("MEDIA INFO EXTRACTION"), ""]
|
||||
output.extend(
|
||||
item["format_func"](f"{item['label']}: {item['value']}") for item in data
|
||||
)
|
||||
|
||||
return ' '.join(proposed_parts) + f".{ext_name}"
|
||||
return "\n".join(output)
|
||||
|
||||
def format_rename_lines(self, rename_data: dict, proposed_name: str) -> list[str]:
|
||||
def format_rename_lines(self, extractor) -> list[str]:
|
||||
"""Format the rename information lines"""
|
||||
lines = []
|
||||
lines.append(f"Movie title: {rename_data['title'] or 'Unknown'}")
|
||||
lines.append(f"Year: {rename_data['year'] or 'Unknown'}")
|
||||
lines.append(f"Video source: {rename_data['source'] or 'Unknown'}")
|
||||
lines.append(f"Frame class: {rename_data['frame_class'] or 'Unknown'}")
|
||||
lines.append(f"Resolution: {rename_data['resolution'] or 'Unknown'}")
|
||||
lines.append(f"Aspect ratio: {rename_data['aspect_ratio'] or 'Unknown'}")
|
||||
lines.append(f"HDR: {rename_data['hdr'] or 'No'}")
|
||||
lines.append(f"Audio langs: {rename_data['audio_langs'] or 'None'}")
|
||||
lines.append(f"Proposed filename: {proposed_name}")
|
||||
return lines
|
||||
data = {
|
||||
"Movie title": extractor.get("title") or "Unknown",
|
||||
"Year": extractor.get("year") or "Unknown",
|
||||
"Video source": extractor.get("source") or "Unknown",
|
||||
"Frame class": extractor.get("frame_class") or "Unknown",
|
||||
"Resolution": extractor.get("resolution") or "Unknown",
|
||||
"Aspect ratio": extractor.get("aspect_ratio") or "Unknown",
|
||||
"HDR": extractor.get("hdr") or "No",
|
||||
"Audio langs": extractor.get("audio_langs") or "None",
|
||||
}
|
||||
|
||||
return [f"{key}: {value}" for key, value in data.items()]
|
||||
|
||||
def _format_extra_metadata(self, metadata: dict) -> str:
|
||||
"""Format extra metadata like duration, title, artist"""
|
||||
extra_info = []
|
||||
if metadata.get('duration'):
|
||||
extra_info.append(f"[cyan]Duration:[/cyan] {metadata['duration']:.1f} seconds")
|
||||
if metadata.get('title'):
|
||||
extra_info.append(f"[cyan]Title:[/cyan] {metadata['title']}")
|
||||
if metadata.get('artist'):
|
||||
extra_info.append(f"[cyan]Artist:[/cyan] {metadata['artist']}")
|
||||
return "\n".join(extra_info) if extra_info else ""
|
||||
data = {}
|
||||
if metadata.get("duration"):
|
||||
data["Duration"] = f"{metadata['duration']:.1f} seconds"
|
||||
if metadata.get("title"):
|
||||
data["Title"] = metadata["title"]
|
||||
if metadata.get("artist"):
|
||||
data["Artist"] = metadata["artist"]
|
||||
|
||||
return "\n".join(
|
||||
ColorFormatter.cyan(f"{key}: {value}") for key, value in data.items()
|
||||
)
|
||||
|
||||
25
renamer/formatters/proposed_name_formatter.py
Normal file
25
renamer/formatters/proposed_name_formatter.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from .color_formatter import ColorFormatter
|
||||
from .date_formatter import DateFormatter
|
||||
|
||||
|
||||
class ProposedNameFormatter:
|
||||
"""Class for formatting proposed filenames"""
|
||||
|
||||
def __init__(self, extractor):
|
||||
self.extractor = extractor
|
||||
|
||||
self.__title = extractor.get("title") or "Unknown Title"
|
||||
self.__year = DateFormatter.format_year(extractor.get("year"))
|
||||
self.__source = extractor.get("source") or None
|
||||
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.__extension = extractor.get("extension") or "ext"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Convert the proposed name to string"""
|
||||
return f"{self.__title} {self.__year} {self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}].{self.__extension}"
|
||||
|
||||
def format_display_string(self) -> str:
|
||||
"""Format the proposed name for display with color"""
|
||||
return ColorFormatter.bold_yellow(str(self))
|
||||
@@ -1,93 +0,0 @@
|
||||
from pymediainfo import MediaInfo
|
||||
from .constants import META_DESCRIPTIONS
|
||||
import magic
|
||||
import mutagen
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def get_media_tracks(file_path):
|
||||
"""Extract compact media track information"""
|
||||
tracks_info = []
|
||||
try:
|
||||
media_info = MediaInfo.parse(file_path)
|
||||
video_tracks = [t for t in media_info.tracks if t.track_type == 'Video']
|
||||
audio_tracks = [t for t in media_info.tracks if t.track_type == 'Audio']
|
||||
sub_tracks = [t for t in media_info.tracks if t.track_type == 'Text']
|
||||
|
||||
# Video tracks
|
||||
for i, v in enumerate(video_tracks[:2]): # Up to 2 videos
|
||||
codec = getattr(v, 'format', None) or getattr(v, 'codec', None) or 'unknown'
|
||||
width = getattr(v, 'width', None) or '?'
|
||||
height = getattr(v, 'height', None) or '?'
|
||||
bitrate = getattr(v, 'bit_rate', None)
|
||||
fps = getattr(v, 'frame_rate', None)
|
||||
profile = getattr(v, 'format_profile', None)
|
||||
|
||||
video_str = f"{codec} {width}x{height}"
|
||||
if bitrate:
|
||||
video_str += f" {bitrate}bps"
|
||||
if fps:
|
||||
video_str += f" {fps}fps"
|
||||
if profile:
|
||||
video_str += f" ({profile})"
|
||||
|
||||
tracks_info.append(f"[green]Video {i+1}:[/green] {video_str}")
|
||||
|
||||
# Audio tracks
|
||||
for i, a in enumerate(audio_tracks[:3]): # Up to 3 audios
|
||||
codec = getattr(a, 'format', None) or getattr(a, 'codec', None) or 'unknown'
|
||||
channels = getattr(a, 'channel_s', None) or '?'
|
||||
lang = getattr(a, 'language', None) or 'und'
|
||||
bitrate = getattr(a, 'bit_rate', None)
|
||||
|
||||
audio_str = f"{codec} {channels}ch {lang}"
|
||||
if bitrate:
|
||||
audio_str += f" {bitrate}bps"
|
||||
|
||||
tracks_info.append(f"[yellow]Audio {i+1}:[/yellow] {audio_str}")
|
||||
|
||||
# Subtitle tracks
|
||||
for i, s in enumerate(sub_tracks[:3]): # Up to 3 subs
|
||||
lang = getattr(s, 'language', None) or 'und'
|
||||
format = getattr(s, 'format', None) or getattr(s, 'codec', None) or 'unknown'
|
||||
|
||||
sub_str = f"{lang} ({format})"
|
||||
tracks_info.append(f"[magenta]Sub {i+1}:[/magenta] {sub_str}")
|
||||
|
||||
except Exception as e:
|
||||
tracks_info.append(f"[red]Track info error: {str(e)}[/red]")
|
||||
|
||||
return "\n".join(tracks_info) if tracks_info else ""
|
||||
|
||||
|
||||
def detect_file_type(file_path):
|
||||
"""Detect file type and return meta_type and desc"""
|
||||
try:
|
||||
info = mutagen.File(file_path)
|
||||
if info is None:
|
||||
# Fallback to magic
|
||||
mime = magic.from_file(str(file_path), mime=True)
|
||||
if mime == 'video/x-matroska':
|
||||
return 'Matroska', 'Matroska multimedia container'
|
||||
elif mime == 'video/mp4':
|
||||
return 'MP4', 'MPEG-4 video container'
|
||||
elif mime == 'video/x-msvideo':
|
||||
return 'AVI', 'Audio Video Interleave'
|
||||
elif mime == 'video/quicktime':
|
||||
return 'QuickTime', 'QuickTime movie'
|
||||
elif mime == 'video/x-ms-wmv':
|
||||
return 'ASF', 'Windows Media'
|
||||
elif mime == 'video/x-flv':
|
||||
return 'FLV', 'Flash Video'
|
||||
elif mime == 'video/webm':
|
||||
return 'WebM', 'WebM multimedia'
|
||||
elif mime == 'video/ogg':
|
||||
return 'Ogg', 'Ogg multimedia'
|
||||
else:
|
||||
return 'Unknown', f'Unknown MIME: {mime}'
|
||||
else:
|
||||
meta_type = type(info).__name__
|
||||
meta_desc = META_DESCRIPTIONS.get(meta_type, f'Unknown type {meta_type}')
|
||||
return meta_type, meta_desc
|
||||
except Exception as e:
|
||||
return f'Error: {str(e)}', f'Error detecting type'
|
||||
Reference in New Issue
Block a user