chore: Bump version to 0.6.1 and update decorators to use new cache system
This commit is contained in:
BIN
dist/renamer-0.6.1-py3-none-any.whl
vendored
Normal file
BIN
dist/renamer-0.6.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "renamer"
|
name = "renamer"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
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"
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class DefaultExtractor:
|
|||||||
|
|
||||||
def extract_extension(self) -> Optional[str]:
|
def extract_extension(self) -> Optional[str]:
|
||||||
"""Return file extension. Returns None as no extension is available."""
|
"""Return file extension. Returns None as no extension is available."""
|
||||||
return None
|
return "ext"
|
||||||
|
|
||||||
def extract_tmdb_url(self) -> Optional[str]:
|
def extract_tmdb_url(self) -> Optional[str]:
|
||||||
"""Return TMDB URL. Returns None as no TMDB URL is available."""
|
"""Return TMDB URL. Returns None as no TMDB URL is available."""
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ file system metadata such as size, timestamps, paths, and extensions.
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from ..decorators import cached_method
|
from ..cache import cached_method
|
||||||
|
|
||||||
# Set up logging conditionally
|
# Set up logging conditionally
|
||||||
if os.getenv('FORMATTER_LOG', '0') == '1':
|
if os.getenv('FORMATTER_LOG', '0') == '1':
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from ..constants import (
|
|||||||
is_valid_year,
|
is_valid_year,
|
||||||
CYRILLIC_TO_ENGLISH
|
CYRILLIC_TO_ENGLISH
|
||||||
)
|
)
|
||||||
from ..decorators import cached_method
|
from ..cache import cached_method
|
||||||
from ..utils.pattern_utils import PatternExtractor
|
from ..utils.pattern_utils import PatternExtractor
|
||||||
import langcodes
|
import langcodes
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
from pymediainfo import MediaInfo
|
from pymediainfo import MediaInfo
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from ..constants import FRAME_CLASSES, MEDIA_TYPES
|
from ..constants import FRAME_CLASSES, MEDIA_TYPES
|
||||||
from ..decorators import cached_method
|
from ..cache import cached_method
|
||||||
import langcodes
|
import langcodes
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import mutagen
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ..constants import MEDIA_TYPES
|
from ..constants import MEDIA_TYPES
|
||||||
from ..decorators import cached_method
|
from ..cache import cached_method
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ from .track_formatter import TrackFormatter
|
|||||||
from .special_info_formatter import SpecialInfoFormatter
|
from .special_info_formatter import SpecialInfoFormatter
|
||||||
from .formatter import FormatterApplier
|
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__ = [
|
__all__ = [
|
||||||
# Base classes
|
# Base classes
|
||||||
'Formatter',
|
'Formatter',
|
||||||
@@ -41,4 +47,14 @@ __all__ = [
|
|||||||
'TrackFormatter',
|
'TrackFormatter',
|
||||||
'SpecialInfoFormatter',
|
'SpecialInfoFormatter',
|
||||||
'FormatterApplier',
|
'FormatterApplier',
|
||||||
|
|
||||||
|
# Decorator instances and classes
|
||||||
|
'date_decorators',
|
||||||
|
'DateDecorators',
|
||||||
|
'special_info_decorators',
|
||||||
|
'SpecialInfoDecorators',
|
||||||
|
'text_decorators',
|
||||||
|
'TextDecorators',
|
||||||
|
'conditional_decorators',
|
||||||
|
'ConditionalDecorators',
|
||||||
]
|
]
|
||||||
88
renamer/formatters/conditional_decorators.py
Normal file
88
renamer/formatters/conditional_decorators.py
Normal file
@@ -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()
|
||||||
37
renamer/formatters/date_decorators.py
Normal file
37
renamer/formatters/date_decorators.py
Normal file
@@ -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()
|
||||||
@@ -1,37 +1,99 @@
|
|||||||
from rich.markup import escape
|
from rich.markup import escape
|
||||||
from .text_formatter import TextFormatter
|
from .special_info_decorators import special_info_decorators
|
||||||
from .date_formatter import DateFormatter
|
from .conditional_decorators import conditional_decorators
|
||||||
from .special_info_formatter import SpecialInfoFormatter
|
from .text_decorators import text_decorators
|
||||||
|
|
||||||
|
|
||||||
class ProposedNameFormatter:
|
class ProposedNameFormatter:
|
||||||
"""Class for formatting proposed filenames"""
|
"""Class for formatting proposed filenames using decorator pattern with properties."""
|
||||||
|
|
||||||
def __init__(self, extractor):
|
def __init__(self, extractor):
|
||||||
"""Initialize with media extractor data"""
|
"""Initialize with media extractor data"""
|
||||||
|
self._extractor = extractor
|
||||||
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"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Convert the proposed name to string"""
|
"""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:
|
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("\\", "-")
|
return result.replace("/", "-").replace("\\", "-")
|
||||||
|
|
||||||
def rename_line_formatted(self, file_path) -> str:
|
def rename_line_formatted(self, file_path) -> str:
|
||||||
"""Format the proposed name for display with color"""
|
"""Format the proposed name for display with color"""
|
||||||
proposed = escape(str(self))
|
|
||||||
if file_path.name == str(self):
|
if file_path.name == str(self):
|
||||||
return f">> {TextFormatter.green(proposed)} <<"
|
return self.rename_line_similar
|
||||||
return f">> {TextFormatter.bold_yellow(proposed)} <<"
|
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))
|
||||||
54
renamer/formatters/special_info_decorators.py
Normal file
54
renamer/formatters/special_info_decorators.py
Normal file
@@ -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()
|
||||||
130
renamer/formatters/text_decorators.py
Normal file
130
renamer/formatters/text_decorators.py
Normal file
@@ -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()
|
||||||
@@ -78,6 +78,10 @@ class TextFormatter:
|
|||||||
def yellow(text: str) -> str:
|
def yellow(text: str) -> str:
|
||||||
return f"[yellow]{text}[/yellow]"
|
return f"[yellow]{text}[/yellow]"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def orange(text: str) -> str:
|
||||||
|
return f"[orange]{text}[/orange]"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def magenta(text: str) -> str:
|
def magenta(text: str) -> str:
|
||||||
return f"[magenta]{text}[/magenta]"
|
return f"[magenta]{text}[/magenta]"
|
||||||
|
|||||||
186
renamer/test/test_decorators.py
Normal file
186
renamer/test/test_decorators.py
Normal file
@@ -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"
|
||||||
195
renamer/test/test_proposed_name_formatter.py
Normal file
195
renamer/test/test_proposed_name_formatter.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user