feat: Bump version to 0.3.1, update filename extraction logic, and enhance rename confirmation screen
This commit is contained in:
BIN
dist/renamer-0.3.1-py3-none-any.whl
vendored
Normal file
BIN
dist/renamer-0.3.1-py3-none-any.whl
vendored
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "renamer"
|
name = "renamer"
|
||||||
version = "0.2.12"
|
version = "0.3.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"
|
||||||
|
|||||||
@@ -185,8 +185,11 @@ class RenamerApp(App):
|
|||||||
extractor = MediaExtractor(node.data)
|
extractor = MediaExtractor(node.data)
|
||||||
proposed_formatter = ProposedNameFormatter(extractor)
|
proposed_formatter = ProposedNameFormatter(extractor)
|
||||||
new_name = str(proposed_formatter)
|
new_name = str(proposed_formatter)
|
||||||
|
logging.info(f"Proposed new name: {new_name!r} for file: {node.data}")
|
||||||
if new_name and new_name != node.data.name:
|
if new_name and new_name != node.data.name:
|
||||||
self.push_screen(RenameConfirmScreen(node.data, new_name))
|
self.push_screen(RenameConfirmScreen(node.data, new_name))
|
||||||
|
else:
|
||||||
|
self.notify("Proposed name is the same as current name; no rename needed.", severity="information", timeout=3)
|
||||||
|
|
||||||
async def action_expand(self):
|
async def action_expand(self):
|
||||||
tree = self.query_one("#file_tree", Tree)
|
tree = self.query_one("#file_tree", Tree)
|
||||||
|
|||||||
@@ -355,10 +355,16 @@ class FilenameExtractor:
|
|||||||
if not langs:
|
if not langs:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Count occurrences and format like mediainfo: "2ukr,eng"
|
# Count occurrences while preserving order of first appearance
|
||||||
lang_counts = Counter(langs)
|
lang_counts = {}
|
||||||
|
for lang in langs:
|
||||||
|
if lang not in lang_counts:
|
||||||
|
lang_counts[lang] = 0
|
||||||
|
lang_counts[lang] += 1
|
||||||
|
|
||||||
|
# Format like mediainfo: "2ukr,eng" preserving order
|
||||||
audio_langs = [f"{count}{lang}" if count > 1 else lang for lang, count in lang_counts.items()]
|
audio_langs = [f"{count}{lang}" if count > 1 else lang for lang, count in lang_counts.items()]
|
||||||
return ','.join(sorted(audio_langs))
|
return ','.join(audio_langs)
|
||||||
|
|
||||||
def extract_audio_tracks(self) -> list[dict]:
|
def extract_audio_tracks(self) -> list[dict]:
|
||||||
"""Extract audio track data from filename (simplified version with only language)"""
|
"""Extract audio track data from filename (simplified version with only language)"""
|
||||||
|
|||||||
@@ -17,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.__db_info = f" [{SpecialInfoFormatter.format_database_info(extractor.get('movie_db'))}]" if extractor.get("movie_db") 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.__extension = extractor.get("extension") or "ext"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Input, Button, Static
|
from textual.widgets import Input, Button, Static
|
||||||
from textual.containers import Vertical, Horizontal, Center, Container
|
from textual.containers import Vertical, Horizontal, Center, Container
|
||||||
from rich.markup import escape
|
from textual.markup import escape
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -97,18 +97,30 @@ class RenameConfirmScreen(Screen):
|
|||||||
#confirm_content {
|
#confirm_content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
Button {
|
# Button {
|
||||||
background: $surface;
|
# background: $surface;
|
||||||
border: solid $surface;
|
# border: solid $surface;
|
||||||
}
|
# }
|
||||||
Button:focus {
|
Button:focus {
|
||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
# color: $text-primary;
|
||||||
border: solid $primary;
|
# border: solid $primary;
|
||||||
}
|
}
|
||||||
#buttons {
|
#buttons {
|
||||||
align: center middle;
|
align: center middle;
|
||||||
}
|
}
|
||||||
|
#new_name_input {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1 0;
|
||||||
|
}
|
||||||
|
#new_name_display {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
#warning_content {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, old_path: Path, new_name: str):
|
def __init__(self, old_path: Path, new_name: str):
|
||||||
@@ -123,17 +135,25 @@ class RenameConfirmScreen(Screen):
|
|||||||
confirm_text = f"""
|
confirm_text = f"""
|
||||||
{TextFormatter.bold(TextFormatter.red("RENAME CONFIRMATION"))}
|
{TextFormatter.bold(TextFormatter.red("RENAME CONFIRMATION"))}
|
||||||
|
|
||||||
Current name: {TextFormatter.cyan(escape(self.old_path.name))}
|
RAW name: {escape(self.old_path.name)}
|
||||||
New name: {TextFormatter.green(escape(self.new_name))}
|
|
||||||
|
|
||||||
{TextFormatter.yellow("This action cannot be undone!")}
|
Current name: {TextFormatter.cyan(escape(self.old_path.name))}
|
||||||
|
Proposed name: {TextFormatter.green(escape(self.new_name))}
|
||||||
|
|
||||||
|
{TextFormatter.yellow("Edit the new name below:")}
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
warning_text = f"""
|
||||||
|
{TextFormatter.bold(TextFormatter.red("This action cannot be undone!"))}
|
||||||
Do you want to proceed with renaming?
|
Do you want to proceed with renaming?
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
with Center():
|
with Center():
|
||||||
with Vertical():
|
with Vertical():
|
||||||
yield Static(confirm_text, id="confirm_content", markup=True)
|
yield Static(confirm_text, id="confirm_content", markup=True)
|
||||||
|
yield Input(value=self.new_name, id="new_name_input", placeholder="New file name")
|
||||||
|
yield Static(f"{TextFormatter.green(escape(self.new_name))}", id="new_name_display", markup=True)
|
||||||
|
yield Static(warning_text, id="warning_content", markup=True)
|
||||||
with Horizontal(id="buttons"):
|
with Horizontal(id="buttons"):
|
||||||
yield Button("Rename (y)", id="rename")
|
yield Button("Rename (y)", id="rename")
|
||||||
yield Button("Cancel (n)", id="cancel")
|
yield Button("Cancel (n)", id="cancel")
|
||||||
@@ -141,6 +161,15 @@ Do you want to proceed with renaming?
|
|||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
self.set_focus(self.query_one("#rename"))
|
self.set_focus(self.query_one("#rename"))
|
||||||
|
|
||||||
|
def on_input_changed(self, event):
|
||||||
|
if event.input.id == "new_name_input":
|
||||||
|
self.new_name = event.input.value
|
||||||
|
self.new_path = self.old_path.parent / self.new_name
|
||||||
|
# Update the display
|
||||||
|
from .formatters.text_formatter import TextFormatter
|
||||||
|
display = self.query_one("#new_name_display", Static)
|
||||||
|
display.update(f"{TextFormatter.green(escape(self.new_name))}")
|
||||||
|
|
||||||
def on_button_pressed(self, event):
|
def on_button_pressed(self, event):
|
||||||
if event.button.id == "rename":
|
if event.button.id == "rename":
|
||||||
try:
|
try:
|
||||||
@@ -156,10 +185,37 @@ Do you want to proceed with renaming?
|
|||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|
||||||
def on_key(self, event):
|
def on_key(self, event):
|
||||||
if event.key == "left":
|
current = self.focused
|
||||||
self.set_focus(self.query_one("#rename"))
|
if current and hasattr(current, 'id'):
|
||||||
elif event.key == "right":
|
if current.id == "new_name_input":
|
||||||
self.set_focus(self.query_one("#cancel"))
|
# When input is focused, let left/right move cursor, use up/down to change focus
|
||||||
|
if event.key == "up":
|
||||||
|
self.set_focus(self.query_one("#cancel"))
|
||||||
|
elif event.key == "down":
|
||||||
|
self.set_focus(self.query_one("#rename"))
|
||||||
|
elif current.id in ("rename", "cancel"):
|
||||||
|
if event.key == "left":
|
||||||
|
if current.id == "rename":
|
||||||
|
self.set_focus(self.query_one("#new_name_input"))
|
||||||
|
elif current.id == "cancel":
|
||||||
|
self.set_focus(self.query_one("#rename"))
|
||||||
|
elif event.key == "right":
|
||||||
|
if current.id == "new_name_input":
|
||||||
|
self.set_focus(self.query_one("#rename"))
|
||||||
|
elif current.id == "rename":
|
||||||
|
self.set_focus(self.query_one("#cancel"))
|
||||||
|
elif event.key == "up":
|
||||||
|
if current.id == "rename":
|
||||||
|
self.set_focus(self.query_one("#new_name_input"))
|
||||||
|
elif current.id == "cancel":
|
||||||
|
self.set_focus(self.query_one("#rename"))
|
||||||
|
elif event.key == "down":
|
||||||
|
if current.id == "new_name_input":
|
||||||
|
self.set_focus(self.query_one("#rename"))
|
||||||
|
elif current.id == "rename":
|
||||||
|
self.set_focus(self.query_one("#cancel"))
|
||||||
|
elif current.id == "cancel":
|
||||||
|
self.set_focus(self.query_one("#new_name_input"))
|
||||||
elif event.key == "y":
|
elif event.key == "y":
|
||||||
# Trigger rename
|
# Trigger rename
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user