diff --git a/dist/renamer-0.6.1-py3-none-any.whl b/dist/renamer-0.6.1-py3-none-any.whl new file mode 100644 index 0000000..7ec44df Binary files /dev/null and b/dist/renamer-0.6.1-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index 73c0753..2e50457 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "renamer" -version = "0.6.0" +version = "0.6.1" description = "Terminal-based media file renamer and metadata viewer" readme = "README.md" requires-python = ">=3.11" diff --git a/renamer/extractors/default_extractor.py b/renamer/extractors/default_extractor.py index 193aa22..e717fca 100644 --- a/renamer/extractors/default_extractor.py +++ b/renamer/extractors/default_extractor.py @@ -102,7 +102,7 @@ class DefaultExtractor: def extract_extension(self) -> Optional[str]: """Return file extension. Returns None as no extension is available.""" - return None + return "ext" def extract_tmdb_url(self) -> Optional[str]: """Return TMDB URL. Returns None as no TMDB URL is available.""" diff --git a/renamer/extractors/fileinfo_extractor.py b/renamer/extractors/fileinfo_extractor.py index 739f1d4..13ed3b6 100644 --- a/renamer/extractors/fileinfo_extractor.py +++ b/renamer/extractors/fileinfo_extractor.py @@ -7,7 +7,7 @@ file system metadata such as size, timestamps, paths, and extensions. from pathlib import Path import logging import os -from ..decorators import cached_method +from ..cache import cached_method # Set up logging conditionally if os.getenv('FORMATTER_LOG', '0') == '1': diff --git a/renamer/extractors/filename_extractor.py b/renamer/extractors/filename_extractor.py index dd05196..341e8a9 100644 --- a/renamer/extractors/filename_extractor.py +++ b/renamer/extractors/filename_extractor.py @@ -8,7 +8,7 @@ from ..constants import ( is_valid_year, CYRILLIC_TO_ENGLISH ) -from ..decorators import cached_method +from ..cache import cached_method from ..utils.pattern_utils import PatternExtractor import langcodes diff --git a/renamer/extractors/mediainfo_extractor.py b/renamer/extractors/mediainfo_extractor.py index c76051e..2c4ed18 100644 --- a/renamer/extractors/mediainfo_extractor.py +++ b/renamer/extractors/mediainfo_extractor.py @@ -2,7 +2,7 @@ from pathlib import Path from pymediainfo import MediaInfo from collections import Counter from ..constants import FRAME_CLASSES, MEDIA_TYPES -from ..decorators import cached_method +from ..cache import cached_method import langcodes import logging diff --git a/renamer/extractors/metadata_extractor.py b/renamer/extractors/metadata_extractor.py index 3140f32..dd0a00d 100644 --- a/renamer/extractors/metadata_extractor.py +++ b/renamer/extractors/metadata_extractor.py @@ -8,7 +8,7 @@ import mutagen import logging from pathlib import Path from ..constants import MEDIA_TYPES -from ..decorators import cached_method +from ..cache import cached_method logger = logging.getLogger(__name__) diff --git a/renamer/formatters/__init__.py b/renamer/formatters/__init__.py index 08d2407..48764b4 100644 --- a/renamer/formatters/__init__.py +++ b/renamer/formatters/__init__.py @@ -23,6 +23,12 @@ from .track_formatter import TrackFormatter from .special_info_formatter import SpecialInfoFormatter from .formatter import FormatterApplier +# Decorator instances +from .date_decorators import date_decorators, DateDecorators +from .special_info_decorators import special_info_decorators, SpecialInfoDecorators +from .text_decorators import text_decorators, TextDecorators +from .conditional_decorators import conditional_decorators, ConditionalDecorators + __all__ = [ # Base classes 'Formatter', @@ -41,4 +47,14 @@ __all__ = [ 'TrackFormatter', 'SpecialInfoFormatter', 'FormatterApplier', + + # Decorator instances and classes + 'date_decorators', + 'DateDecorators', + 'special_info_decorators', + 'SpecialInfoDecorators', + 'text_decorators', + 'TextDecorators', + 'conditional_decorators', + 'ConditionalDecorators', ] \ No newline at end of file diff --git a/renamer/formatters/conditional_decorators.py b/renamer/formatters/conditional_decorators.py new file mode 100644 index 0000000..2d6d3cc --- /dev/null +++ b/renamer/formatters/conditional_decorators.py @@ -0,0 +1,88 @@ +"""Conditional formatting decorators. + +Provides decorators for conditional formatting (wrap, replace_slashes, default): + + @conditional_decorators.wrap("[", "]") + def get_order(self): + return self.extractor.get('order') +""" + +from functools import wraps +from typing import Callable, Any + + +class ConditionalDecorators: + """Conditional formatting decorators (wrap, replace_slashes, default).""" + + @staticmethod + def wrap(left: str, right: str = "") -> Callable: + """Decorator to wrap value with delimiters if it exists. + + Can be used for prefix-only (right=""), suffix-only (left=""), or both. + + Usage: + @conditional_decorators.wrap("[", "]") + def get_order(self): + return self.extractor.get('order') + + # Prefix only + @conditional_decorators.wrap(" ") + def get_source(self): + return self.extractor.get('source') + + # Suffix only + @conditional_decorators.wrap("", ",") + def get_hdr(self): + return self.extractor.get('hdr') + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return f"{left}{result}{right}" if result else "" + return wrapper + return decorator + + @staticmethod + def replace_slashes() -> Callable: + """Decorator to replace forward and back slashes with dashes. + + Usage: + @conditional_decorators.replace_slashes() + def get_title(self): + return self.extractor.get('title') + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + if result: + return str(result).replace("/", "-").replace("\\", "-") + return result or "" + return wrapper + return decorator + + @staticmethod + def default(default_value: Any) -> Callable: + """Decorator to provide a default value if result is None or empty. + + NOTE: It's better to handle defaults in the extractor itself rather than + using this decorator. This decorator should only be used when the extractor + cannot provide a sensible default. + + Usage: + @conditional_decorators.default("Unknown") + def get_value(self): + return self.extractor.get('value') + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> Any: + result = func(*args, **kwargs) + return result if result else default_value + return wrapper + return decorator + + +# Singleton instance +conditional_decorators = ConditionalDecorators() diff --git a/renamer/formatters/date_decorators.py b/renamer/formatters/date_decorators.py new file mode 100644 index 0000000..3759fca --- /dev/null +++ b/renamer/formatters/date_decorators.py @@ -0,0 +1,37 @@ +"""Date formatting decorators. + +Provides decorator versions of DateFormatter methods for cleaner code: + + @date_decorators.year() + def get_year(self): + return self.extractor.get('year') +""" + +from functools import wraps +from typing import Callable +from .date_formatter import DateFormatter + + +class DateDecorators: + """Date and time formatting decorators.""" + + @staticmethod + def modification_date() -> Callable: + """Decorator to format modification dates. + + Usage: + @date_decorators.modification_date() + def get_mtime(self): + return self.file_path.stat().st_mtime + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return DateFormatter.format_modification_date(result) + return wrapper + return decorator + + +# Singleton instance +date_decorators = DateDecorators() diff --git a/renamer/formatters/proposed_name_formatter.py b/renamer/formatters/proposed_name_formatter.py index 28a7f22..6b98e05 100644 --- a/renamer/formatters/proposed_name_formatter.py +++ b/renamer/formatters/proposed_name_formatter.py @@ -1,37 +1,99 @@ from rich.markup import escape -from .text_formatter import TextFormatter -from .date_formatter import DateFormatter -from .special_info_formatter import SpecialInfoFormatter +from .special_info_decorators import special_info_decorators +from .conditional_decorators import conditional_decorators +from .text_decorators import text_decorators class ProposedNameFormatter: - """Class for formatting proposed filenames""" + """Class for formatting proposed filenames using decorator pattern with properties.""" def __init__(self, extractor): """Initialize with media extractor data""" - - self.__order = f"[{extractor.get('order')}] " if extractor.get("order") else "" - self.__title = (extractor.get("title") or "Unknown Title").replace("/", "-").replace("\\", "-") - self.__year = DateFormatter.format_year(extractor.get("year")) - self.__source = f" {extractor.get('source')}" if extractor.get("source") else "" - self.__frame_class = extractor.get("frame_class") or None - self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else "" - self.__audio_langs = extractor.get("audio_langs") or None - self.__special_info = f" [{SpecialInfoFormatter.format_special_info(extractor.get('special_info'))}]" if extractor.get("special_info") else "" - self.__db_info = f" [{SpecialInfoFormatter.format_database_info(extractor.get('movie_db'))}]" if extractor.get("movie_db") else "" - self.__extension = extractor.get("extension") or "ext" + self._extractor = extractor def __str__(self) -> str: """Convert the proposed name to string""" - return self.rename_line() + return self.rename_line + @property + @conditional_decorators.wrap("[", "] ") + def _order(self) -> str: + """Get the order number formatted as [XX] """ + return self._extractor.get("order") + + @property + @conditional_decorators.replace_slashes() + def _title(self) -> str: + """Get the title with slashes replaced""" + return self._extractor.get("title") + + @property + @conditional_decorators.wrap(" (", ")") + def _year(self) -> str: + """Get the year formatted as (YYYY)""" + return self._extractor.get("year") + + @property + @conditional_decorators.wrap(" ") + def _source(self) -> str: + """Get the source""" + return self._extractor.get("source") + + @property + def _frame_class(self) -> str: + """Get the frame class""" + return self._extractor.get("frame_class") or "" + + @property + @conditional_decorators.wrap(",") + def _hdr(self) -> str: + """Get the HDR info formatted with a trailing comma if present""" + return self._extractor.get("hdr") + + @property + def _audio_langs(self) -> str: + """Get the audio languages formatted with a trailing comma if present""" + return self._extractor.get("audio_langs") or "" + + @property + @conditional_decorators.wrap(" [", "]") + @special_info_decorators.special_info() + def _special_info(self) -> str: + """Get the special info formatted within brackets""" + return self._extractor.get("special_info") + + @property + @conditional_decorators.wrap(" [", "]") + @special_info_decorators.database_info() + def _db_info(self) -> str: + """Get the database info formatted within brackets""" + return self._extractor.get("movie_db") + + @property + def _extension(self) -> str: + """Get the file extension""" + return self._extractor.get("extension") + + @property def rename_line(self) -> str: - result = f"{self.__order}{self.__title} {self.__year}{self.__special_info}{self.__source} [{self.__frame_class}{self.__hdr},{self.__audio_langs}]{self.__db_info}.{self.__extension}" + """Generate the proposed filename.""" + result = f"{self._order}{self._title}{self._year}{self._special_info}{self._source} [{self._frame_class}{self._hdr},{self._audio_langs}]{self._db_info}.{self._extension}" return result.replace("/", "-").replace("\\", "-") def rename_line_formatted(self, file_path) -> str: """Format the proposed name for display with color""" - proposed = escape(str(self)) if file_path.name == str(self): - return f">> {TextFormatter.green(proposed)} <<" - return f">> {TextFormatter.bold_yellow(proposed)} <<" + return self.rename_line_similar + return self.rename_line_different + + @property + @text_decorators.green() + def rename_line_similar(self) -> str: + """Generate a simplified proposed filename for similarity checks.""" + return escape(str(self)) + + @property + @text_decorators.orange() + def rename_line_different(self) -> str: + """Generate a detailed proposed filename for difference checks.""" + return escape(str(self)) \ No newline at end of file diff --git a/renamer/formatters/special_info_decorators.py b/renamer/formatters/special_info_decorators.py new file mode 100644 index 0000000..01b0c42 --- /dev/null +++ b/renamer/formatters/special_info_decorators.py @@ -0,0 +1,54 @@ +"""Special info formatting decorators. + +Provides decorator versions of SpecialInfoFormatter methods: + + @special_info_decorators.special_info() + def get_special_info(self): + return self.extractor.get('special_info') +""" + +from functools import wraps +from typing import Callable +from .special_info_formatter import SpecialInfoFormatter + + +class SpecialInfoDecorators: + """Special info and database formatting decorators.""" + + @staticmethod + def special_info() -> Callable: + """Decorator to format special info lists. + + Usage: + @special_info_decorators.special_info() + def get_special_info(self): + return self.extractor.get('special_info') + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return SpecialInfoFormatter.format_special_info(result) + return wrapper + return decorator + + @staticmethod + def database_info() -> Callable: + """Decorator to format database info. + + Usage: + @special_info_decorators.database_info() + def get_db_info(self): + return self.extractor.get('movie_db') + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return SpecialInfoFormatter.format_database_info(result) + return wrapper + return decorator + + +# Singleton instance +special_info_decorators = SpecialInfoDecorators() diff --git a/renamer/formatters/text_decorators.py b/renamer/formatters/text_decorators.py new file mode 100644 index 0000000..ceff529 --- /dev/null +++ b/renamer/formatters/text_decorators.py @@ -0,0 +1,130 @@ +"""Text formatting decorators. + +Provides decorator versions of TextFormatter methods: + + @text_decorators.bold() + def get_title(self): + return self.title +""" + +from functools import wraps +from typing import Callable +from .text_formatter import TextFormatter + + +class TextDecorators: + """Text styling and color decorators.""" + + @staticmethod + def bold() -> Callable: + """Decorator to make text bold.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.bold(str(result)) + return wrapper + return decorator + + @staticmethod + def italic() -> Callable: + """Decorator to make text italic.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.italic(str(result)) + return wrapper + return decorator + + @staticmethod + def green() -> Callable: + """Decorator to color text green.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.green(str(result)) + return wrapper + return decorator + + @staticmethod + def yellow() -> Callable: + """Decorator to color text yellow.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.yellow(str(result)) + return wrapper + return decorator + + @staticmethod + def cyan() -> Callable: + """Decorator to color text cyan.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.cyan(str(result)) + return wrapper + return decorator + + @staticmethod + def magenta() -> Callable: + """Decorator to color text magenta.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.magenta(str(result)) + return wrapper + return decorator + + @staticmethod + def red() -> Callable: + """Decorator to color text red.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.red(str(result)) + return wrapper + return decorator + + @staticmethod + def orange() -> Callable: + """Decorator to color text orange.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.orange(str(result)) + return wrapper + return decorator + + @staticmethod + def uppercase() -> Callable: + """Decorator to convert text to uppercase.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.uppercase(str(result)) + return wrapper + return decorator + + @staticmethod + def lowercase() -> Callable: + """Decorator to convert text to lowercase.""" + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args, **kwargs) -> str: + result = func(*args, **kwargs) + return TextFormatter.lowercase(str(result)) + return wrapper + return decorator + + +# Singleton instance +text_decorators = TextDecorators() diff --git a/renamer/formatters/text_formatter.py b/renamer/formatters/text_formatter.py index 7b4109c..d9b8ec5 100644 --- a/renamer/formatters/text_formatter.py +++ b/renamer/formatters/text_formatter.py @@ -78,6 +78,10 @@ class TextFormatter: def yellow(text: str) -> str: return f"[yellow]{text}[/yellow]" + @staticmethod + def orange(text: str) -> str: + return f"[orange]{text}[/orange]" + @staticmethod def magenta(text: str) -> str: return f"[magenta]{text}[/magenta]" diff --git a/renamer/test/test_decorators.py b/renamer/test/test_decorators.py new file mode 100644 index 0000000..fff92fb --- /dev/null +++ b/renamer/test/test_decorators.py @@ -0,0 +1,186 @@ +"""Tests for formatter decorators.""" + +import pytest +from renamer.formatters import ( + date_decorators, + special_info_decorators, + text_decorators, + conditional_decorators +) + + +class TestDateDecorators: + """Test date formatting decorators.""" + + def test_modification_date_decorator(self): + """Test @date_decorators.modification_date() decorator.""" + class TestClass: + def __init__(self, mtime): + self.mtime = mtime + + @date_decorators.modification_date() + def get_mtime(self): + return self.mtime + + # Test with a known timestamp (2020-01-01 00:00:00 UTC) + obj = TestClass(1577836800.0) + result = obj.get_mtime() + assert "2020-01-01" in result # Date part should be present + + +class TestSpecialInfoDecorators: + """Test special info formatting decorators.""" + + def test_special_info_decorator(self): + """Test @special_info_decorators.special_info() decorator.""" + class TestClass: + def __init__(self, special_info): + self.special_info = special_info + + @special_info_decorators.special_info() + def get_special_info(self): + return self.special_info + + obj = TestClass(["Director's Cut", "Extended Edition"]) + assert obj.get_special_info() == "Director's Cut, Extended Edition" + + obj_none = TestClass(None) + assert obj_none.get_special_info() == "" + + def test_database_info_decorator(self): + """Test @special_info_decorators.database_info() decorator.""" + class TestClass: + def __init__(self, db_info): + self.db_info = db_info + + @special_info_decorators.database_info() + def get_db_info(self): + return self.db_info + + obj = TestClass(["tmdb", "12345"]) + assert obj.get_db_info() == "tmdbid-12345" + + obj_dict = TestClass({"name": "imdb", "id": "tt1234567"}) + assert obj_dict.get_db_info() == "imdbid-tt1234567" + + +class TestTextDecorators: + """Test text formatting decorators.""" + + def test_bold_decorator(self): + """Test @text_decorators.bold() decorator.""" + class TestClass: + @text_decorators.bold() + def get_text(self): + return "Hello" + + obj = TestClass() + assert obj.get_text() == "[bold]Hello[/bold]" + + def test_green_decorator(self): + """Test @text_decorators.green() decorator.""" + class TestClass: + @text_decorators.green() + def get_text(self): + return "Success" + + obj = TestClass() + assert obj.get_text() == "[green]Success[/green]" + + +class TestConditionalDecorators: + """Test conditional formatting decorators.""" + + def test_wrap_decorator_both_sides(self): + """Test @conditional_decorators.wrap() with both left and right.""" + class TestClass: + def __init__(self, order): + self.order = order + + @conditional_decorators.wrap("[", "] ") + def get_order(self): + return self.order + + obj = TestClass("01") + assert obj.get_order() == "[01] " + + obj_none = TestClass(None) + assert obj_none.get_order() == "" + + def test_wrap_decorator_prefix_only(self): + """Test @conditional_decorators.wrap() as prefix (right="").""" + class TestClass: + def __init__(self, source): + self.source = source + + @conditional_decorators.wrap(" ") + def get_source(self): + return self.source + + obj = TestClass("BDRip") + assert obj.get_source() == " BDRip" + + obj_none = TestClass(None) + assert obj_none.get_source() == "" + + def test_wrap_decorator_suffix_only(self): + """Test @conditional_decorators.wrap() as suffix (left="").""" + class TestClass: + def __init__(self, hdr): + self.hdr = hdr + + @conditional_decorators.wrap("", ",") + def get_hdr(self): + return self.hdr + + obj = TestClass("HDR") + assert obj.get_hdr() == "HDR," + + obj_none = TestClass(None) + assert obj_none.get_hdr() == "" + + def test_replace_slashes_decorator(self): + """Test @conditional_decorators.replace_slashes() decorator.""" + class TestClass: + def __init__(self, title): + self.title = title + + @conditional_decorators.replace_slashes() + def get_title(self): + return self.title + + obj = TestClass("Movie/Title\\Test") + assert obj.get_title() == "Movie-Title-Test" + + def test_default_decorator(self): + """Test @conditional_decorators.default() decorator.""" + class TestClass: + def __init__(self, title): + self.title = title + + @conditional_decorators.default("Unknown Title") + def get_title(self): + return self.title + + obj = TestClass(None) + assert obj.get_title() == "Unknown Title" + + obj_with_title = TestClass("Movie Title") + assert obj_with_title.get_title() == "Movie Title" + + def test_chained_decorators(self): + """Test chaining multiple decorators.""" + class TestClass: + def __init__(self, title): + self.title = title + + @conditional_decorators.replace_slashes() + @conditional_decorators.default("Unknown Title") + def get_title(self): + return self.title + + obj = TestClass("Movie/Title") + assert obj.get_title() == "Movie-Title" + + obj_none = TestClass(None) + assert obj_none.get_title() == "Unknown Title" diff --git a/renamer/test/test_proposed_name_formatter.py b/renamer/test/test_proposed_name_formatter.py new file mode 100644 index 0000000..911f666 --- /dev/null +++ b/renamer/test/test_proposed_name_formatter.py @@ -0,0 +1,195 @@ +"""Tests for ProposedNameFormatter with decorator pattern.""" + +import pytest +from pathlib import Path +from renamer.formatters.proposed_name_formatter import ProposedNameFormatter + + +class TestProposedNameFormatter: + """Test ProposedNameFormatter with decorator pattern.""" + + def test_basic_formatting(self): + """Test basic filename formatting with all fields.""" + extractor = { + 'order': '01', + 'title': 'Movie Title', + 'year': 2020, + 'source': 'BDRip', + 'frame_class': '1080p', + 'hdr': 'HDR', + 'audio_langs': 'ukr,eng', + 'special_info': ["Director's Cut"], + 'movie_db': ['tmdb', '12345'], + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + assert '[01]' in result + assert 'Movie Title' in result + assert '(2020)' in result + assert 'BDRip' in result + assert '1080p' in result + assert 'HDR' in result + assert 'ukr,eng' in result + assert "Director's Cut" in result + assert 'tmdbid-12345' in result + assert '.mkv' in result + + def test_minimal_formatting(self): + """Test formatting with minimal fields.""" + extractor = { + 'title': 'Simple Movie', + 'year': 2020, + 'extension': 'mp4' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + assert 'Simple Movie' in result + assert '(2020)' in result + assert '.mp4' in result + assert '[01]' not in result # No order + + def test_title_slash_replacement(self): + """Test that slashes in title are replaced with dashes.""" + extractor = { + 'title': 'Movie/Title\\Test', + 'year': 2020, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + assert 'Movie-Title-Test' in result + assert '/' not in result + assert '\\' not in result + + def test_none_title(self): + """Test formatting when title is None (extractor should provide default).""" + extractor = { + 'title': None, + 'year': 2020, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + # Since title is None, it won't appear (unless extractor provides default) + assert result is not None + + def test_none_extension(self): + """Test formatting when extension is None (extractor should provide default).""" + extractor = { + 'title': 'Movie', + 'year': 2020, + 'extension': None + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + # Extension handling depends on extractor default + assert result is not None + + def test_special_info_list_formatting(self): + """Test special info list is formatted correctly.""" + extractor = { + 'title': 'Movie', + 'year': 2020, + 'special_info': ['Extended Edition', 'Remastered'], + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + assert 'Extended Edition, Remastered' in result + + def test_database_info_formatting(self): + """Test database info is formatted correctly.""" + extractor = { + 'title': 'Movie', + 'year': 2020, + 'movie_db': {'name': 'imdb', 'id': 'tt1234567'}, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + assert 'imdbid-tt1234567' in result + + def test_str_method(self): + """Test __str__ method returns same as rename_line().""" + extractor = { + 'title': 'Movie', + 'year': 2020, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + assert str(formatter) == formatter.rename_line + + def test_formatted_display_matching_name(self): + """Test rename_line_formatted when filename matches proposed name.""" + extractor = { + 'title': 'Movie', + 'year': 2020, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + proposed = str(formatter) + file_path = Path(proposed) + + result = formatter.rename_line_formatted(file_path) + assert '>>' in result + assert '<<' in result + assert '[green]' in result + + def test_formatted_display_different_name(self): + """Test rename_line_formatted when filename differs from proposed name.""" + extractor = { + 'title': 'Movie', + 'year': 2020, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + file_path = Path('different_name.mkv') + + result = formatter.rename_line_formatted(file_path) + assert '>>' in result + assert '<<' in result + + def test_year_formatting(self): + """Test year is wrapped in parentheses.""" + extractor = { + 'title': 'Movie', + 'year': 2020, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + assert '(2020)' in result + + def test_no_year(self): + """Test formatting when no year provided.""" + extractor = { + 'title': 'Movie', + 'year': None, + 'extension': 'mkv' + } + + formatter = ProposedNameFormatter(extractor) + result = formatter.rename_line + + # Should not have empty parentheses + assert '()' not in result diff --git a/uv.lock b/uv.lock index d324975..6717c84 100644 --- a/uv.lock +++ b/uv.lock @@ -462,7 +462,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.6.0" +version = "0.6.1" source = { editable = "." } dependencies = [ { name = "langcodes" },