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] [project]
name = "renamer" name = "renamer"
version = "0.6.1" version = "0.6.2"
description = "Terminal-based media file renamer and metadata viewer" description = "Terminal-based media file renamer and metadata viewer"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"

View File

@@ -2,6 +2,6 @@
from .app import RenamerApp from .app import RenamerApp
from .extractors.extractor import MediaExtractor 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 .constants import MEDIA_TYPES
from .screens import OpenScreen, HelpScreen, RenameConfirmScreen, SettingsScreen from .screens import OpenScreen, HelpScreen, RenameConfirmScreen, SettingsScreen
from .extractors.extractor import MediaExtractor from .extractors.extractor import MediaExtractor
from .formatters.media_formatter import MediaFormatter from .views import MediaPanelView, ProposedFilenameView
from .formatters.proposed_name_formatter import ProposedNameFormatter
from .formatters.text_formatter import TextFormatter from .formatters.text_formatter import TextFormatter
from .formatters.catalog_formatter import CatalogFormatter from .formatters.catalog_formatter import CatalogFormatter
from .settings import Settings from .settings import Settings
@@ -216,7 +215,7 @@ class RenamerApp(App):
mode = self.settings.get("mode") mode = self.settings.get("mode")
if mode == "technical": if mode == "technical":
formatter = MediaFormatter(extractor) formatter = MediaPanelView(extractor)
full_info = formatter.file_info_panel() full_info = formatter.file_info_panel()
else: # catalog else: # catalog
formatter = CatalogFormatter(extractor) formatter = CatalogFormatter(extractor)
@@ -226,7 +225,7 @@ class RenamerApp(App):
self.call_later( self.call_later(
self._update_details, self._update_details,
full_info, full_info,
ProposedNameFormatter(extractor).rename_line_formatted(file_path), ProposedFilenameView(extractor).rename_line_formatted(file_path),
) )
except Exception as e: except Exception as e:
self.call_later( self.call_later(
@@ -345,7 +344,7 @@ By Category:"""
if node and node.data and isinstance(node.data, Path) and node.data.is_file(): if node and node.data and isinstance(node.data, Path) and node.data.is_file():
# Get the proposed name from the extractor # Get the proposed name from the extractor
extractor = MediaExtractor(node.data) extractor = MediaExtractor(node.data)
proposed_formatter = ProposedNameFormatter(extractor) proposed_formatter = ProposedFilenameView(extractor)
new_name = str(proposed_formatter) new_name = str(proposed_formatter)
logging.info(f"Proposed new name: {new_name!r} for file: {node.data}") logging.info(f"Proposed new name: {new_name!r} for file: {node.data}")
if new_name and new_name != node.data.name: 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.cache import Cache
from renamer.settings import Settings from renamer.settings import Settings
from renamer.extractors.extractor import MediaExtractor 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.catalog_formatter import CatalogFormatter
from renamer.formatters.proposed_name_formatter import ProposedNameFormatter
from renamer.formatters.text_formatter import TextFormatter from renamer.formatters.text_formatter import TextFormatter
@@ -171,14 +170,14 @@ class MetadataService:
# Format based on mode # Format based on mode
if mode == "technical": if mode == "technical":
formatter = MediaFormatter(extractor) formatter = MediaPanelView(extractor)
formatted_info = formatter.file_info_panel() formatted_info = formatter.file_info_panel()
else: # catalog else: # catalog
formatter = CatalogFormatter(extractor) formatter = CatalogFormatter(extractor)
formatted_info = formatter.format_catalog_info() formatted_info = formatter.format_catalog_info()
# Generate proposed name # Generate proposed name
proposed_formatter = ProposedNameFormatter(extractor) proposed_formatter = ProposedFilenameView(extractor)
proposed_name = proposed_formatter.rename_line_formatted(file_path) proposed_name = proposed_formatter.rename_line_formatted(file_path)
return { return {

View File

@@ -14,7 +14,7 @@ from pathlib import Path
from typing import Optional, Callable from typing import Optional, Callable
from renamer.extractors.extractor import MediaExtractor from renamer.extractors.extractor import MediaExtractor
from renamer.formatters.proposed_name_formatter import ProposedNameFormatter from renamer.views import ProposedFilenameView
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ class RenameService:
if extractor is None: if extractor is None:
extractor = MediaExtractor(file_path) extractor = MediaExtractor(file_path)
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
# Get the formatted rename line # Get the formatted rename line
rename_line = formatter.rename_line_formatted(file_path) 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 import pytest
from pathlib import Path from pathlib import Path
from renamer.formatters.proposed_name_formatter import ProposedNameFormatter from renamer.views import ProposedFilenameView
class TestProposedNameFormatter: class TestProposedFilenameView:
"""Test ProposedNameFormatter with decorator pattern.""" """Test ProposedFilenameView with decorator pattern."""
def test_basic_formatting(self): def test_basic_formatting(self):
"""Test basic filename formatting with all fields.""" """Test basic filename formatting with all fields."""
@@ -23,7 +23,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
assert '[01]' in result assert '[01]' in result
@@ -45,7 +45,7 @@ class TestProposedNameFormatter:
'extension': 'mp4' 'extension': 'mp4'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
assert 'Simple Movie' in result assert 'Simple Movie' in result
@@ -61,7 +61,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
assert 'Movie-Title-Test' in result assert 'Movie-Title-Test' in result
@@ -76,7 +76,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
# Since title is None, it won't appear (unless extractor provides default) # Since title is None, it won't appear (unless extractor provides default)
@@ -90,7 +90,7 @@ class TestProposedNameFormatter:
'extension': None 'extension': None
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
# Extension handling depends on extractor default # Extension handling depends on extractor default
@@ -105,7 +105,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
assert 'Extended Edition, Remastered' in result assert 'Extended Edition, Remastered' in result
@@ -119,7 +119,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
assert 'imdbid-tt1234567' in result assert 'imdbid-tt1234567' in result
@@ -132,7 +132,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
assert str(formatter) == formatter.rename_line assert str(formatter) == formatter.rename_line
def test_formatted_display_matching_name(self): def test_formatted_display_matching_name(self):
@@ -143,7 +143,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
proposed = str(formatter) proposed = str(formatter)
file_path = Path(proposed) file_path = Path(proposed)
@@ -160,7 +160,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
file_path = Path('different_name.mkv') file_path = Path('different_name.mkv')
result = formatter.rename_line_formatted(file_path) result = formatter.rename_line_formatted(file_path)
@@ -175,7 +175,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
assert '(2020)' in result assert '(2020)' in result
@@ -188,7 +188,7 @@ class TestProposedNameFormatter:
'extension': 'mkv' 'extension': 'mkv'
} }
formatter = ProposedNameFormatter(extractor) formatter = ProposedFilenameView(extractor)
result = formatter.rename_line result = formatter.rename_line
# Should not have empty parentheses # 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 pathlib import Path
from rich.markup import escape from rich.markup import escape
from .size_formatter import SizeFormatter from ..formatters.size_formatter import SizeFormatter
from .date_formatter import DateFormatter from ..formatters.date_formatter import DateFormatter
from .extension_formatter import ExtensionFormatter from ..formatters.extension_formatter import ExtensionFormatter
from .text_formatter import TextFormatter from ..formatters.text_formatter import TextFormatter
from .track_formatter import TrackFormatter from ..formatters.track_formatter import TrackFormatter
from .resolution_formatter import ResolutionFormatter from ..formatters.resolution_formatter import ResolutionFormatter
from .duration_formatter import DurationFormatter from ..formatters.duration_formatter import DurationFormatter
from .special_info_formatter import SpecialInfoFormatter from ..formatters.special_info_formatter import SpecialInfoFormatter
from .formatter import FormatterApplier from ..formatters.formatter import FormatterApplier
class MediaFormatter: class MediaPanelView:
"""Class to format media data for display""" """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): def __init__(self, extractor):
self.extractor = extractor self.extractor = extractor

View File

@@ -1,11 +1,15 @@
from rich.markup import escape from rich.markup import escape
from .special_info_decorators import special_info_decorators from ..formatters.special_info_decorators import special_info_decorators
from .conditional_decorators import conditional_decorators from ..formatters.conditional_decorators import conditional_decorators
from .text_decorators import text_decorators from ..formatters.text_decorators import text_decorators
class ProposedNameFormatter: class ProposedFilenameView:
"""Class for formatting proposed filenames using decorator pattern with properties.""" """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): def __init__(self, extractor):
"""Initialize with media extractor data""" """Initialize with media extractor data"""
@@ -87,12 +91,14 @@ class ProposedNameFormatter:
return self.rename_line_different return self.rename_line_different
@property @property
@conditional_decorators.wrap(">> ", " <<")
@text_decorators.green() @text_decorators.green()
def rename_line_similar(self) -> str: def rename_line_similar(self) -> str:
"""Generate a simplified proposed filename for similarity checks.""" """Generate a simplified proposed filename for similarity checks."""
return escape(str(self)) return escape(str(self))
@property @property
@conditional_decorators.wrap(">> ", " <<")
@text_decorators.orange() @text_decorators.orange()
def rename_line_different(self) -> str: def rename_line_different(self) -> str:
"""Generate a detailed proposed filename for difference checks.""" """Generate a detailed proposed filename for difference checks."""

2
uv.lock generated
View File

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