feat: Add duration extraction and formatting utilities
This commit is contained in:
@@ -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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
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 .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],
|
||||||
|
|||||||
Reference in New Issue
Block a user