From 6d377c9871884018e728cbdf2531594e970c33e2 Mon Sep 17 00:00:00 2001 From: sHa Date: Fri, 26 Dec 2025 12:02:36 +0000 Subject: [PATCH] feat: Add duration extraction and formatting utilities --- renamer/extractor.py | 58 ++++++---- renamer/extractors/mediainfo_extractor.py | 8 ++ renamer/extractors/metadata_extractor.py | 8 -- renamer/formatters/duration_formatter.py | 46 ++++++++ renamer/formatters/media_formatter.py | 131 ++++++++-------------- 5 files changed, 139 insertions(+), 112 deletions(-) create mode 100644 renamer/formatters/duration_formatter.py diff --git a/renamer/extractor.py b/renamer/extractor.py index 913b392..f5643da 100644 --- a/renamer/extractor.py +++ b/renamer/extractor.py @@ -43,9 +43,6 @@ class MediaExtractor: '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()) ], @@ -88,21 +85,44 @@ class MediaExtractor: 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.lower() == source.lower(): + if key in self._sources: + condition = self._conditions.get(key, lambda x: x is not None) + + if source: + for src, func in self._sources[key]: + if src.lower() == source.lower(): + val = func() + return val if condition(val) else None + return None # Source not found for this key, return None + else: + # Use fallback: return first valid value + for src, func in self._sources[key]: val = func() - return val if condition(val) else None - return None # Source not found for this key, return None + if condition(val): + return val + return None 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 + # Key not in _sources, try to call extract_ on extractors + extract_method = f'extract_{key}' + extractors = [ + ('MediaInfo', self.mediainfo_extractor), + ('Metadata', self.metadata_extractor), + ('Filename', self.filename_extractor), + ('FileInfo', self.fileinfo_extractor) + ] + + if source: + for src_name, extractor in extractors: + if src_name.lower() == source.lower(): + if hasattr(extractor, extract_method): + val = getattr(extractor, extract_method)() + return val + return None + else: + # Try all extractors in order + for src_name, extractor in extractors: + if hasattr(extractor, extract_method): + val = getattr(extractor, extract_method)() + if val is not None: + return val + return None \ No newline at end of file diff --git a/renamer/extractors/mediainfo_extractor.py b/renamer/extractors/mediainfo_extractor.py index b539d94..3d066be 100644 --- a/renamer/extractors/mediainfo_extractor.py +++ b/renamer/extractors/mediainfo_extractor.py @@ -28,6 +28,14 @@ class MediaInfoExtractor: return frame_class return 'Unclassified' + def extract_duration(self) -> float | None: + """Extract duration from media info in seconds""" + if self.media_info: + for track in self.media_info.tracks: + if track.track_type == 'General': + return getattr(track, 'duration', 0) / 1000 if getattr(track, 'duration', None) else None + return None + def extract_frame_class(self) -> str | None: """Extract frame class from media info (480p, 720p, 1080p, etc.)""" if not self.video_tracks: diff --git a/renamer/extractors/metadata_extractor.py b/renamer/extractors/metadata_extractor.py index c9bca69..1d764b5 100644 --- a/renamer/extractors/metadata_extractor.py +++ b/renamer/extractors/metadata_extractor.py @@ -31,14 +31,6 @@ class MetadataExtractor: return getattr(self.info, 'artist', None) or getattr(self.info, 'get', lambda x, default=None: default)('artist', [None])[0] # type: ignore return None - def extract_all_metadata(self) -> dict: - """Extract all metadata""" - return { - '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: diff --git a/renamer/formatters/duration_formatter.py b/renamer/formatters/duration_formatter.py new file mode 100644 index 0000000..42e9653 --- /dev/null +++ b/renamer/formatters/duration_formatter.py @@ -0,0 +1,46 @@ +"""Duration formatting utilities""" + +import math + + +class DurationFormatter: + """Class to format duration values""" + + @staticmethod + def format_seconds(duration: float | None) -> str: + """Format duration as seconds: '1234 seconds'""" + if duration is None: + return "Unknown" + return f"{int(duration)} seconds" + + @staticmethod + def format_hhmmss(duration: float | None) -> str: + """Format duration as HH:MM:SS""" + if duration is None: + return "Unknown" + total_seconds = int(duration) + hours = total_seconds // 3600 + minutes = (total_seconds % 3600) // 60 + seconds = total_seconds % 60 + return f"{hours:02d}:{minutes:02d}:{seconds:02d}" + + @staticmethod + def format_hhmm(duration: float | None) -> str: + """Format duration as HH:MM (rounded)""" + if duration is None: + return "Unknown" + total_seconds = int(duration) + hours = total_seconds // 3600 + minutes = (total_seconds % 3600) // 60 + return f"{hours:02d}:{minutes:02d}" + + @staticmethod + def format_full(duration: float | None) -> str: + """Format duration as HH:MM:SS (1234 sec)""" + if duration is None: + return "Unknown" + total_seconds = int(duration) + hours = total_seconds // 3600 + minutes = (total_seconds % 3600) // 60 + seconds = total_seconds % 60 + return f"{hours:02d}:{minutes:02d}:{seconds:02d} ({total_seconds} sec)" \ No newline at end of file diff --git a/renamer/formatters/media_formatter.py b/renamer/formatters/media_formatter.py index 5d42e67..354abad 100644 --- a/renamer/formatters/media_formatter.py +++ b/renamer/formatters/media_formatter.py @@ -6,6 +6,7 @@ from .extension_formatter import ExtensionFormatter from .text_formatter import TextFormatter from .track_formatter import TrackFormatter from .resolution_formatter import ResolutionFormatter +from .duration_formatter import DurationFormatter class MediaFormatter: @@ -25,7 +26,7 @@ class MediaFormatter: # Handle value formatting first (e.g., size formatting) value = item.get("value") - if value is not None: + if value is not None and not isinstance(value, str): value_formatters = item.get("value_formatters", []) if not isinstance(value_formatters, list): value_formatters = [value_formatters] if value_formatters else [] @@ -66,22 +67,14 @@ class MediaFormatter: def file_info_panel(self) -> str: """Return formatted file info panel string""" - - output = self.file_info() - - # Add tracks info - output.append("") - output.extend(self.tracks_info()) - - # Add filename extracted data - output.append("") - output.extend(self.filename_extracted_data()) - - # Add mediainfo extracted data - output.append("") - output.extend(self.mediainfo_extracted_data()) - - return "\n".join(output) + sections = [ + self.file_info(), + self.tracks_info(), + self.filename_extracted_data(), + self.metadata_extracted_data(), + self.mediainfo_extracted_data(), + ] + return "\n\n".join("\n".join(section) for section in sections) def file_info(self) -> list[str]: data = [ @@ -94,13 +87,13 @@ class MediaFormatter: "group": "File Info", "label": "Path", "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("file_path"), + "value": self.extractor.get("file_path", "FileInfo"), "display_formatters": [TextFormatter.blue], }, { "group": "File Info", "label": "Size", - "value": self.extractor.get("file_size"), + "value": self.extractor.get("file_size", "FileInfo"), "value_formatters": [SizeFormatter.format_size_full], "display_formatters": [TextFormatter.bold, TextFormatter.green], }, @@ -108,14 +101,14 @@ class MediaFormatter: "group": "File Info", "label": "Name", "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("file_name"), + "value": self.extractor.get("file_name", "FileInfo"), "display_formatters": [TextFormatter.cyan], }, { "group": "File Info", "label": "Modified", "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("modification_time"), + "value": self.extractor.get("modification_time", "FileInfo"), "value_formatters": [DateFormatter.format_modification_date], "display_formatters": [TextFormatter.bold, TextFormatter.magenta], }, @@ -123,7 +116,7 @@ class MediaFormatter: "group": "File Info", "label": "Extension", "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("extension"), + "value": self.extractor.get("extension", "FileInfo"), "value_formatters": [ExtensionFormatter.format_extension_info], "display_formatters": [TextFormatter.green], }, @@ -176,74 +169,42 @@ class MediaFormatter: return [self._format_data_item(item) for item in data] - def format_filename_extraction_panel(self) -> str: - """Format filename extraction data for the filename panel""" + def metadata_extracted_data(self) -> list[str]: + """Format metadata extraction data for the metadata panel""" data = [ + { + "label": "Metadata Extraction", + "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], + }, { "label": "Title", - "value": self.extractor.get("title") or "Not found", - "display_formatters": [TextFormatter.yellow], + "label_formatters": [TextFormatter.bold], + "value": self.extractor.get("title", "Metadata") or "Not extracted", + "display_formatters": [TextFormatter.grey], }, { - "label": "Year", - "value": self.extractor.get("year") or "Not found", - "display_formatters": [TextFormatter.yellow], + "label": "Duration", + "label_formatters": [TextFormatter.bold], + "value": self.extractor.get("duration", "Metadata") or "Not extracted", + "value_formatters": [DurationFormatter.format_full], + "display_formatters": [TextFormatter.grey], }, { - "label": "Source", - "value": self.extractor.get("source") or "Not found", - "display_formatters": [TextFormatter.yellow], + "label": "Artist", + "label_formatters": [TextFormatter.bold], + "value": self.extractor.get("artist", "Metadata") or "Not extracted", + "display_formatters": [TextFormatter.grey], }, { - "label": "Frame Class", - "value": self.extractor.get("frame_class") or "Not found", - "display_formatters": [TextFormatter.yellow], + "label": "Description", + "label_formatters": [TextFormatter.bold], + "value": self.extractor.get("meta_description", "Metadata") + or "Not extracted", + "display_formatters": [TextFormatter.grey], }, ] - output = [TextFormatter.bold_yellow("FILENAME EXTRACTION"), ""] - for item in data: - output.append(self._format_data_item(item)) - - return "\n".join(output) - - def format_metadata_extraction_panel(self) -> str: - """Format metadata extraction data for the metadata panel""" - metadata = self.extractor.get("metadata") or {} - data = [] - if metadata.get("duration"): - data.append( - { - "label": "Duration", - "value": f"{metadata['duration']:.1f} seconds", - "display_formatters": [TextFormatter.cyan], - } - ) - if metadata.get("title"): - data.append( - { - "label": "Title", - "value": metadata["title"], - "display_formatters": [TextFormatter.cyan], - } - ) - if metadata.get("artist"): - data.append( - { - "label": "Artist", - "value": metadata["artist"], - "display_formatters": [TextFormatter.cyan], - } - ) - - output = [TextFormatter.bold_cyan("METADATA EXTRACTION"), ""] - if data: - for item in data: - output.append(self._format_data_item(item)) - else: - output.append(TextFormatter.dim("No metadata found")) - - return "\n".join(output) + return [self._format_data_item(item) for item in data] def mediainfo_extracted_data(self) -> list[str]: """Format media info extraction data for the mediainfo panel""" @@ -252,6 +213,13 @@ class MediaFormatter: "label": "Media Info Extraction", "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], }, + { + "label": "Duration", + "label_formatters": [TextFormatter.bold], + "value": self.extractor.get("duration", "MediaInfo") or "Not extracted", + "value_formatters": [DurationFormatter.format_full], + "display_formatters": [TextFormatter.grey], + }, { "label": "Frame Class", "label_formatters": [TextFormatter.bold], @@ -322,13 +290,6 @@ class MediaFormatter: or "Not extracted", "display_formatters": [TextFormatter.grey], }, - { - "label": "Aspect ratio", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("aspect_ratio", "Filename") - or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, { "label": "HDR", "label_formatters": [TextFormatter.bold],