From d2ec2354586db53ffa6c74719686c7089e4ae59f Mon Sep 17 00:00:00 2001 From: sHa Date: Thu, 25 Dec 2025 23:35:59 +0000 Subject: [PATCH] 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. --- renamer/app.py | 88 +++--- renamer/constants.py | 131 +++++---- renamer/extractor.py | 159 ++++++----- renamer/extractors/fileinfo_extractor.py | 31 ++- renamer/extractors/filename_extractor.py | 35 +-- renamer/extractors/mediainfo_extractor.py | 174 +++++++----- renamer/extractors/metadata_extractor.py | 75 +++-- renamer/formatters/color_formatter.py | 54 ++++ renamer/formatters/date_formatter.py | 9 +- renamer/formatters/extension_extractor.py | 4 +- renamer/formatters/extension_formatter.py | 28 +- renamer/formatters/media_formatter.py | 258 ++++++++++++------ renamer/formatters/proposed_name_formatter.py | 25 ++ renamer/utils.py | 93 ------- 14 files changed, 664 insertions(+), 500 deletions(-) create mode 100644 renamer/formatters/color_formatter.py create mode 100644 renamer/formatters/proposed_name_formatter.py delete mode 100644 renamer/utils.py diff --git a/renamer/app.py b/renamer/app.py index ce79ad9..ef17cfe 100644 --- a/renamer/app.py +++ b/renamer/app.py @@ -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() - - # Extract all data - rename_data = extractor.extract_all(file_path) - - # 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) + try: + # Initialize extractors and formatters + extractor = MediaExtractor(file_path) + formatter = MediaFormatter() + name_formatter = ProposedNameFormatter(extractor) - def _update_details(self, full_info: str, proposed_name: str): + # 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)}"), + "", + ) + + 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]") - def action_quit(self): + proposed = self.query_one("#proposed", Static) + proposed.update(display_string) + + 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 @@ -166,4 +172,4 @@ class RenamerApp(App): node.collapse() else: tree.cursor_line = node.parent.line - event.prevent_default() \ No newline at end of file + event.prevent_default() diff --git a/renamer/constants.py b/renamer/constants.py index 7759964..fd5ea5e 100644 --- a/renamer/constants.py +++ b/renamer/constants.py @@ -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' - } -} \ No newline at end of file + "4320p": { + "nominal_height": 4320, + "typical_widths": [7680], + "description": "Ultra High Definition (UHD) - 4320p 8K", + }, +} diff --git a/renamer/extractor.py b/renamer/extractor.py index 6afce80..f7c60a1 100644 --- a/renamer/extractor.py +++ b/renamer/extractor.py @@ -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) + + # 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 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) - } \ No newline at end of file + 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 \ No newline at end of file diff --git a/renamer/extractors/fileinfo_extractor.py b/renamer/extractors/fileinfo_extractor.py index 12f46b3..192bf0e 100644 --- a/renamer/extractors/fileinfo_extractor.py +++ b/renamer/extractors/fileinfo_extractor.py @@ -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) \ No newline at end of file + return self._file_path + + def extract_extension(self) -> str: + """Extract file extension without the dot""" + return self.file_path.suffix.lower().lstrip('.') \ No newline at end of file diff --git a/renamer/extractors/filename_extractor.py b/renamer/extractors/filename_extractor.py index da575a2..8408e71 100644 --- a/renamer/extractors/filename_extractor.py +++ b/renamer/extractors/filename_extractor.py @@ -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' \ No newline at end of file diff --git a/renamer/extractors/mediainfo_extractor.py b/renamer/extractors/mediainfo_extractor.py index ace74cc..67a2083 100644 --- a/renamer/extractors/mediainfo_extractor.py +++ b/renamer/extractors/mediainfo_extractor.py @@ -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""" + 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: - 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 - return None \ No newline at end of file + # 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 "" \ No newline at end of file diff --git a/renamer/extractors/metadata_extractor.py b/renamer/extractors/metadata_extractor.py index 38de6bf..c9bca69 100644 --- a/renamer/extractors/metadata_extractor.py +++ b/renamer/extractors/metadata_extractor.py @@ -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) - } \ No newline at end of file + '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' \ No newline at end of file diff --git a/renamer/formatters/color_formatter.py b/renamer/formatters/color_formatter.py new file mode 100644 index 0000000..7c78eb7 --- /dev/null +++ b/renamer/formatters/color_formatter.py @@ -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]" \ No newline at end of file diff --git a/renamer/formatters/date_formatter.py b/renamer/formatters/date_formatter.py index d7210e1..568dec0 100644 --- a/renamer/formatters/date_formatter.py +++ b/renamer/formatters/date_formatter.py @@ -3,8 +3,13 @@ from datetime import datetime class DateFormatter: """Class for formatting dates""" - + @staticmethod def format_modification_date(mtime: float) -> str: """Format file modification time""" - return datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S") \ No newline at end of file + 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 "" diff --git a/renamer/formatters/extension_extractor.py b/renamer/formatters/extension_extractor.py index 6006a4b..0891b66 100644 --- a/renamer/formatters/extension_extractor.py +++ b/renamer/formatters/extension_extractor.py @@ -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}') \ No newline at end of file + return MEDIA_TYPES.get(ext_name, {}).get('description', f'Unknown extension .{ext_name}') \ No newline at end of file diff --git a/renamer/formatters/extension_formatter.py b/renamer/formatters/extension_formatter.py index 05e5d83..07506c4 100644 --- a/renamer/formatters/extension_formatter.py +++ b/renamer/formatters/extension_formatter.py @@ -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]") \ No newline at end of file + 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!')}") \ No newline at end of file diff --git a/renamer/formatters/media_formatter.py b/renamer/formatters/media_formatter.py index e373932..e0df9a3 100644 --- a/renamer/formatters/media_formatter.py +++ b/renamer/formatters/media_formatter.py @@ -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 "" \ No newline at end of file + 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() + ) diff --git a/renamer/formatters/proposed_name_formatter.py b/renamer/formatters/proposed_name_formatter.py new file mode 100644 index 0000000..1ebd341 --- /dev/null +++ b/renamer/formatters/proposed_name_formatter.py @@ -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)) diff --git a/renamer/utils.py b/renamer/utils.py deleted file mode 100644 index 4024da1..0000000 --- a/renamer/utils.py +++ /dev/null @@ -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' \ No newline at end of file