feat: Bump version to 0.2.5, enhance logging, normalize Cyrillic characters, and improve database info formatting
This commit is contained in:
@@ -24,8 +24,11 @@ powershell -c "irm https://astral.sh/uv/install.sh | iex"
|
|||||||
|
|
||||||
#### Install Renamer
|
#### Install Renamer
|
||||||
```bash
|
```bash
|
||||||
# From the built wheel (if available)
|
# One-command install from remote wheel
|
||||||
uv tool install dist/renamer-0.2.0-py3-none-any.whl
|
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)
|
# Or from PyPI (when published)
|
||||||
uv tool install renamer
|
uv tool install renamer
|
||||||
|
|||||||
BIN
dist/renamer-0.2.5-py3-none-any.whl
vendored
Normal file
BIN
dist/renamer-0.2.5-py3-none-any.whl
vendored
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "renamer"
|
name = "renamer"
|
||||||
version = "0.2.4"
|
version = "0.2.5"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import Tree, Static, Footer, LoadingIndicator
|
from textual.widgets import Tree, Static, Footer, LoadingIndicator
|
||||||
from textual.containers import Horizontal, Container, ScrollableContainer, Vertical
|
from textual.containers import Horizontal, Container, ScrollableContainer, Vertical
|
||||||
|
from rich.markup import escape
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from .constants import MEDIA_TYPES
|
from .constants import MEDIA_TYPES
|
||||||
from .screens import OpenScreen, HelpScreen, RenameConfirmScreen
|
from .screens import OpenScreen, HelpScreen, RenameConfirmScreen
|
||||||
@@ -13,6 +16,14 @@ from .formatters.proposed_name_formatter import ProposedNameFormatter
|
|||||||
from .formatters.text_formatter import TextFormatter
|
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):
|
class RenamerApp(App):
|
||||||
CSS = """
|
CSS = """
|
||||||
#left {
|
#left {
|
||||||
@@ -78,12 +89,13 @@ class RenamerApp(App):
|
|||||||
if item.is_dir():
|
if item.is_dir():
|
||||||
if item.name.startswith(".") or item.name == "lost+found":
|
if item.name.startswith(".") or item.name == "lost+found":
|
||||||
continue
|
continue
|
||||||
subnode = node.add(item.name, data=item)
|
subnode = node.add(escape(item.name), data=item)
|
||||||
self.build_tree(item, subnode)
|
self.build_tree(item, subnode)
|
||||||
elif item.is_file() and item.suffix.lower() in {
|
elif item.is_file() and item.suffix.lower() in {
|
||||||
f".{ext}" for ext in MEDIA_TYPES
|
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:
|
except PermissionError:
|
||||||
pass
|
pass
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
@@ -211,7 +223,7 @@ class RenamerApp(App):
|
|||||||
|
|
||||||
node = find_node(tree.root)
|
node = find_node(tree.root)
|
||||||
if node:
|
if node:
|
||||||
node.label = new_path.name
|
node.label = escape(new_path.name)
|
||||||
node.data = new_path
|
node.data = new_path
|
||||||
# If this node is currently selected, refresh the details
|
# If this node is currently selected, refresh the details
|
||||||
if tree.cursor_node == node:
|
if tree.cursor_node == node:
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
from pathlib import Path
|
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:
|
class FileInfoExtractor:
|
||||||
@@ -10,6 +19,7 @@ class FileInfoExtractor:
|
|||||||
self._modification_time = file_path.stat().st_mtime
|
self._modification_time = file_path.stat().st_mtime
|
||||||
self._file_name = file_path.name
|
self._file_name = file_path.name
|
||||||
self._file_path = str(file_path)
|
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:
|
def extract_size(self) -> int:
|
||||||
"""Extract file size in bytes"""
|
"""Extract file size in bytes"""
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ class FilenameExtractor:
|
|||||||
def __init__(self, file_path: Path):
|
def __init__(self, file_path: Path):
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.file_name = file_path.name
|
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:
|
def _get_frame_class_from_height(self, height: int) -> str | None:
|
||||||
"""Get frame class from video height using FRAME_CLASSES constant"""
|
"""Get frame class from video height using FRAME_CLASSES constant"""
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ class MediaInfoExtractor:
|
|||||||
return 'HDR'
|
return 'HDR'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def extract_audio_langs(self) -> str:
|
def extract_audio_langs(self) -> str | None:
|
||||||
"""Extract audio languages from media info"""
|
"""Extract audio languages from media info"""
|
||||||
if not self.audio_tracks:
|
if not self.audio_tracks:
|
||||||
return ''
|
return None
|
||||||
langs = []
|
langs = []
|
||||||
for a in self.audio_tracks:
|
for a in self.audio_tracks:
|
||||||
lang_code = getattr(a, 'language', 'und').lower()
|
lang_code = getattr(a, 'language', 'und').lower()
|
||||||
|
|||||||
@@ -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}')
|
|
||||||
@@ -41,6 +41,7 @@ class FormatterApplier:
|
|||||||
TrackFormatter.format_audio_track,
|
TrackFormatter.format_audio_track,
|
||||||
TrackFormatter.format_subtitle_track,
|
TrackFormatter.format_subtitle_track,
|
||||||
SpecialInfoFormatter.format_special_info,
|
SpecialInfoFormatter.format_special_info,
|
||||||
|
SpecialInfoFormatter.format_database_info,
|
||||||
|
|
||||||
# Text formatters second (transform text content)
|
# Text formatters second (transform text content)
|
||||||
TextFormatter.uppercase,
|
TextFormatter.uppercase,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from rich.markup import escape
|
||||||
from .size_formatter import SizeFormatter
|
from .size_formatter import SizeFormatter
|
||||||
from .date_formatter import DateFormatter
|
from .date_formatter import DateFormatter
|
||||||
from .extension_extractor import ExtensionExtractor
|
|
||||||
from .extension_formatter import ExtensionFormatter
|
from .extension_formatter import ExtensionFormatter
|
||||||
from .text_formatter import TextFormatter
|
from .text_formatter import TextFormatter
|
||||||
from .track_formatter import TrackFormatter
|
from .track_formatter import TrackFormatter
|
||||||
@@ -40,7 +40,7 @@ class MediaFormatter:
|
|||||||
"group": "File Info",
|
"group": "File Info",
|
||||||
"label": "Path",
|
"label": "Path",
|
||||||
"label_formatters": [TextFormatter.bold],
|
"label_formatters": [TextFormatter.bold],
|
||||||
"value": self.extractor.get("file_path", "FileInfo"),
|
"value": escape(str(self.extractor.get("file_path", "FileInfo"))),
|
||||||
"display_formatters": [TextFormatter.blue],
|
"display_formatters": [TextFormatter.blue],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,7 @@ class MediaFormatter:
|
|||||||
"group": "File Info",
|
"group": "File Info",
|
||||||
"label": "Name",
|
"label": "Name",
|
||||||
"label_formatters": [TextFormatter.bold],
|
"label_formatters": [TextFormatter.bold],
|
||||||
"value": self.extractor.get("file_name", "FileInfo"),
|
"value": escape(str(self.extractor.get("file_name", "FileInfo"))),
|
||||||
"display_formatters": [TextFormatter.cyan],
|
"display_formatters": [TextFormatter.cyan],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -283,26 +283,77 @@ class MediaFormatter:
|
|||||||
|
|
||||||
def selected_data(self) -> list[str]:
|
def selected_data(self) -> list[str]:
|
||||||
"""Return formatted selected data string"""
|
"""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 = [
|
data = [
|
||||||
{
|
{
|
||||||
"label": "Selected Data",
|
"label": "Selected Data",
|
||||||
"label_formatters": [TextFormatter.bold, TextFormatter.uppercase],
|
"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": "Title",
|
||||||
"label_formatters": [TextFormatter.bold, TextFormatter.yellow],
|
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
|
||||||
"value": self.extractor.get("title") or "<None>",
|
"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": "Special info",
|
||||||
"label_formatters": [TextFormatter.bold],
|
"label_formatters": [TextFormatter.bold, TextFormatter.blue],
|
||||||
"value": self.extractor.get("special_info") or "<None>",
|
"value": self.extractor.get("special_info") or "<None>",
|
||||||
"value_formatters": [
|
"value_formatters": [
|
||||||
SpecialInfoFormatter.format_special_info,
|
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)
|
return FormatterApplier.format_data_items(data)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from rich.markup import escape
|
||||||
from .text_formatter import TextFormatter
|
from .text_formatter import TextFormatter
|
||||||
from .date_formatter import DateFormatter
|
from .date_formatter import DateFormatter
|
||||||
from .special_info_formatter import SpecialInfoFormatter
|
from .special_info_formatter import SpecialInfoFormatter
|
||||||
@@ -16,7 +17,7 @@ class ProposedNameFormatter:
|
|||||||
self.__frame_class = extractor.get("frame_class") or None
|
self.__frame_class = extractor.get("frame_class") or None
|
||||||
self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else ""
|
self.__hdr = f",{extractor.get('hdr')}" if extractor.get("hdr") else ""
|
||||||
self.__audio_langs = extractor.get("audio_langs") or None
|
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"
|
self.__extension = extractor.get("extension") or "ext"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@@ -28,6 +29,7 @@ class ProposedNameFormatter:
|
|||||||
|
|
||||||
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(str(self))} <<"
|
return f">> {TextFormatter.green(proposed)} <<"
|
||||||
return f">> {TextFormatter.bold_yellow(str(self))} <<"
|
return f">> {TextFormatter.bold_yellow(proposed)} <<"
|
||||||
|
|||||||
@@ -9,3 +9,27 @@ class SpecialInfoFormatter:
|
|||||||
filtered = [str(item) for item in special_info if item is not None]
|
filtered = [str(item) for item in special_info if item is not None]
|
||||||
return ", ".join(filtered)
|
return ", ".join(filtered)
|
||||||
return special_info or ""
|
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"
|
||||||
Reference in New Issue
Block a user