diff --git a/dist/renamer-0.6.4-py3-none-any.whl b/dist/renamer-0.6.4-py3-none-any.whl new file mode 100644 index 0000000..62b3ff7 Binary files /dev/null and b/dist/renamer-0.6.4-py3-none-any.whl differ diff --git a/dist/renamer-0.6.5-py3-none-any.whl b/dist/renamer-0.6.5-py3-none-any.whl new file mode 100644 index 0000000..dc7f9f3 Binary files /dev/null and b/dist/renamer-0.6.5-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index 4ceb103..77d0d6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "renamer" -version = "0.6.3" +version = "0.6.5" description = "Terminal-based media file renamer and metadata viewer" readme = "README.md" requires-python = ">=3.11" diff --git a/renamer/extractors/fileinfo_extractor.py b/renamer/extractors/fileinfo_extractor.py index 13ed3b6..106d48b 100644 --- a/renamer/extractors/fileinfo_extractor.py +++ b/renamer/extractors/fileinfo_extractor.py @@ -45,13 +45,9 @@ class FileInfoExtractor: Args: file_path: Path object pointing to the file to extract info from """ - self.file_path = file_path - self._size = file_path.stat().st_size - self._modification_time = file_path.stat().st_mtime - self._file_name = file_path.name - self._file_path = str(file_path) + self._file_path = file_path + self._stat = file_path.stat() self._cache: dict[str, any] = {} # Internal cache for method results - logging.info(f"FileInfoExtractor: file_name={self._file_name!r}, file_path={self._file_path!r}") @cached_method() def extract_size(self) -> int: @@ -60,7 +56,7 @@ class FileInfoExtractor: Returns: File size in bytes as an integer """ - return self._size + return self._stat.st_size @cached_method() def extract_modification_time(self) -> float: @@ -69,7 +65,7 @@ class FileInfoExtractor: Returns: Unix timestamp (seconds since epoch) as a float """ - return self._modification_time + return self._stat.st_mtime @cached_method() def extract_file_name(self) -> str: @@ -78,7 +74,7 @@ class FileInfoExtractor: Returns: File name including extension (e.g., "movie.mkv") """ - return self._file_name + return self._file_path.name @cached_method() def extract_file_path(self) -> str: @@ -87,7 +83,7 @@ class FileInfoExtractor: Returns: Absolute file path as a string """ - return self._file_path + return str(self._file_path) @cached_method() def extract_extension(self) -> str: @@ -96,4 +92,4 @@ class FileInfoExtractor: Returns: File extension in lowercase without leading dot (e.g., "mkv", "mp4") """ - return self.file_path.suffix.lower().lstrip('.') \ No newline at end of file + return self._file_path.suffix.lower().lstrip('.') \ No newline at end of file diff --git a/renamer/formatters/__init__.py b/renamer/formatters/__init__.py index 48764b4..9400201 100644 --- a/renamer/formatters/__init__.py +++ b/renamer/formatters/__init__.py @@ -28,6 +28,11 @@ from .date_decorators import date_decorators, DateDecorators from .special_info_decorators import special_info_decorators, SpecialInfoDecorators from .text_decorators import text_decorators, TextDecorators from .conditional_decorators import conditional_decorators, ConditionalDecorators +from .size_decorators import size_decorators, SizeDecorators +from .extension_decorators import extension_decorators, ExtensionDecorators +from .duration_decorators import duration_decorators, DurationDecorators +from .resolution_decorators import resolution_decorators, ResolutionDecorators +from .track_decorators import track_decorators, TrackDecorators __all__ = [ # Base classes @@ -57,4 +62,14 @@ __all__ = [ 'TextDecorators', 'conditional_decorators', 'ConditionalDecorators', + 'size_decorators', + 'SizeDecorators', + 'extension_decorators', + 'ExtensionDecorators', + 'duration_decorators', + 'DurationDecorators', + 'resolution_decorators', + 'ResolutionDecorators', + 'track_decorators', + 'TrackDecorators', ] \ No newline at end of file diff --git a/renamer/formatters/conditional_decorators.py b/renamer/formatters/conditional_decorators.py index 2d6d3cc..1044a68 100644 --- a/renamer/formatters/conditional_decorators.py +++ b/renamer/formatters/conditional_decorators.py @@ -19,6 +19,7 @@ class ConditionalDecorators: """Decorator to wrap value with delimiters if it exists. Can be used for prefix-only (right=""), suffix-only (left=""), or both. + Supports format string placeholders that will be filled from function arguments. Usage: @conditional_decorators.wrap("[", "]") @@ -34,12 +35,40 @@ class ConditionalDecorators: @conditional_decorators.wrap("", ",") def get_hdr(self): return self.extractor.get('hdr') + + # With placeholders + @conditional_decorators.wrap("Track {index}: ") + def get_track(self, data, index): + return data """ def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) - return f"{left}{result}{right}" if result else "" + if not result: + return "" + + # Extract format arguments from function signature + # Skip 'self' (args[0]) and the main data argument + format_kwargs = {} + if len(args) > 2: # self, data, index, ... + # Try to detect named parameters from function signature + import inspect + sig = inspect.signature(func) + param_names = list(sig.parameters.keys()) + # Skip first two params (self, data/track/value) + for i, param_name in enumerate(param_names[2:], start=2): + if i < len(args): + format_kwargs[param_name] = args[i] + + # Also add explicit kwargs + format_kwargs.update(kwargs) + + # Format left and right with available arguments + formatted_left = left.format(**format_kwargs) if format_kwargs else left + formatted_right = right.format(**format_kwargs) if format_kwargs else right + + return f"{formatted_left}{result}{formatted_right}" return wrapper return decorator diff --git a/renamer/formatters/duration_decorators.py b/renamer/formatters/duration_decorators.py new file mode 100644 index 0000000..6bbc908 --- /dev/null +++ b/renamer/formatters/duration_decorators.py @@ -0,0 +1,42 @@ +"""Duration formatting decorators. + +Provides decorator versions of DurationFormatter methods. +""" + +from functools import wraps +from typing import Callable +from .duration_formatter import DurationFormatter + + +class DurationDecorators: + """Duration formatting decorators.""" + + @staticmethod + def duration_full() -> Callable: + """Decorator to format duration in full format (HH:MM:SS).""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if not result: + return "" + return DurationFormatter.format_full(result) + return wrapper + return decorator + + @staticmethod + def duration_short() -> Callable: + """Decorator to format duration in short format.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if not result: + return "" + return DurationFormatter.format_short(result) + return wrapper + return decorator + + +# Singleton instance +duration_decorators = DurationDecorators() diff --git a/renamer/formatters/extension_decorators.py b/renamer/formatters/extension_decorators.py new file mode 100644 index 0000000..fdc2da7 --- /dev/null +++ b/renamer/formatters/extension_decorators.py @@ -0,0 +1,29 @@ +"""Extension formatting decorators. + +Provides decorator versions of ExtensionFormatter methods. +""" + +from functools import wraps +from typing import Callable +from .extension_formatter import ExtensionFormatter + + +class ExtensionDecorators: + """Extension formatting decorators.""" + + @staticmethod + def extension_info() -> Callable: + """Decorator to format extension information.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if not result: + return "" + return ExtensionFormatter.format_extension_info(result) + return wrapper + return decorator + + +# Singleton instance +extension_decorators = ExtensionDecorators() diff --git a/renamer/formatters/resolution_decorators.py b/renamer/formatters/resolution_decorators.py new file mode 100644 index 0000000..bc717b1 --- /dev/null +++ b/renamer/formatters/resolution_decorators.py @@ -0,0 +1,29 @@ +"""Resolution formatting decorators. + +Provides decorator versions of ResolutionFormatter methods. +""" + +from functools import wraps +from typing import Callable +from .resolution_formatter import ResolutionFormatter + + +class ResolutionDecorators: + """Resolution formatting decorators.""" + + @staticmethod + def resolution_dimensions() -> Callable: + """Decorator to format resolution as dimensions (WxH).""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if not result: + return "" + return ResolutionFormatter.format_resolution_dimensions(result) + return wrapper + return decorator + + +# Singleton instance +resolution_decorators = ResolutionDecorators() diff --git a/renamer/formatters/size_decorators.py b/renamer/formatters/size_decorators.py new file mode 100644 index 0000000..4475bc5 --- /dev/null +++ b/renamer/formatters/size_decorators.py @@ -0,0 +1,42 @@ +"""Size formatting decorators. + +Provides decorator versions of SizeFormatter methods. +""" + +from functools import wraps +from typing import Callable +from .size_formatter import SizeFormatter + + +class SizeDecorators: + """Size formatting decorators.""" + + @staticmethod + def size_full() -> Callable: + """Decorator to format file size in full format.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if result is None: + return "" + return SizeFormatter.format_size_full(result) + return wrapper + return decorator + + @staticmethod + def size_short() -> Callable: + """Decorator to format file size in short format.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if result is None: + return "" + return SizeFormatter.format_size_short(result) + return wrapper + return decorator + + +# Singleton instance +size_decorators = SizeDecorators() diff --git a/renamer/formatters/text_decorators.py b/renamer/formatters/text_decorators.py index ceff529..9106e4a 100644 --- a/renamer/formatters/text_decorators.py +++ b/renamer/formatters/text_decorators.py @@ -22,6 +22,8 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if result == "": + return "" return TextFormatter.bold(str(result)) return wrapper return decorator @@ -33,6 +35,8 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if result == "": + return "" return TextFormatter.italic(str(result)) return wrapper return decorator @@ -44,6 +48,8 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if result == "": + return "" return TextFormatter.green(str(result)) return wrapper return decorator @@ -55,6 +61,8 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if not result: + return "" return TextFormatter.yellow(str(result)) return wrapper return decorator @@ -66,6 +74,8 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if not result: + return "" return TextFormatter.cyan(str(result)) return wrapper return decorator @@ -77,6 +87,8 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if not result: + return "" return TextFormatter.magenta(str(result)) return wrapper return decorator @@ -88,10 +100,12 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if not result: + return "" return TextFormatter.red(str(result)) return wrapper return decorator - + @staticmethod def orange() -> Callable: """Decorator to color text orange.""" @@ -99,10 +113,38 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if not result: + return "" return TextFormatter.orange(str(result)) return wrapper return decorator + @staticmethod + def blue() -> Callable: + """Decorator to color text blue.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + if not result: + return "" + return TextFormatter.blue(str(result)) + return wrapper + return decorator + + @staticmethod + def grey() -> Callable: + """Decorator to color text grey.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + if not result: + return "" + return TextFormatter.grey(str(result)) + return wrapper + return decorator + @staticmethod def uppercase() -> Callable: """Decorator to convert text to uppercase.""" @@ -110,6 +152,8 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if not result: + return "" return TextFormatter.uppercase(str(result)) return wrapper return decorator @@ -121,10 +165,39 @@ class TextDecorators: @wraps(func) def wrapper(*args, **kwargs) -> str: result = func(*args, **kwargs) + if not result: + return "" return TextFormatter.lowercase(str(result)) return wrapper return decorator + @staticmethod + def url() -> Callable: + """Decorator to format text as a clickable URL.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + if not result: + return "" + return TextFormatter.format_url(str(result)) + return wrapper + return decorator + + @staticmethod + def escape() -> Callable: + """Decorator to escape rich markup in text.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + from rich.markup import escape + result = func(*args, **kwargs) + if not result: + return "" + return escape(str(result)) + return wrapper + return decorator + # Singleton instance text_decorators = TextDecorators() diff --git a/renamer/formatters/track_decorators.py b/renamer/formatters/track_decorators.py new file mode 100644 index 0000000..9fdc1d1 --- /dev/null +++ b/renamer/formatters/track_decorators.py @@ -0,0 +1,55 @@ +"""Track formatting decorators. + +Provides decorator versions of TrackFormatter methods. +""" + +from functools import wraps +from typing import Callable +from .track_formatter import TrackFormatter + + +class TrackDecorators: + """Track formatting decorators.""" + + @staticmethod + def video_track() -> Callable: + """Decorator to format video track data.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if not result: + return "" + return TrackFormatter.format_video_track(result) + return wrapper + return decorator + + @staticmethod + def audio_track() -> Callable: + """Decorator to format audio track data.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if not result: + return "" + return TrackFormatter.format_audio_track(result) + return wrapper + return decorator + + @staticmethod + def subtitle_track() -> Callable: + """Decorator to format subtitle track data.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if not result: + return "" + return TrackFormatter.format_subtitle_track(result) + return wrapper + return decorator + + +# Singleton instance +track_decorators = TrackDecorators() diff --git a/renamer/views/media_panel.py b/renamer/views/media_panel.py index e3ccc30..426e094 100644 --- a/renamer/views/media_panel.py +++ b/renamer/views/media_panel.py @@ -1,14 +1,5 @@ -from pathlib import Path -from rich.markup import escape -from ..formatters.size_formatter import SizeFormatter -from ..formatters.date_formatter import DateFormatter -from ..formatters.extension_formatter import ExtensionFormatter -from ..formatters.text_formatter import TextFormatter -from ..formatters.track_formatter import TrackFormatter -from ..formatters.resolution_formatter import ResolutionFormatter -from ..formatters.duration_formatter import DurationFormatter -from ..formatters.special_info_formatter import SpecialInfoFormatter -from ..formatters.formatter import FormatterApplier +from .media_panel_properties import MediaPanelProperties +from ..formatters.conditional_decorators import conditional_decorators class MediaPanelView: @@ -20,410 +11,125 @@ class MediaPanelView: def __init__(self, extractor): self.extractor = extractor + self._props = MediaPanelProperties(extractor) def file_info_panel(self) -> str: """Return formatted file info panel string""" - sections = [ - self.file_info(), - self.selected_data(), - self.tmdb_data(), - 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) + return "\n".join( + [ + self.fileinfo_section(), + self.selected_section(), + self.tmdb_section(), + self.tracksinfo_section(), + self.filename_section(), + self.metadata_section(), + self.mediainfo_section(), + ] + ) - 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": escape(str(self.extractor.get("file_path", "FileInfo"))), - "display_formatters": [TextFormatter.blue], - }, - { - "group": "File Info", - "label": "Size", - "value": self.extractor.get("file_size", "FileInfo"), - "value_formatters": [SizeFormatter.format_size_full], - "display_formatters": [TextFormatter.bold, TextFormatter.green], - }, - { - "group": "File Info", - "label": "Name", - "label_formatters": [TextFormatter.bold], - "value": escape(str(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", "FileInfo"), - "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", "FileInfo"), - "value_formatters": [ExtensionFormatter.format_extension_info], - "display_formatters": [TextFormatter.green], - }, - ] - return FormatterApplier.format_data_items(data) + @conditional_decorators.wrap("", "\n") + def fileinfo_section(self) -> str: + """Return formatted file info""" + return "\n".join( + [ + self._props.title("File Info"), + self._props.file_path, + self._props.file_size, + self._props.file_name, + self._props.modification_time, + self._props.extension_fileinfo, + ] + ) - def tmdb_data(self) -> list[str]: + @conditional_decorators.wrap("", "\n") + def selected_section(self) -> str: + """Return formatted selected data""" + return "\n".join( + [ + self._props.title("Selected Data"), + self._props.selected_order, + self._props.selected_title, + self._props.selected_year, + self._props.selected_special_info, + self._props.selected_source, + self._props.selected_frame_class, + self._props.selected_hdr, + self._props.selected_audio_langs, + self._props.selected_database_info, + ] + ) + + @conditional_decorators.wrap("", "\n") + def tmdb_section(self) -> str: """Return formatted TMDB data""" - data = [ - { - "label": "TMDB Data", - "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], - }, - { - "label": "ID", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("tmdb_id", "TMDB") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Title", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("title", "TMDB") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Original Title", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("original_title", "TMDB") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Year", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("year", "TMDB") or "", - "value_formatters": [TextFormatter.yellow,], - }, - { - "label": "Database Info", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("movie_db", "TMDB") or "", - "value_formatters": [SpecialInfoFormatter.format_database_info, TextFormatter.yellow], - }, - { - "label": "URL", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("tmdb_url", "TMDB") or "", - "value_formatters": [TextFormatter.format_url], - } - ] - return FormatterApplier.format_data_items(data) + return "\n".join( + [ + self._props.title("TMDB Data"), + self._props.tmdb_id, + self._props.tmdb_title, + self._props.tmdb_original_title, + self._props.tmdb_year, + self._props.tmdb_database_info, + self._props.tmdb_url, + ] + ) - def tracks_info(self) -> list[str]: - """Return formatted tracks information""" - data = [ - { - "group": "Tracks Info", - "label": "Tracks Info", - "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], - } - ] + @conditional_decorators.wrap("", "\n") + def tracksinfo_section(self) -> str: + """Return formatted tracks information panel""" + return "\n".join( + [ + self._props.title("Tracks Info"), + *self._props.video_tracks, + *self._props.audio_tracks, + *self._props.subtitle_tracks, + ] + ) - # Get video tracks - video_tracks = self.extractor.get("video_tracks", "MediaInfo") or [] - for item in video_tracks: - data.append( - { - "group": "Tracks Info", - "label": "Video Track", - "value": item, - "value_formatters": TrackFormatter.format_video_track, - "display_formatters": [TextFormatter.green], - } - ) - - # Get audio tracks - audio_tracks = self.extractor.get("audio_tracks", "MediaInfo") or [] - for i, item in enumerate(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], - } - ) - - # Get subtitle tracks - subtitle_tracks = self.extractor.get("subtitle_tracks", "MediaInfo") or [] - for i, item in enumerate(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 FormatterApplier.format_data_items(data) - - 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", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("title", "Metadata") or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - { - "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": "Artist", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("artist", "Metadata") or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - ] - - return FormatterApplier.format_data_items(data) - - def mediainfo_extracted_data(self) -> list[str]: - """Format media info extraction data for the mediainfo panel""" - data = [ - { - "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], - "value": self.extractor.get("frame_class", "MediaInfo") - or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - { - "label": "Resolution", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("resolution", "MediaInfo") - or "Not extracted", - "value_formatters": [ResolutionFormatter.format_resolution_dimensions], - "display_formatters": [TextFormatter.grey], - }, - { - "label": "Aspect Ratio", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("aspect_ratio", "MediaInfo") - or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - { - "label": "HDR", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("hdr", "MediaInfo") or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - { - "label": "Audio Languages", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("audio_langs", "MediaInfo") - or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - { - "label": "Anamorphic", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("anamorphic", "MediaInfo") or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - { - "label": "Extension", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("extension", "MediaInfo") or "Not extracted", - "value_formatters": [ExtensionFormatter.format_extension_info], - "display_formatters": [TextFormatter.grey], - }, - { - "label": "3D Layout", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("3d_layout", "MediaInfo") or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - ] - return FormatterApplier.format_data_items(data) - - def filename_extracted_data(self) -> list[str]: + @conditional_decorators.wrap("", "\n") + def filename_section(self) -> str: """Return formatted filename extracted data""" - data = [ - { - "label": "Filename Extracted Data", - "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], - }, - { - "label": "Order", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("order", "Filename") or "Not extracted", - "display_formatters": [TextFormatter.yellow], - }, - { - "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": "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], - }, - { - "label": "Special info", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("special_info", "Filename") - or "Not extracted", - "value_formatters": [ - SpecialInfoFormatter.format_special_info, - TextFormatter.blue, - ], - "display_formatters": [TextFormatter.grey], - }, - { - "label": "Movie DB", - "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("movie_db", "Filename") or "Not extracted", - "display_formatters": [TextFormatter.grey], - }, - ] + return "\n".join( + [ + self._props.title("Filename Extracted Data"), + self._props.filename_order, + self._props.filename_title, + self._props.filename_year, + self._props.filename_source, + self._props.filename_frame_class, + self._props.filename_hdr, + self._props.filename_audio_langs, + self._props.filename_special_info, + self._props.filename_movie_db, + ] + ) - return FormatterApplier.format_data_items(data) + @conditional_decorators.wrap("", "\n") + def metadata_section(self) -> str: + """Return formatted metadata extraction data""" + return "\n".join( + [ + self._props.title("Metadata Extraction"), + self._props.metadata_title, + self._props.metadata_duration, + self._props.metadata_artist, + ] + ) - def selected_data(self) -> list[str]: - """Return formatted selected data string""" - import logging - import os - if os.getenv("FORMATTER_LOG"): - frame_class = self.extractor.get("frame_class") - audio_langs = self.extractor.get("audio_langs") - logging.info(f"Selected data - frame_class: {frame_class!r}, audio_langs: {audio_langs!r}") - # Also check from Filename source - frame_class_filename = self.extractor.get("frame_class", "Filename") - audio_langs_filename = self.extractor.get("audio_langs", "Filename") - logging.info(f"From Filename - frame_class: {frame_class_filename!r}, audio_langs: {audio_langs_filename!r}") - data = [ - { - "label": "Selected Data", - "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], - }, - { - "label": "Order", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("order") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Title", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("title") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Year", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("year") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Special info", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("special_info") or "", - "value_formatters": [ - SpecialInfoFormatter.format_special_info, - TextFormatter.yellow, - ], - }, - { - "label": "Source", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("source") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Frame class", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("frame_class") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "HDR", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("hdr") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Audio langs", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("audio_langs") or "", - "value_formatters": [TextFormatter.yellow], - }, - { - "label": "Database Info", - "label_formatters": [TextFormatter.bold, TextFormatter.blue], - "value": self.extractor.get("movie_db") or "", - "value_formatters": [SpecialInfoFormatter.format_database_info, TextFormatter.yellow], - } - ] - return FormatterApplier.format_data_items(data) + @conditional_decorators.wrap("", "\n") + def mediainfo_section(self) -> str: + """Return formatted media info extraction data""" + return "\n".join( + [ + self._props.title("Media Info Extraction"), + self._props.mediainfo_duration, + self._props.mediainfo_frame_class, + self._props.mediainfo_resolution, + self._props.mediainfo_aspect_ratio, + self._props.mediainfo_hdr, + self._props.mediainfo_audio_langs, + self._props.mediainfo_anamorphic, + self._props.mediainfo_extension, + self._props.mediainfo_3d_layout, + ] + ) diff --git a/renamer/views/media_panel_properties.py b/renamer/views/media_panel_properties.py new file mode 100644 index 0000000..a23b0fd --- /dev/null +++ b/renamer/views/media_panel_properties.py @@ -0,0 +1,456 @@ +"""Media panel property methods using decorator pattern. + +This module contains all the formatted property methods that return +display-ready values for the media panel view. Each property uses +decorators to apply formatting, similar to ProposedFilenameView. +""" + +from ..formatters import ( + date_decorators, + text_decorators, + conditional_decorators, + size_decorators, + extension_decorators, + duration_decorators, + resolution_decorators, + special_info_decorators, + track_decorators, +) + + +class MediaPanelProperties: + """Formatted properties for media panel display. + + This class provides @property methods that return formatted values + ready for display in the media panel. Each property applies the + appropriate decorators for styling and formatting. + """ + + def __init__(self, extractor): + self._extractor = extractor + + # ============================================================ + # Section Title Formatter + # ============================================================ + + @text_decorators.bold() + @text_decorators.uppercase() + def title(self, title: str) -> str: + """Format section title with bold and uppercase styling.""" + return title + + # ============================================================ + # File Info Properties + # ============================================================ + + @property + @conditional_decorators.wrap("Path: ") + @text_decorators.blue() + @text_decorators.escape() + def file_path(self) -> str: + """Get file path formatted with label.""" + return self._extractor.get("file_path") + + @property + @conditional_decorators.wrap("Size: ") + @text_decorators.green() + @size_decorators.size_full() + def file_size(self) -> str: + """Get file size formatted with label.""" + return self._extractor.get("file_size") + + @property + @conditional_decorators.wrap("Name: ") + @text_decorators.cyan() + @text_decorators.escape() + def file_name(self) -> str: + """Get file name formatted with label.""" + return self._extractor.get("file_name") + + @property + @conditional_decorators.wrap("Modified: ") + @text_decorators.magenta() + @date_decorators.modification_date() + def modification_time(self) -> str: + """Get modification time formatted with label.""" + return self._extractor.get("modification_time") + + @property + @conditional_decorators.wrap("Extension: ") + @text_decorators.green() + @extension_decorators.extension_info() + def extension_fileinfo(self) -> str: + """Get extension from FileInfo formatted with label.""" + return self._extractor.get("extension") + + # ============================================================ + # TMDB Properties + # ============================================================ + + @property + @text_decorators.blue() + @conditional_decorators.wrap("ID: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def tmdb_id(self) -> str: + """Get TMDB ID formatted with label.""" + return self._extractor.get("tmdb_id", "TMDB") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Title: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def tmdb_title(self) -> str: + """Get TMDB title formatted with label.""" + return self._extractor.get("title", "TMDB") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Original Title: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def tmdb_original_title(self) -> str: + """Get TMDB original title formatted with label.""" + return self._extractor.get("original_title", "TMDB") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Year: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def tmdb_year(self) -> str: + """Get TMDB year formatted with label.""" + return self._extractor.get("year", "TMDB") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Database Info: ") + @text_decorators.yellow() + @conditional_decorators.default("") + @special_info_decorators.database_info() + def tmdb_database_info(self) -> str: + """Get TMDB database info formatted with label.""" + return self._extractor.get("movie_db", "TMDB") + + @property + # @text_decorators.blue() + @conditional_decorators.default("") + @text_decorators.url() + def tmdb_url(self) -> str: + """Get TMDB URL formatted with label.""" + return self._extractor.get("tmdb_url", "TMDB") + + # ============================================================ + # Metadata Extraction Properties + # ============================================================ + + @property + @conditional_decorators.wrap("Title: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def metadata_title(self) -> str: + """Get metadata title formatted with label.""" + return self._extractor.get("title", "Metadata") + + @property + @conditional_decorators.wrap("Duration: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + @duration_decorators.duration_full() + def metadata_duration(self) -> str: + """Get metadata duration formatted with label.""" + return self._extractor.get("duration", "Metadata") + + @property + @conditional_decorators.wrap("Artist: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def metadata_artist(self) -> str: + """Get metadata artist formatted with label.""" + return self._extractor.get("artist", "Metadata") + + # ============================================================ + # MediaInfo Extraction Properties + # ============================================================ + + @property + @conditional_decorators.wrap("Duration: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + @duration_decorators.duration_full() + def mediainfo_duration(self) -> str: + """Get MediaInfo duration formatted with label.""" + return self._extractor.get("duration", "MediaInfo") + + @property + @conditional_decorators.wrap("Frame Class: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def mediainfo_frame_class(self) -> str: + """Get MediaInfo frame class formatted with label.""" + return self._extractor.get("frame_class", "MediaInfo") + + @property + @conditional_decorators.wrap("Resolution: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + @resolution_decorators.resolution_dimensions() + def mediainfo_resolution(self) -> str: + """Get MediaInfo resolution formatted with label.""" + return self._extractor.get("resolution", "MediaInfo") + + @property + @conditional_decorators.wrap("Aspect Ratio: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def mediainfo_aspect_ratio(self) -> str: + """Get MediaInfo aspect ratio formatted with label.""" + return self._extractor.get("aspect_ratio", "MediaInfo") + + @property + @conditional_decorators.wrap("HDR: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def mediainfo_hdr(self) -> str: + """Get MediaInfo HDR formatted with label.""" + return self._extractor.get("hdr", "MediaInfo") + + @property + @conditional_decorators.wrap("Audio Languages: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def mediainfo_audio_langs(self) -> str: + """Get MediaInfo audio languages formatted with label.""" + return self._extractor.get("audio_langs", "MediaInfo") + + @property + @conditional_decorators.wrap("Anamorphic: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def mediainfo_anamorphic(self) -> str: + """Get MediaInfo anamorphic formatted with label.""" + return self._extractor.get("anamorphic", "MediaInfo") + + @property + @conditional_decorators.wrap("Extension: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + @extension_decorators.extension_info() + def mediainfo_extension(self) -> str: + """Get MediaInfo extension formatted with label.""" + return self._extractor.get("extension", "MediaInfo") + + @property + @conditional_decorators.wrap("3D Layout: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def mediainfo_3d_layout(self) -> str: + """Get MediaInfo 3D layout formatted with label.""" + return self._extractor.get("3d_layout", "MediaInfo") + + # ============================================================ + # Filename Extraction Properties + # ============================================================ + + @property + @conditional_decorators.wrap("Order: ") + @text_decorators.yellow() + @conditional_decorators.default("Not extracted") + def filename_order(self) -> str: + """Get filename order formatted with label.""" + return self._extractor.get("order", "Filename") + + @property + @conditional_decorators.wrap("Movie title: ") + @text_decorators.grey() + @conditional_decorators.default("") + def filename_title(self) -> str: + """Get filename title formatted with label.""" + return self._extractor.get("title", "Filename") + + @property + @conditional_decorators.wrap("Year: ") + @text_decorators.grey() + @conditional_decorators.default("") + def filename_year(self) -> str: + """Get filename year formatted with label.""" + return self._extractor.get("year", "Filename") + + @property + @conditional_decorators.wrap("Video source: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def filename_source(self) -> str: + """Get filename source formatted with label.""" + return self._extractor.get("source", "Filename") + + @property + @conditional_decorators.wrap("Frame class: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def filename_frame_class(self) -> str: + """Get filename frame class formatted with label.""" + return self._extractor.get("frame_class", "Filename") + + @property + @conditional_decorators.wrap("HDR: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def filename_hdr(self) -> str: + """Get filename HDR formatted with label.""" + return self._extractor.get("hdr", "Filename") + + @property + @conditional_decorators.wrap("Audio langs: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def filename_audio_langs(self) -> str: + """Get filename audio languages formatted with label.""" + return self._extractor.get("audio_langs", "Filename") + + @property + @conditional_decorators.wrap("Special info: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + @text_decorators.blue() + @special_info_decorators.special_info() + def filename_special_info(self) -> str: + """Get filename special info formatted with label.""" + return self._extractor.get("special_info", "Filename") + + @property + @conditional_decorators.wrap("Movie DB: ") + @text_decorators.grey() + @conditional_decorators.default("Not extracted") + def filename_movie_db(self) -> str: + """Get filename movie DB formatted with label.""" + return self._extractor.get("movie_db", "Filename") + + # ============================================================ + # Selected Data Properties + # ============================================================ + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Order: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def selected_order(self) -> str: + """Get selected order formatted with label.""" + return self._extractor.get("order") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Title: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def selected_title(self) -> str: + """Get selected title formatted with label.""" + return self._extractor.get("title") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Year: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def selected_year(self) -> str: + """Get selected year formatted with label.""" + return self._extractor.get("year") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Special info: ") + @text_decorators.yellow() + @conditional_decorators.default("") + @special_info_decorators.special_info() + def selected_special_info(self) -> str: + """Get selected special info formatted with label.""" + return self._extractor.get("special_info") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Source: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def selected_source(self) -> str: + """Get selected source formatted with label.""" + return self._extractor.get("source") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Frame class: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def selected_frame_class(self) -> str: + """Get selected frame class formatted with label.""" + return self._extractor.get("frame_class") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("HDR: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def selected_hdr(self) -> str: + """Get selected HDR formatted with label.""" + return self._extractor.get("hdr") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Audio langs: ") + @text_decorators.yellow() + @conditional_decorators.default("") + def selected_audio_langs(self) -> str: + """Get selected audio languages formatted with label.""" + return self._extractor.get("audio_langs") + + @property + @text_decorators.blue() + @conditional_decorators.wrap("Database Info: ") + @text_decorators.yellow() + @conditional_decorators.default("") + @special_info_decorators.database_info() + def selected_database_info(self) -> str: + """Get selected database info formatted with label.""" + return self._extractor.get("movie_db") + + @property + def video_tracks(self) -> list[str]: + """Return formatted video track data""" + tracks = self._extractor.get("video_tracks", "MediaInfo") or [] + return [self.video_track(track, i) for i, track in enumerate(tracks, start=1)] + + @text_decorators.green() + @conditional_decorators.wrap("Video Track {index}: ") + @track_decorators.video_track() + def video_track(self, track, index) -> str: + """Get video track info formatted with label.""" + return track + + @property + def audio_tracks(self) -> list[str]: + """Return formatted audio track data""" + tracks = self._extractor.get("audio_tracks", "MediaInfo") or [] + return [self.audio_track(track, i) for i, track in enumerate(tracks, start=1)] + + @text_decorators.yellow() + @conditional_decorators.wrap("Audio Track {index}: ") + @track_decorators.audio_track() + def audio_track(self, track, index) -> str: + """Get audio track info formatted with label.""" + return track + + @property + def subtitle_tracks(self) -> list[str]: + """Return formatted subtitle track data""" + tracks = self._extractor.get("subtitle_tracks", "MediaInfo") or [] + return [ + self.subtitle_track(track, i) for i, track in enumerate(tracks, start=1) + ] + + @text_decorators.magenta() + @conditional_decorators.wrap("Subtitle Track {index}: ") + @track_decorators.subtitle_track() + def subtitle_track(self, track, index) -> str: + """Get subtitle track info formatted with label.""" + return track diff --git a/uv.lock b/uv.lock index a788f98..8e7d5b5 100644 --- a/uv.lock +++ b/uv.lock @@ -462,7 +462,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.6.3" +version = "0.6.5" source = { editable = "." } dependencies = [ { name = "langcodes" },