feat: Refactor formatting and extraction logic
- Added `langcodes` dependency for improved language handling. - Replaced `ColorFormatter` with `TextFormatter` for consistent text styling across the application. - Introduced `TrackFormatter` for better track information formatting. - Updated `MediaFormatter` to utilize new formatting methods and improved data handling. - Refactored `MediaExtractor` to enhance data extraction logic and improve readability. - Removed deprecated `ColorFormatter` methods and replaced them with `TextFormatter` equivalents. - Added new methods for extracting and formatting audio and subtitle tracks. - Updated tests to reflect changes in the extraction logic and formatting.
This commit is contained in:
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"renamer"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"python-magic>=0.4.27",
|
"python-magic>=0.4.27",
|
||||||
"pymediainfo>=6.0.0",
|
"pymediainfo>=6.0.0",
|
||||||
"pytest>=7.0.0",
|
"pytest>=7.0.0",
|
||||||
|
"langcodes>=3.5.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from .screens import OpenScreen
|
|||||||
from .extractor import MediaExtractor
|
from .extractor import MediaExtractor
|
||||||
from .formatters.media_formatter import MediaFormatter
|
from .formatters.media_formatter import MediaFormatter
|
||||||
from .formatters.proposed_name_formatter import ProposedNameFormatter
|
from .formatters.proposed_name_formatter import ProposedNameFormatter
|
||||||
from .formatters.color_formatter import ColorFormatter
|
from .formatters.text_formatter import TextFormatter
|
||||||
|
|
||||||
|
|
||||||
class RenamerApp(App):
|
class RenamerApp(App):
|
||||||
@@ -116,19 +116,17 @@ class RenamerApp(App):
|
|||||||
try:
|
try:
|
||||||
# Initialize extractors and formatters
|
# Initialize extractors and formatters
|
||||||
extractor = MediaExtractor(file_path)
|
extractor = MediaExtractor(file_path)
|
||||||
formatter = MediaFormatter()
|
|
||||||
name_formatter = ProposedNameFormatter(extractor)
|
|
||||||
|
|
||||||
# Update UI
|
# Update UI
|
||||||
self.call_later(
|
self.call_later(
|
||||||
self._update_details,
|
self._update_details,
|
||||||
formatter.format_file_info_panel(extractor),
|
MediaFormatter(extractor).file_info_panel(),
|
||||||
name_formatter.format_display_string(),
|
ProposedNameFormatter(extractor).rename_line_formatted(),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.call_later(
|
self.call_later(
|
||||||
self._update_details,
|
self._update_details,
|
||||||
ColorFormatter.red(f"Error extracting details: {str(e)}"),
|
TextFormatter.red(f"Error extracting details: {str(e)}"),
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,57 +18,57 @@ class MediaExtractor:
|
|||||||
# Define sources for each data type
|
# Define sources for each data type
|
||||||
self._sources = {
|
self._sources = {
|
||||||
'title': [
|
'title': [
|
||||||
('metadata', lambda: self.metadata_extractor.extract_title()),
|
('Metadata', lambda: self.metadata_extractor.extract_title()),
|
||||||
('filename', lambda: self.filename_extractor.extract_title())
|
('Filename', lambda: self.filename_extractor.extract_title())
|
||||||
],
|
],
|
||||||
'year': [
|
'year': [
|
||||||
('filename', lambda: self.filename_extractor.extract_year())
|
('Filename', lambda: self.filename_extractor.extract_year())
|
||||||
],
|
],
|
||||||
'source': [
|
'source': [
|
||||||
('filename', lambda: self.filename_extractor.extract_source())
|
('Filename', lambda: self.filename_extractor.extract_source())
|
||||||
],
|
],
|
||||||
'frame_class': [
|
'frame_class': [
|
||||||
('mediainfo', lambda: self.mediainfo_extractor.extract_frame_class()),
|
('MediaInfo', lambda: self.mediainfo_extractor.extract_frame_class()),
|
||||||
('filename', lambda: self.filename_extractor.extract_frame_class())
|
('Filename', lambda: self.filename_extractor.extract_frame_class())
|
||||||
],
|
],
|
||||||
'resolution': [
|
'resolution': [
|
||||||
('mediainfo', lambda: self.mediainfo_extractor.extract_resolution())
|
('MediaInfo', lambda: self.mediainfo_extractor.extract_resolution())
|
||||||
],
|
],
|
||||||
'aspect_ratio': [
|
'aspect_ratio': [
|
||||||
('mediainfo', lambda: self.mediainfo_extractor.extract_aspect_ratio())
|
('MediaInfo', lambda: self.mediainfo_extractor.extract_aspect_ratio())
|
||||||
],
|
],
|
||||||
'hdr': [
|
'hdr': [
|
||||||
('mediainfo', lambda: self.mediainfo_extractor.extract_hdr())
|
('MediaInfo', lambda: self.mediainfo_extractor.extract_hdr())
|
||||||
],
|
],
|
||||||
'audio_langs': [
|
'audio_langs': [
|
||||||
('mediainfo', lambda: self.mediainfo_extractor.extract_audio_langs())
|
('MediaInfo', lambda: self.mediainfo_extractor.extract_audio_langs())
|
||||||
],
|
],
|
||||||
'metadata': [
|
'metadata': [
|
||||||
('metadata', lambda: self.metadata_extractor.extract_all_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())
|
||||||
],
|
],
|
||||||
'meta_description': [
|
'meta_description': [
|
||||||
('metadata', lambda: self.metadata_extractor.extract_meta_description())
|
('Metadata', lambda: self.metadata_extractor.extract_meta_description())
|
||||||
],
|
],
|
||||||
'file_size': [
|
'file_size': [
|
||||||
('fileinfo', lambda: self.fileinfo_extractor.extract_size())
|
('FileInfo', lambda: self.fileinfo_extractor.extract_size())
|
||||||
],
|
],
|
||||||
'modification_time': [
|
'modification_time': [
|
||||||
('fileinfo', lambda: self.fileinfo_extractor.extract_modification_time())
|
('FileInfo', lambda: self.fileinfo_extractor.extract_modification_time())
|
||||||
],
|
],
|
||||||
'file_name': [
|
'file_name': [
|
||||||
('fileinfo', lambda: self.fileinfo_extractor.extract_file_name())
|
('FileInfo', lambda: self.fileinfo_extractor.extract_file_name())
|
||||||
],
|
],
|
||||||
'file_path': [
|
'file_path': [
|
||||||
('fileinfo', lambda: self.fileinfo_extractor.extract_file_path())
|
('FileInfo', lambda: self.fileinfo_extractor.extract_file_path())
|
||||||
],
|
],
|
||||||
'extension': [
|
'extension': [
|
||||||
('fileinfo', lambda: self.fileinfo_extractor.extract_extension())
|
('FileInfo', lambda: self.fileinfo_extractor.extract_extension())
|
||||||
],
|
],
|
||||||
'tracks': [
|
'tracks': [
|
||||||
('mediainfo', lambda: self.mediainfo_extractor.extract_tracks())
|
('MediaInfo', lambda: self.mediainfo_extractor.extract_tracks())
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class MediaExtractor:
|
|||||||
'hdr': lambda x: x is not None,
|
'hdr': lambda x: x is not None,
|
||||||
'audio_langs': lambda x: x is not None,
|
'audio_langs': lambda x: x is not None,
|
||||||
'metadata': lambda x: x is not None,
|
'metadata': lambda x: x is not None,
|
||||||
'tracks': lambda x: x != ""
|
'tracks': lambda x: x is not None and any(x.get(k, []) for k in ['video_tracks', 'audio_tracks', 'subtitle_tracks'])
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, key: str, source: str | None = None):
|
def get(self, key: str, source: str | None = None):
|
||||||
@@ -95,10 +95,10 @@ class MediaExtractor:
|
|||||||
|
|
||||||
if source:
|
if source:
|
||||||
for src, func in self._sources[key]:
|
for src, func in self._sources[key]:
|
||||||
if src == source:
|
if src.lower() == source.lower():
|
||||||
val = func()
|
val = func()
|
||||||
return val if condition(val) else None
|
return val if condition(val) else None
|
||||||
raise ValueError(f"No such source '{source}' for key '{key}'")
|
return None # Source not found for this key, return None
|
||||||
else:
|
else:
|
||||||
# Use fallback: return first valid value
|
# Use fallback: return first valid value
|
||||||
for src, func in self._sources[key]:
|
for src, func in self._sources[key]:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
from pymediainfo import MediaInfo
|
from pymediainfo import MediaInfo
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from ..constants import FRAME_CLASSES
|
from ..constants import FRAME_CLASSES
|
||||||
from ..formatters.color_formatter import ColorFormatter
|
import langcodes
|
||||||
|
|
||||||
|
|
||||||
class MediaInfoExtractor:
|
class MediaInfoExtractor:
|
||||||
@@ -37,14 +37,14 @@ class MediaInfoExtractor:
|
|||||||
return self._get_frame_class_from_height(height)
|
return self._get_frame_class_from_height(height)
|
||||||
return 'Unclassified'
|
return 'Unclassified'
|
||||||
|
|
||||||
def extract_resolution(self) -> str | None:
|
def extract_resolution(self) -> tuple[int, int] | None:
|
||||||
"""Extract actual video resolution (WIDTHxHEIGHT) from media info"""
|
"""Extract actual video resolution as (width, height) tuple from media info"""
|
||||||
if not self.video_tracks:
|
if not self.video_tracks:
|
||||||
return None
|
return None
|
||||||
width = getattr(self.video_tracks[0], 'width', None)
|
width = getattr(self.video_tracks[0], 'width', None)
|
||||||
height = getattr(self.video_tracks[0], 'height', None)
|
height = getattr(self.video_tracks[0], 'height', None)
|
||||||
if width and height:
|
if width and height:
|
||||||
return f"{width}x{height}"
|
return width, height
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def extract_aspect_ratio(self) -> str | None:
|
def extract_aspect_ratio(self) -> str | None:
|
||||||
@@ -69,72 +69,65 @@ class MediaInfoExtractor:
|
|||||||
"""Extract audio languages from media info"""
|
"""Extract audio languages from media info"""
|
||||||
if not self.audio_tracks:
|
if not self.audio_tracks:
|
||||||
return ''
|
return ''
|
||||||
lang_map = {
|
langs = []
|
||||||
'en': 'eng', 'fr': 'fre', 'de': 'ger', 'uk': 'ukr', 'ru': 'rus',
|
for a in self.audio_tracks:
|
||||||
'es': 'spa', 'it': 'ita', 'pt': 'por', 'ja': 'jpn', 'ko': 'kor',
|
lang_code = getattr(a, 'language', 'und').lower()
|
||||||
'zh': 'chi', 'und': 'und'
|
try:
|
||||||
}
|
# Try to get the 3-letter code
|
||||||
langs = [getattr(a, 'language', 'und').lower()[:3] for a in self.audio_tracks]
|
lang_obj = langcodes.Language.get(lang_code)
|
||||||
langs = [lang_map.get(lang, lang) for lang in langs]
|
alpha3 = lang_obj.to_alpha3()
|
||||||
|
langs.append(alpha3)
|
||||||
|
except:
|
||||||
|
# If conversion fails, use the original code
|
||||||
|
langs.append(lang_code[:3])
|
||||||
|
|
||||||
lang_counts = Counter(langs)
|
lang_counts = Counter(langs)
|
||||||
audio_langs = [f"{count}{lang}" if count > 1 else lang for lang, count in lang_counts.items()]
|
audio_langs = [f"{count}{lang}" if count > 1 else lang for lang, count in lang_counts.items()]
|
||||||
return ','.join(audio_langs)
|
return ','.join(audio_langs)
|
||||||
|
|
||||||
def extract_video_dimensions(self) -> tuple[int, int] | None:
|
def extract_video_tracks(self) -> list[dict]:
|
||||||
"""Extract video width and height"""
|
"""Extract video track data"""
|
||||||
if not self.video_tracks:
|
tracks = []
|
||||||
return None
|
for v in self.video_tracks[:2]: # Up to 2 videos
|
||||||
width = getattr(self.video_tracks[0], 'width', None)
|
track_data = {
|
||||||
height = getattr(self.video_tracks[0], 'height', None)
|
'codec': getattr(v, 'format', None) or getattr(v, 'codec', None) or 'unknown',
|
||||||
if width and height:
|
'width': getattr(v, 'width', None),
|
||||||
return width, height
|
'height': getattr(v, 'height', None),
|
||||||
return None
|
'bitrate': getattr(v, 'bit_rate', None),
|
||||||
|
'fps': getattr(v, 'frame_rate', None),
|
||||||
|
'profile': getattr(v, 'format_profile', None),
|
||||||
|
}
|
||||||
|
tracks.append(track_data)
|
||||||
|
return tracks
|
||||||
|
|
||||||
def extract_tracks(self) -> str:
|
def extract_audio_tracks(self) -> list[dict]:
|
||||||
"""Extract compact media track information"""
|
"""Extract audio track data"""
|
||||||
tracks_info = []
|
tracks = []
|
||||||
try:
|
for a in self.audio_tracks[:3]: # Up to 3 audios
|
||||||
# Video tracks
|
track_data = {
|
||||||
for i, v in enumerate(self.video_tracks[:2]): # Up to 2 videos
|
'codec': getattr(a, 'format', None) or getattr(a, 'codec', None) or 'unknown',
|
||||||
codec = getattr(v, 'format', None) or getattr(v, 'codec', None) or 'unknown'
|
'channels': getattr(a, 'channel_s', None),
|
||||||
width = getattr(v, 'width', None) or '?'
|
'language': getattr(a, 'language', None) or 'und',
|
||||||
height = getattr(v, 'height', None) or '?'
|
'bitrate': getattr(a, 'bit_rate', None),
|
||||||
bitrate = getattr(v, 'bit_rate', None)
|
}
|
||||||
fps = getattr(v, 'frame_rate', None)
|
tracks.append(track_data)
|
||||||
profile = getattr(v, 'format_profile', None)
|
return tracks
|
||||||
|
|
||||||
video_str = f"{codec} {width}x{height}"
|
def extract_subtitle_tracks(self) -> list[dict]:
|
||||||
if bitrate:
|
"""Extract subtitle track data"""
|
||||||
video_str += f" {bitrate}bps"
|
tracks = []
|
||||||
if fps:
|
for s in self.sub_tracks[:3]: # Up to 3 subs
|
||||||
video_str += f" {fps}fps"
|
track_data = {
|
||||||
if profile:
|
'language': getattr(s, 'language', None) or 'und',
|
||||||
video_str += f" ({profile})"
|
'format': getattr(s, 'format', None) or getattr(s, 'codec', None) or 'unknown',
|
||||||
|
}
|
||||||
|
tracks.append(track_data)
|
||||||
|
return tracks
|
||||||
|
|
||||||
tracks_info.append(ColorFormatter.green(f"Video {i+1}: {video_str}"))
|
def extract_tracks(self) -> dict:
|
||||||
|
"""Extract media track information as data"""
|
||||||
# Audio tracks
|
return {
|
||||||
for i, a in enumerate(self.audio_tracks[:3]): # Up to 3 audios
|
'video_tracks': self.extract_video_tracks(),
|
||||||
codec = getattr(a, 'format', None) or getattr(a, 'codec', None) or 'unknown'
|
'audio_tracks': self.extract_audio_tracks(),
|
||||||
channels = getattr(a, 'channel_s', None) or '?'
|
'subtitle_tracks': self.extract_subtitle_tracks(),
|
||||||
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 ""
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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]"
|
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ..constants import MEDIA_TYPES
|
from ..constants import MEDIA_TYPES
|
||||||
from .color_formatter import ColorFormatter
|
from .text_formatter import TextFormatter
|
||||||
|
|
||||||
|
|
||||||
class ExtensionFormatter:
|
class ExtensionFormatter:
|
||||||
"""Class for formatting extension information"""
|
"""Class for formatting extension information"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_extension_match(ext_name: str, meta_type: str) -> bool:
|
def format_extension_info(ext_name: str) -> str:
|
||||||
"""Check if file extension matches detected type"""
|
"""Format extension information"""
|
||||||
if ext_name in MEDIA_TYPES and MEDIA_TYPES[ext_name]['meta_type'] == meta_type:
|
if ext_name in MEDIA_TYPES:
|
||||||
return True
|
ext_desc = MEDIA_TYPES[ext_name]['description']
|
||||||
return False
|
return f"{ext_name} - {TextFormatter.grey(ext_desc)}"
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
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"{ColorFormatter.bold_green('Extension:')} {ext_name} - {ColorFormatter.grey(ext_desc)}"
|
|
||||||
else:
|
else:
|
||||||
return (f"{ColorFormatter.bold_yellow('Extension:')} {ext_name} - {ColorFormatter.grey(ext_desc)}\n"
|
return f"{ext_name} - {TextFormatter.grey('Unknown extension')}"
|
||||||
f"{ColorFormatter.bold_red('Meta extension:')} {meta_type} - {ColorFormatter.grey(meta_desc)}\n"
|
|
||||||
f"{ColorFormatter.bold_red('Warning: Extensions do not match!')}")
|
|
||||||
7
renamer/formatters/helper_formatter.py
Normal file
7
renamer/formatters/helper_formatter.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
class HelperFormatter:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def escape_underscores(text: str) -> str:
|
||||||
|
"""Escape underscores in a string by prefixing them with a backslash"""
|
||||||
|
return text.replace("_", r"\_")
|
||||||
@@ -3,113 +3,220 @@ from .size_formatter import SizeFormatter
|
|||||||
from .date_formatter import DateFormatter
|
from .date_formatter import DateFormatter
|
||||||
from .extension_extractor import ExtensionExtractor
|
from .extension_extractor import ExtensionExtractor
|
||||||
from .extension_formatter import ExtensionFormatter
|
from .extension_formatter import ExtensionFormatter
|
||||||
from .color_formatter import ColorFormatter
|
from .text_formatter import TextFormatter
|
||||||
|
from .track_formatter import TrackFormatter
|
||||||
|
from .resolution_formatter import ResolutionFormatter
|
||||||
|
|
||||||
|
|
||||||
class MediaFormatter:
|
class MediaFormatter:
|
||||||
"""Class to format media data for display"""
|
"""Class to format media data for display"""
|
||||||
|
|
||||||
def format_file_info_panel(self, extractor) -> str:
|
def __init__(self, extractor):
|
||||||
"""Format file information for the file info panel"""
|
self.extractor = extractor
|
||||||
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
|
def _format_data_item(self, item: dict) -> str:
|
||||||
ext_name = ExtensionExtractor.get_extension_name(
|
"""Apply all formatting to a data item and return the formatted string"""
|
||||||
Path(extractor.get("file_path"))
|
# Define text formatters that should be applied before markup
|
||||||
)
|
text_formatters_set = {
|
||||||
ext_desc = ExtensionExtractor.get_extension_description(ext_name)
|
TextFormatter.uppercase,
|
||||||
meta_type = extractor.get("meta_type")
|
TextFormatter.lowercase,
|
||||||
meta_desc = extractor.get("meta_description")
|
TextFormatter.camelcase,
|
||||||
match = ExtensionFormatter.check_extension_match(ext_name, meta_type)
|
}
|
||||||
ext_info = ExtensionFormatter.format_extension_info(
|
|
||||||
ext_name, ext_desc, meta_type, meta_desc, match
|
|
||||||
)
|
|
||||||
|
|
||||||
output = [ColorFormatter.bold_blue("FILE INFO"), ""]
|
# Handle value formatting first (e.g., size formatting)
|
||||||
output.extend(
|
value = item.get("value")
|
||||||
item["format_func"](f"{item['label']}: {item['value']}") for item in data
|
if value is not None:
|
||||||
)
|
value_formatters = item.get("value_formatters", [])
|
||||||
output.append(ext_info)
|
if not isinstance(value_formatters, list):
|
||||||
|
value_formatters = [value_formatters] if value_formatters else []
|
||||||
|
for formatter in value_formatters:
|
||||||
|
value = formatter(value)
|
||||||
|
|
||||||
|
# Handle label formatting
|
||||||
|
label = item.get("label", "")
|
||||||
|
if label:
|
||||||
|
label_formatters = item.get("label_formatters", [])
|
||||||
|
if not isinstance(label_formatters, list):
|
||||||
|
label_formatters = [label_formatters] if label_formatters else []
|
||||||
|
# Separate text and markup formatters, apply text first
|
||||||
|
text_fs = [f for f in label_formatters if f in text_formatters_set]
|
||||||
|
markup_fs = [f for f in label_formatters if f not in text_formatters_set]
|
||||||
|
ordered_formatters = text_fs + markup_fs
|
||||||
|
for formatter in ordered_formatters:
|
||||||
|
label = formatter(label)
|
||||||
|
|
||||||
|
# Create the display string
|
||||||
|
if value is not None:
|
||||||
|
display_string = f"{label}: {value}"
|
||||||
|
else:
|
||||||
|
display_string = label
|
||||||
|
|
||||||
|
# Handle display formatting (e.g., color)
|
||||||
|
display_formatters = item.get("display_formatters", [])
|
||||||
|
if not isinstance(display_formatters, list):
|
||||||
|
display_formatters = [display_formatters] if display_formatters else []
|
||||||
|
# Separate text and markup formatters, apply text first
|
||||||
|
text_fs = [f for f in display_formatters if f in text_formatters_set]
|
||||||
|
markup_fs = [f for f in display_formatters if f not in text_formatters_set]
|
||||||
|
ordered_formatters = text_fs + markup_fs
|
||||||
|
for formatter in ordered_formatters:
|
||||||
|
display_string = formatter(display_string)
|
||||||
|
|
||||||
|
return display_string
|
||||||
|
|
||||||
|
def file_info_panel(self) -> str:
|
||||||
|
"""Return formatted file info panel string"""
|
||||||
|
|
||||||
|
output = self.file_info()
|
||||||
|
|
||||||
# Add tracks 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("")
|
||||||
output.append(tracks_text)
|
output.extend(self.tracks_info())
|
||||||
|
|
||||||
# Add rename lines
|
# Add filename extracted data
|
||||||
rename_lines = self.format_rename_lines(extractor)
|
|
||||||
output.append("")
|
output.append("")
|
||||||
output.extend(rename_lines)
|
output.extend(self.filename_extracted_data())
|
||||||
|
|
||||||
|
# Add mediainfo extracted data
|
||||||
|
output.append("")
|
||||||
|
output.extend(self.mediainfo_extracted_data())
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
def format_filename_extraction_panel(self, extractor) -> str:
|
def file_info(self) -> list[str]:
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
"group": "File Info",
|
||||||
|
"label": "File Info",
|
||||||
|
"label_formatters": [TextFormatter.bold, TextFormatter.uppercase],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "File Info",
|
||||||
|
"label": "Path",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("file_path"),
|
||||||
|
"display_formatters": [TextFormatter.blue],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "File Info",
|
||||||
|
"label": "Size",
|
||||||
|
"value": self.extractor.get("file_size"),
|
||||||
|
"value_formatters": [SizeFormatter.format_size_full],
|
||||||
|
"display_formatters": [TextFormatter.bold, TextFormatter.green],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "File Info",
|
||||||
|
"label": "Name",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("file_name"),
|
||||||
|
"display_formatters": [TextFormatter.cyan],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "File Info",
|
||||||
|
"label": "Modified",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("modification_time"),
|
||||||
|
"value_formatters": [DateFormatter.format_modification_date],
|
||||||
|
"display_formatters": [TextFormatter.bold, TextFormatter.magenta],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "File Info",
|
||||||
|
"label": "Extension",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("extension"),
|
||||||
|
"value_formatters": [ExtensionFormatter.format_extension_info],
|
||||||
|
"display_formatters": [TextFormatter.green],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return [self._format_data_item(item) for item in data]
|
||||||
|
|
||||||
|
def tracks_info(self) -> list[str]:
|
||||||
|
"""Return formatted tracks information"""
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
"group": "Tracks Info",
|
||||||
|
"label": "Tracks Info",
|
||||||
|
"label_formatters": [TextFormatter.bold, TextFormatter.uppercase],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for item in self.extractor.get("tracks").get("video_tracks"):
|
||||||
|
data.append(
|
||||||
|
{
|
||||||
|
"group": "Tracks Info",
|
||||||
|
"label": "Video Track",
|
||||||
|
"value": item,
|
||||||
|
"value_formatters": TrackFormatter.format_video_track,
|
||||||
|
"display_formatters": [TextFormatter.green],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for i, item in enumerate(
|
||||||
|
self.extractor.get("tracks").get("audio_tracks"), start=1
|
||||||
|
):
|
||||||
|
data.append(
|
||||||
|
{
|
||||||
|
"group": "Tracks Info",
|
||||||
|
"label": f"Audio Track {i}",
|
||||||
|
"value": item,
|
||||||
|
"value_formatters": TrackFormatter.format_audio_track,
|
||||||
|
"display_formatters": [TextFormatter.yellow],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for i, item in enumerate(
|
||||||
|
self.extractor.get("tracks").get("subtitle_tracks"), start=1
|
||||||
|
):
|
||||||
|
data.append(
|
||||||
|
{
|
||||||
|
"group": "Tracks Info",
|
||||||
|
"label": f"Subtitle Track {i}",
|
||||||
|
"value": item,
|
||||||
|
"value_formatters": TrackFormatter.format_subtitle_track,
|
||||||
|
"display_formatters": [TextFormatter.magenta],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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"""
|
"""Format filename extraction data for the filename panel"""
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
"label": "Title",
|
"label": "Title",
|
||||||
"value": extractor.get("title") or "Not found",
|
"value": self.extractor.get("title") or "Not found",
|
||||||
"format_func": ColorFormatter.yellow,
|
"display_formatters": [TextFormatter.yellow],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Year",
|
"label": "Year",
|
||||||
"value": extractor.get("year") or "Not found",
|
"value": self.extractor.get("year") or "Not found",
|
||||||
"format_func": ColorFormatter.yellow,
|
"display_formatters": [TextFormatter.yellow],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Source",
|
"label": "Source",
|
||||||
"value": extractor.get("source") or "Not found",
|
"value": self.extractor.get("source") or "Not found",
|
||||||
"format_func": ColorFormatter.yellow,
|
"display_formatters": [TextFormatter.yellow],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Frame Class",
|
"label": "Frame Class",
|
||||||
"value": extractor.get("frame_class") or "Not found",
|
"value": self.extractor.get("frame_class") or "Not found",
|
||||||
"format_func": ColorFormatter.yellow,
|
"display_formatters": [TextFormatter.yellow],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
output = [ColorFormatter.bold_yellow("FILENAME EXTRACTION"), ""]
|
output = [TextFormatter.bold_yellow("FILENAME EXTRACTION"), ""]
|
||||||
output.extend(
|
for item in data:
|
||||||
item["format_func"](f"{item['label']}: {item['value']}") for item in data
|
output.append(self._format_data_item(item))
|
||||||
)
|
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
def format_metadata_extraction_panel(self, extractor) -> str:
|
def format_metadata_extraction_panel(self) -> str:
|
||||||
"""Format metadata extraction data for the metadata panel"""
|
"""Format metadata extraction data for the metadata panel"""
|
||||||
metadata = extractor.get("metadata") or {}
|
metadata = self.extractor.get("metadata") or {}
|
||||||
data = []
|
data = []
|
||||||
if metadata.get("duration"):
|
if metadata.get("duration"):
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"label": "Duration",
|
"label": "Duration",
|
||||||
"value": f"{metadata['duration']:.1f} seconds",
|
"value": f"{metadata['duration']:.1f} seconds",
|
||||||
"format_func": ColorFormatter.cyan,
|
"display_formatters": [TextFormatter.cyan],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if metadata.get("title"):
|
if metadata.get("title"):
|
||||||
@@ -117,7 +224,7 @@ class MediaFormatter:
|
|||||||
{
|
{
|
||||||
"label": "Title",
|
"label": "Title",
|
||||||
"value": metadata["title"],
|
"value": metadata["title"],
|
||||||
"format_func": ColorFormatter.cyan,
|
"display_formatters": [TextFormatter.cyan],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if metadata.get("artist"):
|
if metadata.get("artist"):
|
||||||
@@ -125,67 +232,119 @@ class MediaFormatter:
|
|||||||
{
|
{
|
||||||
"label": "Artist",
|
"label": "Artist",
|
||||||
"value": metadata["artist"],
|
"value": metadata["artist"],
|
||||||
"format_func": ColorFormatter.cyan,
|
"display_formatters": [TextFormatter.cyan],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
output = [ColorFormatter.bold_cyan("METADATA EXTRACTION"), ""]
|
output = [TextFormatter.bold_cyan("METADATA EXTRACTION"), ""]
|
||||||
if data:
|
if data:
|
||||||
output.extend(
|
for item in data:
|
||||||
item["format_func"](f"{item['label']}: {item['value']}")
|
output.append(self._format_data_item(item))
|
||||||
for item in data
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
output.append(ColorFormatter.dim("No metadata found"))
|
output.append(TextFormatter.dim("No metadata found"))
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
def format_mediainfo_extraction_panel(self, extractor) -> 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"""
|
||||||
data = [
|
data = [
|
||||||
|
{
|
||||||
|
"label": "Media Info Extraction",
|
||||||
|
"label_formatters": [TextFormatter.bold, TextFormatter.uppercase],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Frame Class",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("frame_class", "MediaInfo")
|
||||||
|
or "Not extracted",
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Resolution",
|
"label": "Resolution",
|
||||||
"value": extractor.get("resolution") or "Not found",
|
"label_formatters": [TextFormatter.bold],
|
||||||
"format_func": ColorFormatter.green,
|
"value": self.extractor.get("resolution", "MediaInfo")
|
||||||
|
or "Not extracted",
|
||||||
|
"value_formatters": [ResolutionFormatter.format_resolution_dimensions],
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Aspect Ratio",
|
"label": "Aspect Ratio",
|
||||||
"value": extractor.get("aspect_ratio") or "Not found",
|
"label_formatters": [TextFormatter.bold],
|
||||||
"format_func": ColorFormatter.green,
|
"value": self.extractor.get("aspect_ratio", "MediaInfo")
|
||||||
|
or "Not extracted",
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "HDR",
|
"label": "HDR",
|
||||||
"value": extractor.get("hdr") or "Not found",
|
"label_formatters": [TextFormatter.bold],
|
||||||
"format_func": ColorFormatter.green,
|
"value": self.extractor.get("hdr", "MediaInfo") or "Not extracted",
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Audio Languages",
|
"label": "Audio Languages",
|
||||||
"value": extractor.get("audio_langs") or "Not found",
|
"label_formatters": [TextFormatter.bold],
|
||||||
"format_func": ColorFormatter.green,
|
"value": self.extractor.get("audio_langs", "MediaInfo")
|
||||||
|
or "Not extracted",
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return [self._format_data_item(item) for item in data]
|
||||||
|
|
||||||
|
def filename_extracted_data(self) -> list[str]:
|
||||||
|
"""Return formatted filename extracted data"""
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
"label": "Filename Extracted Data",
|
||||||
|
"label_formatters": [TextFormatter.bold, TextFormatter.uppercase],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Movie title",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("title", "Filename"),
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Year",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("year", "Filename"),
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Video source",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("source", "Filename") or "Not extracted",
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Frame class",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("frame_class", "Filename")
|
||||||
|
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],
|
||||||
|
"value": self.extractor.get("hdr", "Filename") or "Not extracted",
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Audio langs",
|
||||||
|
"label_formatters": [TextFormatter.bold],
|
||||||
|
"value": self.extractor.get("audio_langs", "Filename")
|
||||||
|
or "Not extracted",
|
||||||
|
"display_formatters": [TextFormatter.grey],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
output = [ColorFormatter.bold_green("MEDIA INFO EXTRACTION"), ""]
|
return [self._format_data_item(item) for item in data]
|
||||||
output.extend(
|
|
||||||
item["format_func"](f"{item['label']}: {item['value']}") for item in data
|
|
||||||
)
|
|
||||||
|
|
||||||
return "\n".join(output)
|
|
||||||
|
|
||||||
def format_rename_lines(self, extractor) -> list[str]:
|
|
||||||
"""Format the rename information 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:
|
def _format_extra_metadata(self, metadata: dict) -> str:
|
||||||
"""Format extra metadata like duration, title, artist"""
|
"""Format extra metadata like duration, title, artist"""
|
||||||
@@ -198,5 +357,5 @@ class MediaFormatter:
|
|||||||
data["Artist"] = metadata["artist"]
|
data["Artist"] = metadata["artist"]
|
||||||
|
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
ColorFormatter.cyan(f"{key}: {value}") for key, value in data.items()
|
TextFormatter.cyan(f"{key}: {value}") for key, value in data.items()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .color_formatter import ColorFormatter
|
from .text_formatter import TextFormatter
|
||||||
from .date_formatter import DateFormatter
|
from .date_formatter import DateFormatter
|
||||||
|
|
||||||
|
|
||||||
@@ -6,11 +6,11 @@ class ProposedNameFormatter:
|
|||||||
"""Class for formatting proposed filenames"""
|
"""Class for formatting proposed filenames"""
|
||||||
|
|
||||||
def __init__(self, extractor):
|
def __init__(self, extractor):
|
||||||
self.extractor = extractor
|
"""Initialize with media extractor data"""
|
||||||
|
|
||||||
self.__title = extractor.get("title") or "Unknown Title"
|
self.__title = extractor.get("title") or "Unknown Title"
|
||||||
self.__year = DateFormatter.format_year(extractor.get("year"))
|
self.__year = DateFormatter.format_year(extractor.get("year"))
|
||||||
self.__source = extractor.get("source") or None
|
self.__source = f" {extractor.get('source')}" if extractor.get("source") else ""
|
||||||
self.__frame_class = extractor.get("frame_class") or None
|
self.__frame_class = extractor.get("frame_class") or None
|
||||||
self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else ""
|
self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else ""
|
||||||
self.__audio_langs = extractor.get("audio_langs") or None
|
self.__audio_langs = extractor.get("audio_langs") or None
|
||||||
@@ -18,8 +18,11 @@ class ProposedNameFormatter:
|
|||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Convert the proposed name to string"""
|
"""Convert the proposed name to string"""
|
||||||
|
return self.rename_line()
|
||||||
|
|
||||||
|
def rename_line(self) -> str:
|
||||||
return f"{self.__title} {self.__year}{self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}].{self.__extension}"
|
return f"{self.__title} {self.__year}{self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}].{self.__extension}"
|
||||||
|
|
||||||
def format_display_string(self) -> str:
|
def rename_line_formatted(self) -> str:
|
||||||
"""Format the proposed name for display with color"""
|
"""Format the proposed name for display with color"""
|
||||||
return ColorFormatter.bold_yellow(str(self))
|
return f">> {TextFormatter.bold_yellow(str(self))} <<"
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class ResolutionFormatter:
|
|||||||
return f'{height}p'
|
return f'{height}p'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_resolution_dimensions(width: int, height: int) -> str:
|
def format_resolution_dimensions(resolution: tuple[int, int]) -> str:
|
||||||
"""Format resolution as WIDTHxHEIGHT"""
|
"""Format resolution as WIDTHxHEIGHT"""
|
||||||
|
width, height = resolution
|
||||||
return f"{width}x{height}"
|
return f"{width}x{height}"
|
||||||
103
renamer/formatters/text_formatter.py
Normal file
103
renamer/formatters/text_formatter.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
class TextFormatter:
|
||||||
|
"""Class for formatting text with colors and styles using Textual markup"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bold(text: str) -> str:
|
||||||
|
return f"[bold]{text}[/bold]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def italic(text: str) -> str:
|
||||||
|
return f"[italic]{text}[/italic]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def underline(text: str) -> str:
|
||||||
|
return f"[underline]{text}[/underline]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def uppercase(text: str) -> str:
|
||||||
|
return text.upper()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def lowercase(text: str) -> str:
|
||||||
|
return text.lower()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def camelcase(text: str) -> str:
|
||||||
|
"""Convert text to CamelCase (first letter of each word capitalized)"""
|
||||||
|
return ''.join(word.capitalize() for word in text.split())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bold_green(text: str) -> str:
|
||||||
|
"""Deprecated: Use [TextFormatter.bold, TextFormatter.green] instead"""
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"TextFormatter.bold_green is deprecated. Use [TextFormatter.bold, TextFormatter.green] instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2
|
||||||
|
)
|
||||||
|
return f"[bold green]{text}[/bold green]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bold_cyan(text: str) -> str:
|
||||||
|
"""Deprecated: Use [TextFormatter.bold, TextFormatter.cyan] instead"""
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"TextFormatter.bold_cyan is deprecated. Use [TextFormatter.bold, TextFormatter.cyan] instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2
|
||||||
|
)
|
||||||
|
return f"[bold cyan]{text}[/bold cyan]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bold_magenta(text: str) -> str:
|
||||||
|
"""Deprecated: Use [TextFormatter.bold, TextFormatter.magenta] instead"""
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"TextFormatter.bold_magenta is deprecated. Use [TextFormatter.bold, TextFormatter.magenta] instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2
|
||||||
|
)
|
||||||
|
return f"[bold magenta]{text}[/bold magenta]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bold_yellow(text: str) -> str:
|
||||||
|
"""Deprecated: Use [TextFormatter.bold, TextFormatter.yellow] instead"""
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
"TextFormatter.bold_yellow is deprecated. Use [TextFormatter.bold, TextFormatter.yellow] instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2
|
||||||
|
)
|
||||||
|
return f"[bold yellow]{text}[/bold yellow]"
|
||||||
|
|
||||||
|
@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 blue(text: str) -> str:
|
||||||
|
return f"[blue]{text}[/blue]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def grey(text: str) -> str:
|
||||||
|
return f"[grey]{text}[/grey]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dim(text: str) -> str:
|
||||||
|
return f"[dim]{text}[/dim]"
|
||||||
44
renamer/formatters/track_formatter.py
Normal file
44
renamer/formatters/track_formatter.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
class TrackFormatter:
|
||||||
|
"""Class to format track information into display strings"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_video_track(track: dict) -> str:
|
||||||
|
"""Format a video track dict into a display string"""
|
||||||
|
codec = track.get('codec', 'unknown')
|
||||||
|
width = track.get('width', '?')
|
||||||
|
height = track.get('height', '?')
|
||||||
|
bitrate = track.get('bitrate')
|
||||||
|
fps = track.get('fps')
|
||||||
|
profile = track.get('profile')
|
||||||
|
|
||||||
|
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})"
|
||||||
|
|
||||||
|
return video_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_audio_track(track: dict) -> str:
|
||||||
|
"""Format an audio track dict into a display string"""
|
||||||
|
codec = track.get('codec', 'unknown')
|
||||||
|
channels = track.get('channels', '?')
|
||||||
|
lang = track.get('language', 'und')
|
||||||
|
bitrate = track.get('bitrate')
|
||||||
|
|
||||||
|
audio_str = f"{codec} {channels}ch {lang}"
|
||||||
|
if bitrate:
|
||||||
|
audio_str += f" {bitrate}bps"
|
||||||
|
|
||||||
|
return audio_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_subtitle_track(track: dict) -> str:
|
||||||
|
"""Format a subtitle track dict into a display string"""
|
||||||
|
lang = track.get('language', 'und')
|
||||||
|
format = track.get('format', 'unknown')
|
||||||
|
|
||||||
|
return f"{lang} ({format})"
|
||||||
@@ -30,9 +30,3 @@ class TestMediaInfoExtractor:
|
|||||||
langs = extractor.extract_audio_langs(test_file)
|
langs = extractor.extract_audio_langs(test_file)
|
||||||
# Text files don't have audio tracks
|
# Text files don't have audio tracks
|
||||||
assert langs == ''
|
assert langs == ''
|
||||||
|
|
||||||
def test_extract_video_dimensions(self, extractor, test_file):
|
|
||||||
"""Test extracting video dimensions"""
|
|
||||||
dims = extractor.extract_video_dimensions(test_file)
|
|
||||||
# Text files don't have video dimensions
|
|
||||||
assert dims is None
|
|
||||||
11
uv.lock
generated
11
uv.lock
generated
@@ -20,6 +20,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langcodes"
|
||||||
|
version = "3.5.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/75/f9edc5d72945019312f359e69ded9f82392a81d49c5051ed3209b100c0d2/langcodes-3.5.1.tar.gz", hash = "sha256:40bff315e01b01d11c2ae3928dd4f5cbd74dd38f9bd912c12b9a3606c143f731", size = 191084, upload-time = "2025-12-02T16:22:01.627Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/c1/d10b371bcba7abce05e2b33910e39c33cfa496a53f13640b7b8e10bb4d2b/langcodes-3.5.1-py3-none-any.whl", hash = "sha256:b6a9c25c603804e2d169165091d0cdb23934610524a21d226e4f463e8e958a72", size = 183050, upload-time = "2025-12-02T16:21:59.954Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linkify-it-py"
|
name = "linkify-it-py"
|
||||||
version = "2.0.3"
|
version = "2.0.3"
|
||||||
@@ -158,6 +167,7 @@ name = "renamer"
|
|||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "langcodes" },
|
||||||
{ name = "mutagen" },
|
{ name = "mutagen" },
|
||||||
{ name = "pymediainfo" },
|
{ name = "pymediainfo" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -167,6 +177,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "langcodes", specifier = ">=3.5.1" },
|
||||||
{ name = "mutagen", specifier = ">=1.47.0" },
|
{ name = "mutagen", specifier = ">=1.47.0" },
|
||||||
{ name = "pymediainfo", specifier = ">=6.0.0" },
|
{ name = "pymediainfo", specifier = ">=6.0.0" },
|
||||||
{ name = "pytest", specifier = ">=7.0.0" },
|
{ name = "pytest", specifier = ">=7.0.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user