feat: Add duration extraction and formatting utilities

This commit is contained in:
sHa
2025-12-26 12:02:36 +00:00
parent 1d6eb9593e
commit 6d377c9871
5 changed files with 139 additions and 112 deletions

View File

@@ -43,9 +43,6 @@ class MediaExtractor:
'audio_langs': [ 'audio_langs': [
('MediaInfo', lambda: self.mediainfo_extractor.extract_audio_langs()) ('MediaInfo', lambda: self.mediainfo_extractor.extract_audio_langs())
], ],
'metadata': [
('Metadata', lambda: self.metadata_extractor.extract_all_metadata())
],
'meta_type': [ 'meta_type': [
('Metadata', lambda: self.metadata_extractor.extract_meta_type()) ('Metadata', lambda: self.metadata_extractor.extract_meta_type())
], ],
@@ -88,9 +85,7 @@ class MediaExtractor:
def get(self, key: str, source: str | None = None): def get(self, key: str, source: str | None = None):
"""Get extracted data by key, optionally from specific source""" """Get extracted data by key, optionally from specific source"""
if key not in self._sources: if key in self._sources:
raise ValueError(f"Unknown key: {key}")
condition = self._conditions.get(key, lambda x: x is not None) condition = self._conditions.get(key, lambda x: x is not None)
if source: if source:
@@ -106,3 +101,28 @@ class MediaExtractor:
if condition(val): if condition(val):
return val return val
return None return None
else:
# Key not in _sources, try to call extract_<key> 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

View File

@@ -28,6 +28,14 @@ class MediaInfoExtractor:
return frame_class return frame_class
return 'Unclassified' 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: def extract_frame_class(self) -> str | None:
"""Extract frame class from media info (480p, 720p, 1080p, etc.)""" """Extract frame class from media info (480p, 720p, 1080p, etc.)"""
if not self.video_tracks: if not self.video_tracks:

View File

@@ -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 getattr(self.info, 'artist', None) or getattr(self.info, 'get', lambda x, default=None: default)('artist', [None])[0] # type: ignore
return None 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: def extract_meta_type(self) -> str:
"""Extract meta type from metadata""" """Extract meta type from metadata"""
if self.info: if self.info:

View File

@@ -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)"

View File

@@ -6,6 +6,7 @@ from .extension_formatter import ExtensionFormatter
from .text_formatter import TextFormatter from .text_formatter import TextFormatter
from .track_formatter import TrackFormatter from .track_formatter import TrackFormatter
from .resolution_formatter import ResolutionFormatter from .resolution_formatter import ResolutionFormatter
from .duration_formatter import DurationFormatter
class MediaFormatter: class MediaFormatter:
@@ -25,7 +26,7 @@ class MediaFormatter:
# Handle value formatting first (e.g., size formatting) # Handle value formatting first (e.g., size formatting)
value = item.get("value") 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", []) value_formatters = item.get("value_formatters", [])
if not isinstance(value_formatters, list): if not isinstance(value_formatters, list):
value_formatters = [value_formatters] if value_formatters else [] value_formatters = [value_formatters] if value_formatters else []
@@ -66,22 +67,14 @@ class MediaFormatter:
def file_info_panel(self) -> str: def file_info_panel(self) -> str:
"""Return formatted file info panel string""" """Return formatted file info panel string"""
sections = [
output = self.file_info() self.file_info(),
self.tracks_info(),
# Add tracks info self.filename_extracted_data(),
output.append("") self.metadata_extracted_data(),
output.extend(self.tracks_info()) self.mediainfo_extracted_data(),
]
# Add filename extracted data return "\n\n".join("\n".join(section) for section in sections)
output.append("")
output.extend(self.filename_extracted_data())
# Add mediainfo extracted data
output.append("")
output.extend(self.mediainfo_extracted_data())
return "\n".join(output)
def file_info(self) -> list[str]: def file_info(self) -> list[str]:
data = [ data = [
@@ -94,13 +87,13 @@ class MediaFormatter:
"group": "File Info", "group": "File Info",
"label": "Path", "label": "Path",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],
"value": self.extractor.get("file_path"), "value": self.extractor.get("file_path", "FileInfo"),
"display_formatters": [TextFormatter.blue], "display_formatters": [TextFormatter.blue],
}, },
{ {
"group": "File Info", "group": "File Info",
"label": "Size", "label": "Size",
"value": self.extractor.get("file_size"), "value": self.extractor.get("file_size", "FileInfo"),
"value_formatters": [SizeFormatter.format_size_full], "value_formatters": [SizeFormatter.format_size_full],
"display_formatters": [TextFormatter.bold, TextFormatter.green], "display_formatters": [TextFormatter.bold, TextFormatter.green],
}, },
@@ -108,14 +101,14 @@ class MediaFormatter:
"group": "File Info", "group": "File Info",
"label": "Name", "label": "Name",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],
"value": self.extractor.get("file_name"), "value": self.extractor.get("file_name", "FileInfo"),
"display_formatters": [TextFormatter.cyan], "display_formatters": [TextFormatter.cyan],
}, },
{ {
"group": "File Info", "group": "File Info",
"label": "Modified", "label": "Modified",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],
"value": self.extractor.get("modification_time"), "value": self.extractor.get("modification_time", "FileInfo"),
"value_formatters": [DateFormatter.format_modification_date], "value_formatters": [DateFormatter.format_modification_date],
"display_formatters": [TextFormatter.bold, TextFormatter.magenta], "display_formatters": [TextFormatter.bold, TextFormatter.magenta],
}, },
@@ -123,7 +116,7 @@ class MediaFormatter:
"group": "File Info", "group": "File Info",
"label": "Extension", "label": "Extension",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],
"value": self.extractor.get("extension"), "value": self.extractor.get("extension", "FileInfo"),
"value_formatters": [ExtensionFormatter.format_extension_info], "value_formatters": [ExtensionFormatter.format_extension_info],
"display_formatters": [TextFormatter.green], "display_formatters": [TextFormatter.green],
}, },
@@ -176,74 +169,42 @@ class MediaFormatter:
return [self._format_data_item(item) for item in data] return [self._format_data_item(item) for item in data]
def format_filename_extraction_panel(self) -> str: def metadata_extracted_data(self) -> list[str]:
"""Format filename extraction data for the filename panel""" """Format metadata extraction data for the metadata panel"""
data = [ data = [
{
"label": "Metadata Extraction",
"label_formatters": [TextFormatter.bold, TextFormatter.uppercase],
},
{ {
"label": "Title", "label": "Title",
"value": self.extractor.get("title") or "Not found", "label_formatters": [TextFormatter.bold],
"display_formatters": [TextFormatter.yellow], "value": self.extractor.get("title", "Metadata") or "Not extracted",
"display_formatters": [TextFormatter.grey],
}, },
{ {
"label": "Year", "label": "Duration",
"value": self.extractor.get("year") or "Not found", "label_formatters": [TextFormatter.bold],
"display_formatters": [TextFormatter.yellow], "value": self.extractor.get("duration", "Metadata") or "Not extracted",
"value_formatters": [DurationFormatter.format_full],
"display_formatters": [TextFormatter.grey],
}, },
{ {
"label": "Source", "label": "Artist",
"value": self.extractor.get("source") or "Not found", "label_formatters": [TextFormatter.bold],
"display_formatters": [TextFormatter.yellow], "value": self.extractor.get("artist", "Metadata") or "Not extracted",
"display_formatters": [TextFormatter.grey],
}, },
{ {
"label": "Frame Class", "label": "Description",
"value": self.extractor.get("frame_class") or "Not found", "label_formatters": [TextFormatter.bold],
"display_formatters": [TextFormatter.yellow], "value": self.extractor.get("meta_description", "Metadata")
or "Not extracted",
"display_formatters": [TextFormatter.grey],
}, },
] ]
output = [TextFormatter.bold_yellow("FILENAME EXTRACTION"), ""] return [self._format_data_item(item) for item in data]
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)
def mediainfo_extracted_data(self) -> list[str]: def mediainfo_extracted_data(self) -> list[str]:
"""Format media info extraction data for the mediainfo panel""" """Format media info extraction data for the mediainfo panel"""
@@ -252,6 +213,13 @@ class MediaFormatter:
"label": "Media Info Extraction", "label": "Media Info Extraction",
"label_formatters": [TextFormatter.bold, TextFormatter.uppercase], "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": "Frame Class",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],
@@ -322,13 +290,6 @@ class MediaFormatter:
or "Not extracted", or "Not extracted",
"display_formatters": [TextFormatter.grey], "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": "HDR",
"label_formatters": [TextFormatter.bold], "label_formatters": [TextFormatter.bold],