diff --git a/.gitignore b/.gitignore index 90aa8dc..f1ba00b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ __pycache__/ build/ wheels/ *.egg-info - +*.log # Virtual environments .venv diff --git a/README.md b/README.md index eae8f5e..7b3d665 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,24 @@ renamer /path/to/media/directory 4. Press **y** to confirm or **n** to cancel 5. The file will be renamed and the tree updated automatically +## Debugging + +### Formatter Logging +The application includes detailed logging for formatter operations that can be enabled for debugging purposes. + +To enable formatter logging: +```bash +FORMATTER_LOG=1 renamer /path/to/directory +``` + +This will create a `formatter.log` file in the current directory containing: +- Formatter call sequences and ordering +- Input/output values for each formatter +- Caller information (file and line number) +- Any errors during formatting + +Useful for troubleshooting metadata display issues or formatter problems. + ## Architecture The application uses a modular architecture with separate extractors and formatters: diff --git a/ToDo.md b/ToDo.md index 12a611c..4efb45b 100644 --- a/ToDo.md +++ b/ToDo.md @@ -22,7 +22,8 @@ TODO Steps: 19. ✅ Optimize tree updates to avoid full reloads after renaming 20. ✅ Add loading indicators for metadata extraction 21. ✅ Add error handling for file operations and metadata extraction -22. Implement metadata editing capabilities (future enhancement) -23. Add batch rename operations (future enhancement) -24. Add configuration file support (future enhancement) -25. Add plugin system for custom extractors/formatters (future enhancement) \ No newline at end of file +22. 🔄 Implement blue highlighting for changed parts in proposed filename display (show differences between current and proposed names) +23. Implement metadata editing capabilities (future enhancement) +24. Add batch rename operations (future enhancement) +25. Add configuration file support (future enhancement) +26. Add plugin system for custom extractors/formatters (future enhancement) \ No newline at end of file diff --git a/dist/renamer-0.2.0-py3-none-any.whl b/dist/renamer-0.2.0-py3-none-any.whl deleted file mode 100644 index 6f576c2..0000000 Binary files a/dist/renamer-0.2.0-py3-none-any.whl and /dev/null differ diff --git a/dist/renamer-0.2.2-py3-none-any.whl b/dist/renamer-0.2.4-py3-none-any.whl similarity index 53% rename from dist/renamer-0.2.2-py3-none-any.whl rename to dist/renamer-0.2.4-py3-none-any.whl index b43a4cf..7742483 100644 Binary files a/dist/renamer-0.2.2-py3-none-any.whl and b/dist/renamer-0.2.4-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index 408d722..1e6bd4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "renamer" -version = "0.2.3" +version = "0.2.4" description = "Terminal-based media file renamer and metadata viewer" readme = "README.md" requires-python = ">=3.11" diff --git a/renamer/constants.py b/renamer/constants.py index 1926893..5d39475 100644 --- a/renamer/constants.py +++ b/renamer/constants.py @@ -151,4 +151,9 @@ SPECIAL_EDITIONS = [ "Festival Cut", "Workprint", "Rough Cut", + "Special Assembly Cut", + "Amazon Edition", + "Amazon", + "Netflix Edition", + "HBO Edition", ] diff --git a/renamer/formatters/formatter.py b/renamer/formatters/formatter.py new file mode 100644 index 0000000..89e29b1 --- /dev/null +++ b/renamer/formatters/formatter.py @@ -0,0 +1,132 @@ +from .text_formatter import TextFormatter +from .duration_formatter import DurationFormatter +from .size_formatter import SizeFormatter +from .date_formatter import DateFormatter +from .extension_formatter import ExtensionFormatter +from .resolution_formatter import ResolutionFormatter +from .track_formatter import TrackFormatter +from .special_info_formatter import SpecialInfoFormatter +import logging +import inspect +import os + + +# Set up logging conditionally +if os.getenv('FORMATTER_LOG', '0') == '1': + logging.basicConfig(filename='formatter.log', level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') +else: + logging.basicConfig(level=logging.CRITICAL) # Disable logging + + +class FormatterApplier: + """Class to apply multiple formatters in correct order""" + + # Define the global order of all formatters + FORMATTER_ORDER = [ + # Data formatters first (transform raw data) + DurationFormatter.format_seconds, + DurationFormatter.format_hhmmss, + DurationFormatter.format_hhmm, + DurationFormatter.format_full, + SizeFormatter.format_size, + SizeFormatter.format_size_full, + DateFormatter.format_modification_date, + DateFormatter.format_year, + ExtensionFormatter.format_extension_info, + ResolutionFormatter.get_frame_class_from_resolution, + ResolutionFormatter.format_resolution_p, + ResolutionFormatter.format_resolution_dimensions, + TrackFormatter.format_video_track, + TrackFormatter.format_audio_track, + TrackFormatter.format_subtitle_track, + SpecialInfoFormatter.format_special_info, + + # Text formatters second (transform text content) + TextFormatter.uppercase, + TextFormatter.lowercase, + TextFormatter.camelcase, + + # Markup formatters last (add visual styling) + TextFormatter.bold, + TextFormatter.italic, + TextFormatter.underline, + TextFormatter.bold_green, + TextFormatter.bold_cyan, + TextFormatter.bold_magenta, + TextFormatter.bold_yellow, + TextFormatter.green, + TextFormatter.yellow, + TextFormatter.magenta, + TextFormatter.cyan, + TextFormatter.red, + TextFormatter.blue, + TextFormatter.grey, + TextFormatter.dim, + ] + + @staticmethod + def apply_formatters(value, formatters): + """Apply multiple formatters to value in the global order""" + if not isinstance(formatters, list): + formatters = [formatters] if formatters else [] + + # Sort formatters according to the global order + ordered_formatters = sorted(formatters, key=lambda f: FormatterApplier.FORMATTER_ORDER.index(f) if f in FormatterApplier.FORMATTER_ORDER else len(FormatterApplier.FORMATTER_ORDER)) + + # Get caller info + frame = inspect.currentframe() + if frame and frame.f_back: + caller = f"{frame.f_back.f_code.co_filename}:{frame.f_back.f_lineno} in {frame.f_back.f_code.co_name}" + else: + caller = "Unknown" + + logging.info(f"Caller: {caller}") + logging.info(f"Original formatters: {[f.__name__ if hasattr(f, '__name__') else str(f) for f in formatters]}") + logging.info(f"Ordered formatters: {[f.__name__ if hasattr(f, '__name__') else str(f) for f in ordered_formatters]}") + logging.info(f"Input value: {repr(value)}") + + # Apply in the ordered sequence + for formatter in ordered_formatters: + try: + old_value = value + value = formatter(value) + logging.debug(f"Applied {formatter.__name__ if hasattr(formatter, '__name__') else str(formatter)}: {repr(old_value)} -> {repr(value)}") + except Exception as e: + logging.error(f"Error applying {formatter.__name__ if hasattr(formatter, '__name__') else str(formatter)}: {e}") + value = "Unknown" + + logging.info(f"Final value: {repr(value)}") + return value + + @staticmethod + def format_data_item(item: dict) -> str | None: + """Apply all formatting to a data item and return the formatted string""" + # Handle value formatting first (e.g., size formatting) + value = item.get("value") + if value is not None and value != "Not extracted": + value_formatters = item.get("value_formatters", []) + value = FormatterApplier.apply_formatters(value, value_formatters) + + # Handle label formatting + label = item.get("label", "") + if label: + label_formatters = item.get("label_formatters", []) + label = FormatterApplier.apply_formatters(label, label_formatters) + + # 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", []) + display_string = FormatterApplier.apply_formatters(display_string, display_formatters) + + return display_string + + @staticmethod + def format_data_items(data: list[dict]) -> list: + """Apply formatting to a list of data items""" + return [FormatterApplier.format_data_item(item) for item in data] \ No newline at end of file diff --git a/renamer/formatters/media_formatter.py b/renamer/formatters/media_formatter.py index 49e90c3..4c0fd28 100644 --- a/renamer/formatters/media_formatter.py +++ b/renamer/formatters/media_formatter.py @@ -8,6 +8,7 @@ from .track_formatter import TrackFormatter from .resolution_formatter import ResolutionFormatter from .duration_formatter import DurationFormatter from .special_info_formatter import SpecialInfoFormatter +from .formatter import FormatterApplier class MediaFormatter: @@ -16,56 +17,6 @@ class MediaFormatter: def __init__(self, extractor): self.extractor = extractor - def _format_data_item(self, item: dict) -> str: - """Apply all formatting to a data item and return the formatted string""" - # Define text formatters that should be applied before markup - text_formatters_set = { - TextFormatter.uppercase, - TextFormatter.lowercase, - TextFormatter.camelcase, - } - - # Handle value formatting first (e.g., size formatting) - value = item.get("value") - if value is not None and not isinstance(value, str): - value_formatters = item.get("value_formatters", []) - if not isinstance(value_formatters, list): - value_formatters = [value_formatters] if value_formatters else [] - 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""" sections = [ @@ -123,7 +74,7 @@ class MediaFormatter: "display_formatters": [TextFormatter.green], }, ] - return [self._format_data_item(item) for item in data] + return FormatterApplier.format_data_items(data) def tracks_info(self) -> list[str]: """Return formatted tracks information""" @@ -174,7 +125,7 @@ class MediaFormatter: } ) - return [self._format_data_item(item) for item in data] + return FormatterApplier.format_data_items(data) def metadata_extracted_data(self) -> list[str]: """Format metadata extraction data for the metadata panel""" @@ -204,7 +155,7 @@ class MediaFormatter: }, ] - return [self._format_data_item(item) for item in data] + return FormatterApplier.format_data_items(data) def mediainfo_extracted_data(self) -> list[str]: """Format media info extraction data for the mediainfo panel""" @@ -256,7 +207,7 @@ class MediaFormatter: "display_formatters": [TextFormatter.grey], }, ] - return [self._format_data_item(item) for item in data] + return FormatterApplier.format_data_items(data) def filename_extracted_data(self) -> list[str]: """Return formatted filename extracted data""" @@ -328,7 +279,7 @@ class MediaFormatter: }, ] - return [self._format_data_item(item) for item in data] + return FormatterApplier.format_data_items(data) def selected_data(self) -> list[str]: """Return formatted selected data string""" @@ -337,6 +288,12 @@ class MediaFormatter: "label": "Selected Data", "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], }, + { + "label": "Title", + "label_formatters": [TextFormatter.bold, TextFormatter.yellow], + "value": self.extractor.get("title") or "", + "value_formatters": [TextFormatter.blue], + }, { "label": "Special info", "label_formatters": [TextFormatter.bold], @@ -348,18 +305,4 @@ class MediaFormatter: "display_formatters": [TextFormatter.yellow], }, ] - return [self._format_data_item(item) for item in data] - - def _format_extra_metadata(self, metadata: dict) -> str: - """Format extra metadata like duration, title, artist""" - data = {} - if metadata.get("duration"): - data["Duration"] = f"{metadata['duration']:.1f} seconds" - if metadata.get("title"): - data["Title"] = metadata["title"] - if metadata.get("artist"): - data["Artist"] = metadata["artist"] - - return "\n".join( - TextFormatter.cyan(f"{key}: {value}") for key, value in data.items() - ) + return FormatterApplier.format_data_items(data) diff --git a/renamer/formatters/special_info_formatter.py b/renamer/formatters/special_info_formatter.py index 63dff59..617e4e1 100644 --- a/renamer/formatters/special_info_formatter.py +++ b/renamer/formatters/special_info_formatter.py @@ -5,5 +5,7 @@ class SpecialInfoFormatter: def format_special_info(special_info): """Convert special info list to comma-separated string""" if isinstance(special_info, list): - return ", ".join(special_info) + # Filter out None values and ensure all items are strings + filtered = [str(item) for item in special_info if item is not None] + return ", ".join(filtered) return special_info or "" \ No newline at end of file diff --git a/uv.lock b/uv.lock index 4752a2c..fd88f24 100644 --- a/uv.lock +++ b/uv.lock @@ -164,7 +164,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.2.3" +version = "0.2.4" source = { editable = "." } dependencies = [ { name = "langcodes" },