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:
BIN
dist/renamer-0.6.2-py3-none-any.whl
vendored
Normal file
BIN
dist/renamer-0.6.2-py3-none-any.whl
vendored
Normal file
Binary file not shown.
@@ -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"
|
||||||
|
|||||||
@@ -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']
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
14
renamer/views/__init__.py
Normal 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',
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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."""
|
||||||
Reference in New Issue
Block a user