Add ProposedFilenameView and MediaPanelView with comprehensive tests

- Implemented ProposedFilenameView to generate standardized filenames using a decorator pattern.
- Created MediaPanelView to assemble media data panels for display, aggregating multiple formatters.
- Added tests for ProposedFilenameView covering various formatting scenarios, including basic, minimal, and special cases.
- Introduced a views package to organize and expose the new views.
- Ensured proper formatting and display of media information, including file info, TMDB data, and track information.
This commit is contained in:
sHa
2026-01-02 12:29:04 +00:00
parent e64aaf320b
commit 981000793f
11 changed files with 70 additions and 48 deletions

BIN
dist/renamer-0.6.2-py3-none-any.whl vendored Normal file

Binary file not shown.

View File

@@ -1,6 +1,6 @@
[project]
name = "renamer"
version = "0.6.1"
version = "0.6.2"
description = "Terminal-based media file renamer and metadata viewer"
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -2,6 +2,6 @@
from .app import RenamerApp
from .extractors.extractor import MediaExtractor
from .formatters.media_formatter import MediaFormatter
from .views import MediaPanelView, ProposedFilenameView
__all__ = ['RenamerApp', 'MediaExtractor', 'MediaFormatter']
__all__ = ['RenamerApp', 'MediaExtractor', 'MediaPanelView', 'ProposedFilenameView']

View File

@@ -14,8 +14,7 @@ import os
from .constants import MEDIA_TYPES
from .screens import OpenScreen, HelpScreen, RenameConfirmScreen, SettingsScreen
from .extractors.extractor import MediaExtractor
from .formatters.media_formatter import MediaFormatter
from .formatters.proposed_name_formatter import ProposedNameFormatter
from .views import MediaPanelView, ProposedFilenameView
from .formatters.text_formatter import TextFormatter
from .formatters.catalog_formatter import CatalogFormatter
from .settings import Settings
@@ -216,7 +215,7 @@ class RenamerApp(App):
mode = self.settings.get("mode")
if mode == "technical":
formatter = MediaFormatter(extractor)
formatter = MediaPanelView(extractor)
full_info = formatter.file_info_panel()
else: # catalog
formatter = CatalogFormatter(extractor)
@@ -226,7 +225,7 @@ class RenamerApp(App):
self.call_later(
self._update_details,
full_info,
ProposedNameFormatter(extractor).rename_line_formatted(file_path),
ProposedFilenameView(extractor).rename_line_formatted(file_path),
)
except Exception as e:
self.call_later(
@@ -345,7 +344,7 @@ By Category:"""
if node and node.data and isinstance(node.data, Path) and node.data.is_file():
# Get the proposed name from the extractor
extractor = MediaExtractor(node.data)
proposed_formatter = ProposedNameFormatter(extractor)
proposed_formatter = ProposedFilenameView(extractor)
new_name = str(proposed_formatter)
logging.info(f"Proposed new name: {new_name!r} for file: {node.data}")
if new_name and new_name != node.data.name:

View File

@@ -16,9 +16,8 @@ from threading import Lock
from renamer.cache import Cache
from renamer.settings import Settings
from renamer.extractors.extractor import MediaExtractor
from renamer.formatters.media_formatter import MediaFormatter
from renamer.views import MediaPanelView, ProposedFilenameView
from renamer.formatters.catalog_formatter import CatalogFormatter
from renamer.formatters.proposed_name_formatter import ProposedNameFormatter
from renamer.formatters.text_formatter import TextFormatter
@@ -171,14 +170,14 @@ class MetadataService:
# Format based on mode
if mode == "technical":
formatter = MediaFormatter(extractor)
formatter = MediaPanelView(extractor)
formatted_info = formatter.file_info_panel()
else: # catalog
formatter = CatalogFormatter(extractor)
formatted_info = formatter.format_catalog_info()
# Generate proposed name
proposed_formatter = ProposedNameFormatter(extractor)
proposed_formatter = ProposedFilenameView(extractor)
proposed_name = proposed_formatter.rename_line_formatted(file_path)
return {

View File

@@ -14,7 +14,7 @@ from pathlib import Path
from typing import Optional, Callable
from renamer.extractors.extractor import MediaExtractor
from renamer.formatters.proposed_name_formatter import ProposedNameFormatter
from renamer.views import ProposedFilenameView
logger = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ class RenameService:
if extractor is None:
extractor = MediaExtractor(file_path)
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
# Get the formatted rename line
rename_line = formatter.rename_line_formatted(file_path)

View File

@@ -1,12 +1,12 @@
"""Tests for ProposedNameFormatter with decorator pattern."""
"""Tests for ProposedFilenameView with decorator pattern."""
import pytest
from pathlib import Path
from renamer.formatters.proposed_name_formatter import ProposedNameFormatter
from renamer.views import ProposedFilenameView
class TestProposedNameFormatter:
"""Test ProposedNameFormatter with decorator pattern."""
class TestProposedFilenameView:
"""Test ProposedFilenameView with decorator pattern."""
def test_basic_formatting(self):
"""Test basic filename formatting with all fields."""
@@ -23,7 +23,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
assert '[01]' in result
@@ -45,7 +45,7 @@ class TestProposedNameFormatter:
'extension': 'mp4'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
assert 'Simple Movie' in result
@@ -61,7 +61,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
assert 'Movie-Title-Test' in result
@@ -76,7 +76,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
# Since title is None, it won't appear (unless extractor provides default)
@@ -90,7 +90,7 @@ class TestProposedNameFormatter:
'extension': None
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
# Extension handling depends on extractor default
@@ -105,7 +105,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
assert 'Extended Edition, Remastered' in result
@@ -119,7 +119,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
assert 'imdbid-tt1234567' in result
@@ -132,7 +132,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
assert str(formatter) == formatter.rename_line
def test_formatted_display_matching_name(self):
@@ -143,7 +143,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
proposed = str(formatter)
file_path = Path(proposed)
@@ -160,7 +160,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
file_path = Path('different_name.mkv')
result = formatter.rename_line_formatted(file_path)
@@ -175,7 +175,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
assert '(2020)' in result
@@ -188,7 +188,7 @@ class TestProposedNameFormatter:
'extension': 'mkv'
}
formatter = ProposedNameFormatter(extractor)
formatter = ProposedFilenameView(extractor)
result = formatter.rename_line
# Should not have empty parentheses

14
renamer/views/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
"""Views package - assembles formatted data for display.
Views compose multiple formatters to create complex display outputs.
Unlike formatters which transform single values, views aggregate and
orchestrate multiple formatters to build complete UI panels.
"""
from .proposed_filename import ProposedFilenameView
from .media_panel import MediaPanelView
__all__ = [
'ProposedFilenameView',
'MediaPanelView',
]

View File

@@ -1,18 +1,22 @@
from pathlib import Path
from rich.markup import escape
from .size_formatter import SizeFormatter
from .date_formatter import DateFormatter
from .extension_formatter import ExtensionFormatter
from .text_formatter import TextFormatter
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
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
class MediaFormatter:
"""Class to format media data for display"""
class MediaPanelView:
"""View for assembling media data panels for display.
This view aggregates multiple formatters to create comprehensive
display panels for technical and catalog modes.
"""
def __init__(self, extractor):
self.extractor = extractor

View File

@@ -1,11 +1,15 @@
from rich.markup import escape
from .special_info_decorators import special_info_decorators
from .conditional_decorators import conditional_decorators
from .text_decorators import text_decorators
from ..formatters.special_info_decorators import special_info_decorators
from ..formatters.conditional_decorators import conditional_decorators
from ..formatters.text_decorators import text_decorators
class ProposedNameFormatter:
"""Class for formatting proposed filenames using decorator pattern with properties."""
class ProposedFilenameView:
"""View for generating proposed filenames using decorator pattern with properties.
This view composes formatter decorators to generate clean, standardized filenames
from extracted metadata. It uses property decorators for declarative formatting.
"""
def __init__(self, extractor):
"""Initialize with media extractor data"""
@@ -87,12 +91,14 @@ class ProposedNameFormatter:
return self.rename_line_different
@property
@conditional_decorators.wrap(">> ", " <<")
@text_decorators.green()
def rename_line_similar(self) -> str:
"""Generate a simplified proposed filename for similarity checks."""
return escape(str(self))
@property
@conditional_decorators.wrap(">> ", " <<")
@text_decorators.orange()
def rename_line_different(self) -> str:
"""Generate a detailed proposed filename for difference checks."""

2
uv.lock generated
View File

@@ -462,7 +462,7 @@ wheels = [
[[package]]
name = "renamer"
version = "0.6.1"
version = "0.6.2"
source = { editable = "." }
dependencies = [
{ name = "langcodes" },