feat: Update version to 0.2.4, enhance logging capabilities, and improve special info formatting

This commit is contained in:
sHa
2025-12-27 01:31:21 +00:00
parent 44a6a978b8
commit 95cbaee7fa
11 changed files with 179 additions and 78 deletions

2
.gitignore vendored
View File

@@ -4,6 +4,6 @@ __pycache__/
build/
wheels/
*.egg-info
*.log
# Virtual environments
.venv

View File

@@ -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:

View File

@@ -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)
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)

Binary file not shown.

View File

@@ -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"

View File

@@ -151,4 +151,9 @@ SPECIAL_EDITIONS = [
"Festival Cut",
"Workprint",
"Rough Cut",
"Special Assembly Cut",
"Amazon Edition",
"Amazon",
"Netflix Edition",
"HBO Edition",
]

View File

@@ -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]

View File

@@ -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 "<None>",
"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)

View File

@@ -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 ""

2
uv.lock generated
View File

@@ -164,7 +164,7 @@ wheels = [
[[package]]
name = "renamer"
version = "0.2.3"
version = "0.2.4"
source = { editable = "." }
dependencies = [
{ name = "langcodes" },