mirror of
https://github.com/shadoll/moma.git
synced 2026-08-28 03:27:34 +00:00
feat: restructure renamer package and implement media extraction features
- Updated `pyproject.toml` to reflect new package structure. - Created `renamer/__init__.py` to initialize the package. - Implemented `RenamerApp` in `renamer/app.py` for the main application interface. - Added constants for video extensions in `renamer/constants.py`. - Developed `MediaExtractor` class in `renamer/extractor.py` for extracting metadata from media files. - Created various extractor classes in `renamer/extractors/` for handling filename, metadata, and media info extraction. - Added formatting classes in `renamer/formatters/` for displaying media information and proposed filenames. - Implemented utility functions in `renamer/utils.py` for detecting file types and extracting media track information. - Introduced `OpenScreen` in `renamer/screens.py` for user input of directory paths. - Enhanced error handling and user feedback throughout the application.
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Tree, Static, Footer, LoadingIndicator
|
||||
from textual.containers import Horizontal, Container, ScrollableContainer, Vertical
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
|
||||
from .constants import VIDEO_EXTENSIONS
|
||||
from .utils import get_media_tracks
|
||||
from .screens import OpenScreen
|
||||
from .extractor import MediaExtractor
|
||||
from .formatters.media_formatter import MediaFormatter
|
||||
|
||||
|
||||
class RenamerApp(App):
|
||||
CSS = """
|
||||
#left {
|
||||
width: 50%;
|
||||
padding: 1;
|
||||
}
|
||||
#right {
|
||||
width: 50%;
|
||||
padding: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
("o", "open", "Open directory"),
|
||||
("s", "scan", "Scan"),
|
||||
]
|
||||
|
||||
def __init__(self, scan_dir):
|
||||
super().__init__()
|
||||
self.scan_dir = Path(scan_dir) if scan_dir else None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal():
|
||||
with Container(id="left"):
|
||||
yield Tree("Files", id="file_tree")
|
||||
with Container(id="right"):
|
||||
with Vertical():
|
||||
yield LoadingIndicator(id="loading")
|
||||
with ScrollableContainer(id="details_container"):
|
||||
yield Static("Select a file to view details", id="details", markup=True)
|
||||
yield Static("", id="proposed", markup=True)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
loading = self.query_one("#loading", LoadingIndicator)
|
||||
loading.display = False
|
||||
self.scan_files()
|
||||
|
||||
def scan_files(self):
|
||||
if not self.scan_dir.exists() or not self.scan_dir.is_dir():
|
||||
details = self.query_one("#details", Static)
|
||||
details.update("Error: Directory does not exist or is not a directory")
|
||||
return
|
||||
tree = self.query_one("#file_tree", Tree)
|
||||
tree.clear()
|
||||
tree.root.add(".", data=self.scan_dir)
|
||||
self.build_tree(self.scan_dir, tree.root)
|
||||
tree.root.expand()
|
||||
self.set_focus(tree)
|
||||
|
||||
def build_tree(self, path: Path, node):
|
||||
try:
|
||||
for item in sorted(path.iterdir()):
|
||||
try:
|
||||
if item.is_dir():
|
||||
subnode = node.add(item.name, data=item)
|
||||
self.build_tree(item, subnode)
|
||||
elif item.is_file() and item.suffix.lower() in VIDEO_EXTENSIONS:
|
||||
node.add(item.name, data=item)
|
||||
except PermissionError:
|
||||
pass
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
def _start_loading_animation(self):
|
||||
loading = self.query_one("#loading", LoadingIndicator)
|
||||
loading.display = True
|
||||
details = self.query_one("#details", Static)
|
||||
details.update("Retrieving media data")
|
||||
proposed = self.query_one("#proposed", Static)
|
||||
proposed.update("")
|
||||
|
||||
def _stop_loading_animation(self):
|
||||
loading = self.query_one("#loading", LoadingIndicator)
|
||||
loading.display = False
|
||||
|
||||
def on_tree_node_highlighted(self, event):
|
||||
node = event.node
|
||||
if node.data and isinstance(node.data, Path):
|
||||
if node.data.is_dir():
|
||||
self._stop_loading_animation()
|
||||
details = self.query_one("#details", Static)
|
||||
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()
|
||||
|
||||
def _extract_and_show_details(self, file_path: Path):
|
||||
time.sleep(1) # Minimum delay to show loading
|
||||
# Initialize extractors and formatters
|
||||
extractor = MediaExtractor()
|
||||
formatter = MediaFormatter()
|
||||
|
||||
# Extract all data
|
||||
rename_data = extractor.extract_all(file_path)
|
||||
|
||||
# Get media tracks info
|
||||
tracks_text = get_media_tracks(file_path)
|
||||
if not tracks_text:
|
||||
tracks_text = "[grey]No track info available[/grey]"
|
||||
|
||||
# Format file info
|
||||
full_info = formatter.format_file_info(file_path, rename_data)
|
||||
full_info += f"\n\n{tracks_text}"
|
||||
|
||||
# Format proposed name
|
||||
ext_name = file_path.suffix.lower().lstrip('.')
|
||||
proposed_name = formatter.format_proposed_name(rename_data, ext_name)
|
||||
|
||||
# Format rename lines
|
||||
rename_lines = formatter.format_rename_lines(rename_data, proposed_name)
|
||||
full_info += f"\n\n" + "\n".join(rename_lines[:-1])
|
||||
|
||||
# Update UI
|
||||
self.call_later(self._update_details, full_info, proposed_name)
|
||||
|
||||
def _update_details(self, full_info: str, proposed_name: str):
|
||||
self._stop_loading_animation()
|
||||
details = self.query_one("#details", Static)
|
||||
details.update(full_info)
|
||||
|
||||
proposed = self.query_one("#proposed", Static)
|
||||
proposed.update(f"[bold yellow]Proposed filename: {proposed_name}[/bold yellow]")
|
||||
|
||||
def action_quit(self):
|
||||
self.exit()
|
||||
|
||||
def action_open(self):
|
||||
self.push_screen(OpenScreen())
|
||||
|
||||
def action_scan(self):
|
||||
if self.scan_dir:
|
||||
self.scan_files()
|
||||
|
||||
def on_key(self, event):
|
||||
if event.key == "right":
|
||||
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_dir():
|
||||
if not node.is_expanded:
|
||||
node.expand()
|
||||
tree.cursor_line = node.line + 1
|
||||
event.prevent_default()
|
||||
elif event.key == "left":
|
||||
tree = self.query_one("#file_tree", Tree)
|
||||
node = tree.cursor_node
|
||||
if node and node.parent:
|
||||
if node.is_expanded:
|
||||
node.collapse()
|
||||
else:
|
||||
tree.cursor_line = node.parent.line
|
||||
event.prevent_default()
|
||||
Reference in New Issue
Block a user