feat: Add duration extraction and formatting utilities
This commit is contained in:
@@ -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}")
|
||||
if key in self._sources:
|
||||
condition = self._conditions.get(key, lambda x: x is not None)
|
||||
|
||||
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 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
|
||||
# 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
46
renamer/formatters/duration_formatter.py
Normal file
46
renamer/formatters/duration_formatter.py
Normal 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)"
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user