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 ### Medium Priority
- [ ] **Metadata Editing Capabilities** - [ ] **MKV Metadata Editor with mkvpropedit**
- Edit embedded metadata tags - Fast metadata editing without re-encoding (using mkvpropedit)
- Batch editing support - Edit container title from TMDB data
- Validation and preview - Set audio/subtitle track languages from filename
- Set track names and flags
- Batch editing support with preview
- Validation before applying changes
- [ ] **Batch Rename Operations** - [ ] **Batch Rename Operations**
- Select multiple files - 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] [project]
name = "renamer" name = "renamer"
version = "0.7.9" version = "0.7.10"
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"

View File

@@ -232,20 +232,43 @@ class RenamerApp(App):
def on_tree_node_highlighted(self, event): def on_tree_node_highlighted(self, event):
node = event.node node = event.node
if node.data and isinstance(node.data, Path): 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() self._stop_loading_animation()
details = self.query_one("#details_technical", Static) details = self.query_one("#details_technical", Static)
details.display = True details.display = True
details_catalog = self.query_one("#details_catalog", Static) details_catalog = self.query_one("#details_catalog", Static)
details_catalog.display = False 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 = self.query_one("#proposed", Static)
proposed.update("") 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): def _extract_and_show_details(self, file_path: Path):
try: try:
@@ -311,15 +334,30 @@ class RenamerApp(App):
# Get the directory to scan # Get the directory to scan
path = node.data path = node.data
if path.is_file():
# If it's a file, scan its parent directory # Check if the path still exists
path = path.parent if not path.exists():
# Find the parent node in the tree 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: if node.parent:
node = node.parent node.remove()
else: return
self.notify("Cannot scan root level file", severity="warning", timeout=3)
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 # Clear the node and rescan
node.remove_children() node.remove_children()
@@ -333,11 +371,20 @@ class RenamerApp(App):
async def action_refresh(self): async def action_refresh(self):
tree = self.query_one("#file_tree", Tree) tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node node = tree.cursor_node
if node and node.data and isinstance(node.data, Path) and node.data.is_file(): if node and node.data and isinstance(node.data, Path):
self._start_loading_animation() # Check if path still exists
threading.Thread( if not node.data.exists():
target=self._extract_and_show_details, args=(node.data,) self.notify(f"Path no longer exists: {node.data.name}", severity="error", timeout=3)
).start() 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): async def action_help(self):
self.push_screen(HelpScreen()) self.push_screen(HelpScreen())
@@ -410,25 +457,47 @@ By Category:"""
async def action_rename(self): async def action_rename(self):
tree = self.query_one("#file_tree", Tree) tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node node = tree.cursor_node
if node and node.data and isinstance(node.data, Path) and node.data.is_file(): if node and node.data and isinstance(node.data, Path):
# Get the proposed name from the extractor # Check if file exists
extractor = MediaExtractor(node.data) if not node.data.exists():
proposed_formatter = ProposedFilenameView(extractor) self.notify(f"File no longer exists: {node.data.name}", severity="error", timeout=3)
new_name = str(proposed_formatter) return
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) try:
if new_name: if node.data.is_file():
self.push_screen(RenameConfirmScreen(node.data, new_name)) # 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): async def action_convert(self):
"""Convert AVI/MPG/MPEG/WebM/MP4 file to MKV with metadata preservation.""" """Convert AVI/MPG/MPEG/WebM/MP4 file to MKV with metadata preservation."""
tree = self.query_one("#file_tree", Tree) tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node 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) self.notify("Please select a file first", severity="warning", timeout=3)
return 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 file_path = node.data
conversion_service = ConversionService() conversion_service = ConversionService()
@@ -466,10 +535,23 @@ By Category:"""
tree = self.query_one("#file_tree", Tree) tree = self.query_one("#file_tree", Tree)
node = tree.cursor_node 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) self.notify("Please select a file first", severity="warning", timeout=3)
return 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 file_path = node.data
# Show confirmation screen # Show confirmation screen

2
uv.lock generated
View File

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