feat: Bump version to 0.2.5, enhance logging, normalize Cyrillic characters, and improve database info formatting

This commit is contained in:
sHa
2025-12-27 02:55:34 +00:00
parent 95cbaee7fa
commit f2060b4a92
15 changed files with 136 additions and 37 deletions

View File

@@ -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

BIN
dist/renamer-0.2.5-py3-none-any.whl vendored Normal file

Binary file not shown.

View File

@@ -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"

View File

@@ -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:

View File

@@ -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"""

View File

@@ -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"""

View File

@@ -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()

View File

@@ -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}')

View File

@@ -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,

View File

@@ -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 "<None>",
"value_formatters": [TextFormatter.yellow],
},
{
"label": "Title",
"label_formatters": [TextFormatter.bold, TextFormatter.yellow],
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
"value": self.extractor.get("title") or "<None>",
"value_formatters": [TextFormatter.blue],
"value_formatters": [TextFormatter.yellow],
},
{
"label": "Year",
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
"value": self.extractor.get("year") or "<None>",
"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 "<None>",
"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 "<None>",
"value_formatters": [TextFormatter.yellow],
},
{
"label": "Frame class",
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
"value": self.extractor.get("frame_class") or "<None>",
"value_formatters": [TextFormatter.yellow],
},
{
"label": "HDR",
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
"value": self.extractor.get("hdr") or "<None>",
"value_formatters": [TextFormatter.yellow],
},
{
"label": "Audio langs",
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
"value": self.extractor.get("audio_langs") or "<None>",
"value_formatters": [TextFormatter.yellow],
},
{
"label": "DBid",
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
"value": self.extractor.get("movie_db") or "<None>",
"value_formatters": [SpecialInfoFormatter.format_database_info, TextFormatter.yellow],
}
]
return FormatterApplier.format_data_items(data)

View File

@@ -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)} <<"

View File

@@ -9,3 +9,27 @@ class SpecialInfoFormatter:
filtered = [str(item) for item in special_info if item is not None]
return ", ".join(filtered)
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"

2
uv.lock generated
View File

@@ -164,7 +164,7 @@ wheels = [
[[package]]
name = "renamer"
version = "0.2.4"
version = "0.2.5"
source = { editable = "." }
dependencies = [
{ name = "langcodes" },