diff --git a/INSTALL.md b/INSTALL.md index edc95ca..004aa88 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -24,8 +24,11 @@ powershell -c "irm https://astral.sh/uv/install.sh | iex" #### Install Renamer ```bash -# From the built wheel (if available) -uv tool install dist/renamer-0.2.0-py3-none-any.whl +# One-command install from remote wheel +uv tool install https://git.shadoll.dev/sha/renamer/raw/branch/main/dist/renamer-0.2.4-py3-none-any.whl + +# Or from local wheel (if downloaded) +uv tool install dist/renamer-0.2.4-py3-none-any.whl # Or from PyPI (when published) uv tool install renamer diff --git a/dist/renamer-0.2.5-py3-none-any.whl b/dist/renamer-0.2.5-py3-none-any.whl new file mode 100644 index 0000000..52c6b3c Binary files /dev/null and b/dist/renamer-0.2.5-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index 1e6bd4c..a9fb251 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "renamer" -version = "0.2.4" +version = "0.2.5" description = "Terminal-based media file renamer and metadata viewer" readme = "README.md" requires-python = ">=3.11" diff --git a/renamer/app.py b/renamer/app.py index c549166..cc4583d 100644 --- a/renamer/app.py +++ b/renamer/app.py @@ -1,9 +1,12 @@ from textual.app import App, ComposeResult from textual.widgets import Tree, Static, Footer, LoadingIndicator from textual.containers import Horizontal, Container, ScrollableContainer, Vertical +from rich.markup import escape from pathlib import Path import threading import time +import logging +import os from .constants import MEDIA_TYPES from .screens import OpenScreen, HelpScreen, RenameConfirmScreen @@ -13,6 +16,14 @@ from .formatters.proposed_name_formatter import ProposedNameFormatter from .formatters.text_formatter import TextFormatter +# Set up logging conditionally +if os.getenv('FORMATTER_LOG', '0') == '1': + logging.basicConfig(filename='formatter.log', level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') +else: + logging.basicConfig(level=logging.CRITICAL) # Disable logging + + class RenamerApp(App): CSS = """ #left { @@ -78,12 +89,13 @@ class RenamerApp(App): if item.is_dir(): if item.name.startswith(".") or item.name == "lost+found": continue - subnode = node.add(item.name, data=item) + subnode = node.add(escape(item.name), data=item) self.build_tree(item, subnode) elif item.is_file() and item.suffix.lower() in { f".{ext}" for ext in MEDIA_TYPES }: - node.add(item.name, data=item) + logging.info(f"Adding file to tree: {item.name!r} (full path: {item})") + node.add(escape(item.name), data=item) except PermissionError: pass except PermissionError: @@ -211,7 +223,7 @@ class RenamerApp(App): node = find_node(tree.root) if node: - node.label = new_path.name + node.label = escape(new_path.name) node.data = new_path # If this node is currently selected, refresh the details if tree.cursor_node == node: diff --git a/renamer/extractors/fileinfo_extractor.py b/renamer/extractors/fileinfo_extractor.py index 192bf0e..e93f3cc 100644 --- a/renamer/extractors/fileinfo_extractor.py +++ b/renamer/extractors/fileinfo_extractor.py @@ -1,4 +1,13 @@ from pathlib import Path +import logging +import os + +# Set up logging conditionally +if os.getenv('FORMATTER_LOG', '0') == '1': + logging.basicConfig(filename='formatter.log', level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') +else: + logging.basicConfig(level=logging.CRITICAL) # Disable logging class FileInfoExtractor: @@ -10,6 +19,7 @@ class FileInfoExtractor: self._modification_time = file_path.stat().st_mtime self._file_name = file_path.name self._file_path = str(file_path) + logging.info(f"FileInfoExtractor: file_name={self._file_name!r}, file_path={self._file_path!r}") def extract_size(self) -> int: """Extract file size in bytes""" diff --git a/renamer/extractors/filename_extractor.py b/renamer/extractors/filename_extractor.py index 105feac..69c0426 100644 --- a/renamer/extractors/filename_extractor.py +++ b/renamer/extractors/filename_extractor.py @@ -11,6 +11,18 @@ class FilenameExtractor: def __init__(self, file_path: Path): self.file_path = file_path self.file_name = file_path.name + self.file_name = self._normalize_cyrillic(self.file_name) + + def _normalize_cyrillic(self, text: str) -> str: + """Normalize Cyrillic characters to English equivalents for parsing""" + replacements = { + 'р': 'p', + 'і': 'i', + # Add more as needed + } + for cyr, eng in replacements.items(): + text = text.replace(cyr, eng) + return text def _get_frame_class_from_height(self, height: int) -> str | None: """Get frame class from video height using FRAME_CLASSES constant""" diff --git a/renamer/extractors/mediainfo_extractor.py b/renamer/extractors/mediainfo_extractor.py index 5dd42d4..f8a11aa 100644 --- a/renamer/extractors/mediainfo_extractor.py +++ b/renamer/extractors/mediainfo_extractor.py @@ -73,10 +73,10 @@ class MediaInfoExtractor: return 'HDR' return None - def extract_audio_langs(self) -> str: + def extract_audio_langs(self) -> str | None: """Extract audio languages from media info""" if not self.audio_tracks: - return '' + return None langs = [] for a in self.audio_tracks: lang_code = getattr(a, 'language', 'und').lower() diff --git a/renamer/formatters/extension_extractor.py b/renamer/formatters/extension_extractor.py deleted file mode 100644 index 0891b66..0000000 --- a/renamer/formatters/extension_extractor.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path -from ..constants import MEDIA_TYPES - - -class ExtensionExtractor: - """Class for extracting extension information""" - - @staticmethod - def get_extension_name(file_path: Path) -> str: - """Get extension name without dot""" - return file_path.suffix.lower().lstrip('.') - - @staticmethod - def get_extension_description(ext_name: str) -> str: - """Get description for extension""" - return MEDIA_TYPES.get(ext_name, {}).get('description', f'Unknown extension .{ext_name}') \ No newline at end of file diff --git a/renamer/formatters/formatter.py b/renamer/formatters/formatter.py index 89e29b1..af76133 100644 --- a/renamer/formatters/formatter.py +++ b/renamer/formatters/formatter.py @@ -41,6 +41,7 @@ class FormatterApplier: TrackFormatter.format_audio_track, TrackFormatter.format_subtitle_track, SpecialInfoFormatter.format_special_info, + SpecialInfoFormatter.format_database_info, # Text formatters second (transform text content) TextFormatter.uppercase, diff --git a/renamer/formatters/media_formatter.py b/renamer/formatters/media_formatter.py index 4c0fd28..d0262a2 100644 --- a/renamer/formatters/media_formatter.py +++ b/renamer/formatters/media_formatter.py @@ -1,7 +1,7 @@ from pathlib import Path +from rich.markup import escape from .size_formatter import SizeFormatter from .date_formatter import DateFormatter -from .extension_extractor import ExtensionExtractor from .extension_formatter import ExtensionFormatter from .text_formatter import TextFormatter from .track_formatter import TrackFormatter @@ -40,7 +40,7 @@ class MediaFormatter: "group": "File Info", "label": "Path", "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("file_path", "FileInfo"), + "value": escape(str(self.extractor.get("file_path", "FileInfo"))), "display_formatters": [TextFormatter.blue], }, { @@ -54,7 +54,7 @@ class MediaFormatter: "group": "File Info", "label": "Name", "label_formatters": [TextFormatter.bold], - "value": self.extractor.get("file_name", "FileInfo"), + "value": escape(str(self.extractor.get("file_name", "FileInfo"))), "display_formatters": [TextFormatter.cyan], }, { @@ -283,26 +283,77 @@ class MediaFormatter: def selected_data(self) -> list[str]: """Return formatted selected data string""" + import logging + import os + if os.getenv("FORMATTER_LOG"): + frame_class = self.extractor.get("frame_class") + audio_langs = self.extractor.get("audio_langs") + logging.info(f"Selected data - frame_class: {frame_class!r}, audio_langs: {audio_langs!r}") + # Also check from Filename source + frame_class_filename = self.extractor.get("frame_class", "Filename") + audio_langs_filename = self.extractor.get("audio_langs", "Filename") + logging.info(f"From Filename - frame_class: {frame_class_filename!r}, audio_langs: {audio_langs_filename!r}") data = [ { "label": "Selected Data", "label_formatters": [TextFormatter.bold, TextFormatter.uppercase], }, + { + "label": "Order", + "label_formatters": [TextFormatter.bold, TextFormatter.blue], + "value": self.extractor.get("order") or "", + "value_formatters": [TextFormatter.yellow], + }, { "label": "Title", - "label_formatters": [TextFormatter.bold, TextFormatter.yellow], + "label_formatters": [TextFormatter.bold, TextFormatter.blue], "value": self.extractor.get("title") or "", - "value_formatters": [TextFormatter.blue], + "value_formatters": [TextFormatter.yellow], + }, + { + "label": "Year", + "label_formatters": [TextFormatter.bold, TextFormatter.blue], + "value": self.extractor.get("year") or "", + "value_formatters": [TextFormatter.yellow, DateFormatter.format_year], }, { "label": "Special info", - "label_formatters": [TextFormatter.bold], + "label_formatters": [TextFormatter.bold, TextFormatter.blue], "value": self.extractor.get("special_info") or "", "value_formatters": [ SpecialInfoFormatter.format_special_info, - TextFormatter.blue, + TextFormatter.yellow, ], - "display_formatters": [TextFormatter.yellow], }, + { + "label": "Source", + "label_formatters": [TextFormatter.bold, TextFormatter.blue], + "value": self.extractor.get("source") or "", + "value_formatters": [TextFormatter.yellow], + }, + { + "label": "Frame class", + "label_formatters": [TextFormatter.bold, TextFormatter.blue], + "value": self.extractor.get("frame_class") or "", + "value_formatters": [TextFormatter.yellow], + }, + { + "label": "HDR", + "label_formatters": [TextFormatter.bold, TextFormatter.blue], + "value": self.extractor.get("hdr") or "", + "value_formatters": [TextFormatter.yellow], + }, + { + "label": "Audio langs", + "label_formatters": [TextFormatter.bold, TextFormatter.blue], + "value": self.extractor.get("audio_langs") or "", + "value_formatters": [TextFormatter.yellow], + }, + { + "label": "DBid", + "label_formatters": [TextFormatter.bold, TextFormatter.blue], + "value": self.extractor.get("movie_db") or "", + "value_formatters": [SpecialInfoFormatter.format_database_info, TextFormatter.yellow], + } ] return FormatterApplier.format_data_items(data) diff --git a/renamer/formatters/proposed_name_formatter.py b/renamer/formatters/proposed_name_formatter.py index ab910ae..754ecae 100644 --- a/renamer/formatters/proposed_name_formatter.py +++ b/renamer/formatters/proposed_name_formatter.py @@ -1,3 +1,4 @@ +from rich.markup import escape from .text_formatter import TextFormatter from .date_formatter import DateFormatter from .special_info_formatter import SpecialInfoFormatter @@ -16,7 +17,7 @@ class ProposedNameFormatter: 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.__special_info = f" [{SpecialInfoFormatter.format_special_info(extractor.get('special_info'))}]" if extractor.get("special_info") else "" self.__extension = extractor.get("extension") or "ext" def __str__(self) -> str: @@ -28,6 +29,7 @@ class ProposedNameFormatter: 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(str(self))} <<" - return f">> {TextFormatter.bold_yellow(str(self))} <<" + return f">> {TextFormatter.green(proposed)} <<" + return f">> {TextFormatter.bold_yellow(proposed)} <<" diff --git a/renamer/formatters/special_info_formatter.py b/renamer/formatters/special_info_formatter.py index 617e4e1..ee2cc68 100644 --- a/renamer/formatters/special_info_formatter.py +++ b/renamer/formatters/special_info_formatter.py @@ -8,4 +8,28 @@ class SpecialInfoFormatter: # Filter out None values and ensure all items are strings filtered = [str(item) for item in special_info if item is not None] return ", ".join(filtered) - return special_info or "" \ No newline at end of file + return special_info or "" + + @staticmethod + def format_database_info(database_info): + """Format database info dictionary or tuple/list into a string""" + import logging + import os + if os.getenv("FORMATTER_LOG"): + logging.info(f"format_database_info called with: {database_info!r} (type: {type(database_info)})") + if isinstance(database_info, dict) and 'name' in database_info and 'id' in database_info: + db_name = database_info['name'] + db_id = database_info['id'] + result = f"{db_name}id-{db_id}" + if os.getenv("FORMATTER_LOG"): + logging.info(f"Formatted dict to: {result!r}") + return result + elif isinstance(database_info, (tuple, list)) and len(database_info) == 2: + db_name, db_id = database_info + result = f"{db_name}id-{db_id}" + if os.getenv("FORMATTER_LOG"): + logging.info(f"Formatted tuple/list to: {result!r}") + return result + if os.getenv("FORMATTER_LOG"): + logging.info("Returning 'Unknown'") + return "Unknown" \ No newline at end of file diff --git a/renamer/test/filenames/[01] A Turtle's Tale (2010) BDRip [1080р,ukr,eng] [tmdbid-49953].mkv b/renamer/test/filenames/[01] A Turtle's Tale (2010) BDRip [1080р,ukr,eng] [tmdbid-49953].mkv new file mode 100644 index 0000000..e69de29 diff --git a/renamer/test/filenames/[02] A Turtle's Tale 2. Sammy's Escape from Paradise (2012) [720p,ukr,eng] [tmdbid-113594].mkv b/renamer/test/filenames/[02] A Turtle's Tale 2. Sammy's Escape from Paradise (2012) [720p,ukr,eng] [tmdbid-113594].mkv new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock index fd88f24..b0a2782 100644 --- a/uv.lock +++ b/uv.lock @@ -164,7 +164,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.2.4" +version = "0.2.5" source = { editable = "." } dependencies = [ { name = "langcodes" },