feat: Update ToDo list with MKV metadata editing capabilities and bump version to 0.7.10

This commit is contained in:
sHa
2026-01-04 17:10:34 +00:00
parent 0c3a173819
commit 9b353a7e7e
5 changed files with 122 additions and 37 deletions

11
ToDo.md
View File

@@ -29,10 +29,13 @@ This file tracks future feature enhancements and improvements.
### Medium Priority
- [ ] **Metadata Editing Capabilities**
- Edit embedded metadata tags
- Batch editing support
- Validation and preview
- [ ] **MKV Metadata Editor with mkvpropedit**
- Fast metadata editing without re-encoding (using mkvpropedit)
- Edit container title from TMDB data
- Set audio/subtitle track languages from filename
- Set track names and flags
- Batch editing support with preview
- Validation before applying changes
- [ ] **Batch Rename Operations**
- Select multiple files

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

Binary file not shown.

View File

@@ -1,6 +1,6 @@
[project]
name = "renamer"
version = "0.7.9"
version = "0.7.10"
description = "Terminal-based media file renamer and metadata viewer"
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -232,20 +232,43 @@ class RenamerApp(App):
def on_tree_node_highlighted(self, event):
node = event.node
if node.data and isinstance(node.data, Path):
if node.data.is_dir():
# Check if path still exists
if not node.data.exists():
self._stop_loading_animation()
details = self.query_one("#details_technical", Static)
details.display = True
details_catalog = self.query_one("#details_catalog", Static)
details_catalog.display = False
details.update("Directory")
details.update(f"[red]Path no longer exists: {node.data.name}[/red]")
proposed = self.query_one("#proposed", Static)
proposed.update("")
return
try:
if node.data.is_dir():
self._stop_loading_animation()
details = self.query_one("#details_technical", Static)
details.display = True
details_catalog = self.query_one("#details_catalog", Static)
details_catalog.display = False
details.update("Directory")
proposed = self.query_one("#proposed", Static)
proposed.update("")
elif node.data.is_file():
self._start_loading_animation()
threading.Thread(
target=self._extract_and_show_details, args=(node.data,)
).start()
except (FileNotFoundError, OSError):
# Handle race condition where file was deleted between exists() check and is_file() call
self._stop_loading_animation()
details = self.query_one("#details_technical", Static)
details.display = True
details_catalog = self.query_one("#details_catalog", Static)
details_catalog.display = False
details.update(f"[red]Error accessing path: {node.data.name}[/red]")
proposed = self.query_one("#proposed", Static)
proposed.update("")
elif node.data.is_file():
self._start_loading_animation()
threading.Thread(
target=self._extract_and_show_details, args=(node.data,)
).start()
def _extract_and_show_details(self, file_path: Path):
try:
@@ -311,15 +334,30 @@ class RenamerApp(App):
# Get the directory to scan
path = node.data
if path.is_file():
# If it's a file, scan its parent directory
path = path.parent
# Find the parent node in the tree
# Check if the path still exists
if not path.exists():
self.notify(f"Path no longer exists: {path.name}", severity="error", timeout=3)
# Remove the node from the tree since the file/dir is gone
if node.parent:
node = node.parent
else:
self.notify("Cannot scan root level file", severity="warning", timeout=3)
return
node.remove()
return
try:
if path.is_file():
# If it's a file, scan its parent directory
path = path.parent
# Find the parent node in the tree
if node.parent:
node = node.parent
else:
self.notify("Cannot scan root level file", severity="warning", timeout=3)
return
except (FileNotFoundError, OSError) as e:
self.notify(f"Error accessing path: {e}", severity="error", timeout=3)
if node.parent:
node.remove()
return
# Clear the node and rescan
node.remove_children()
@@ -333,11 +371,20 @@ class RenamerApp(App):
async def action_refresh(self):
tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node
if node and node.data and isinstance(node.data, Path) and node.data.is_file():
self._start_loading_animation()
threading.Thread(
target=self._extract_and_show_details, args=(node.data,)
).start()
if node and node.data and isinstance(node.data, Path):
# Check if path still exists
if not node.data.exists():
self.notify(f"Path no longer exists: {node.data.name}", severity="error", timeout=3)
return
try:
if node.data.is_file():
self._start_loading_animation()
threading.Thread(
target=self._extract_and_show_details, args=(node.data,)
).start()
except (FileNotFoundError, OSError) as e:
self.notify(f"Error accessing file: {e}", severity="error", timeout=3)
async def action_help(self):
self.push_screen(HelpScreen())
@@ -410,25 +457,47 @@ By Category:"""
async def action_rename(self):
tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node
if node and node.data and isinstance(node.data, Path) and node.data.is_file():
# Get the proposed name from the extractor
extractor = MediaExtractor(node.data)
proposed_formatter = ProposedFilenameView(extractor)
new_name = str(proposed_formatter)
logging.info(f"Proposed new name: {new_name!r} for file: {node.data}")
# Always open rename dialog, even if names are the same (user might want to manually edit)
if new_name:
self.push_screen(RenameConfirmScreen(node.data, new_name))
if node and node.data and isinstance(node.data, Path):
# Check if file exists
if not node.data.exists():
self.notify(f"File no longer exists: {node.data.name}", severity="error", timeout=3)
return
try:
if node.data.is_file():
# Get the proposed name from the extractor
extractor = MediaExtractor(node.data)
proposed_formatter = ProposedFilenameView(extractor)
new_name = str(proposed_formatter)
logging.info(f"Proposed new name: {new_name!r} for file: {node.data}")
# Always open rename dialog, even if names are the same (user might want to manually edit)
if new_name:
self.push_screen(RenameConfirmScreen(node.data, new_name))
except (FileNotFoundError, OSError) as e:
self.notify(f"Error accessing file: {e}", severity="error", timeout=3)
async def action_convert(self):
"""Convert AVI/MPG/MPEG/WebM/MP4 file to MKV with metadata preservation."""
tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node
if not (node and node.data and isinstance(node.data, Path) and node.data.is_file()):
if not (node and node.data and isinstance(node.data, Path)):
self.notify("Please select a file first", severity="warning", timeout=3)
return
# Check if file exists
if not node.data.exists():
self.notify(f"File no longer exists: {node.data.name}", severity="error", timeout=3)
return
try:
if not node.data.is_file():
self.notify("Please select a file first", severity="warning", timeout=3)
return
except (FileNotFoundError, OSError) as e:
self.notify(f"Error accessing file: {e}", severity="error", timeout=3)
return
file_path = node.data
conversion_service = ConversionService()
@@ -466,10 +535,23 @@ By Category:"""
tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node
if not (node and node.data and isinstance(node.data, Path) and node.data.is_file()):
if not (node and node.data and isinstance(node.data, Path)):
self.notify("Please select a file first", severity="warning", timeout=3)
return
# Check if file exists
if not node.data.exists():
self.notify(f"File no longer exists: {node.data.name}", severity="error", timeout=3)
return
try:
if not node.data.is_file():
self.notify("Please select a file first", severity="warning", timeout=3)
return
except (FileNotFoundError, OSError) as e:
self.notify(f"Error accessing file: {e}", severity="error", timeout=3)
return
file_path = node.data
# Show confirmation screen

2
uv.lock generated
View File

@@ -462,7 +462,7 @@ wheels = [
[[package]]
name = "renamer"
version = "0.7.9"
version = "0.7.10"
source = { editable = "." }
dependencies = [
{ name = "langcodes" },