added media catalog mode, impooved cache

This commit is contained in:
sha
2025-12-29 19:47:55 +00:00
parent eedc32bf31
commit 50de7e1d4a
20 changed files with 900 additions and 106 deletions
+74 -12
View File
@@ -1,6 +1,7 @@
from textual.app import App, ComposeResult
from textual.widgets import Tree, Static, Footer, LoadingIndicator
from textual.containers import Horizontal, Container, ScrollableContainer, Vertical
from textual.widget import Widget
from rich.markup import escape
from pathlib import Path
import threading
@@ -9,11 +10,14 @@ import logging
import os
from .constants import MEDIA_TYPES
from .screens import OpenScreen, HelpScreen, RenameConfirmScreen
from .screens import OpenScreen, HelpScreen, RenameConfirmScreen, SettingsScreen
from .extractors.extractor import MediaExtractor
from .formatters.media_formatter import MediaFormatter
from .formatters.proposed_name_formatter import ProposedNameFormatter
from .formatters.text_formatter import TextFormatter
from .formatters.catalog_formatter import CatalogFormatter
from .settings import Settings
from .cache import Cache
# Set up logging conditionally
@@ -43,13 +47,17 @@ class RenamerApp(App):
("f", "refresh", "Refresh"),
("r", "rename", "Rename"),
("p", "expand", "Toggle Tree"),
("m", "toggle_mode", "Toggle Mode"),
("h", "help", "Help"),
("ctrl+s", "settings", "Settings"),
]
def __init__(self, scan_dir):
super().__init__()
self.scan_dir = Path(scan_dir) if scan_dir else None
self.tree_expanded = False
self.settings = Settings()
self.cache = Cache()
def compose(self) -> ComposeResult:
with Horizontal():
@@ -60,7 +68,10 @@ class RenamerApp(App):
yield LoadingIndicator(id="loading")
with ScrollableContainer(id="details_container"):
yield Static(
"Select a file to view details", id="details", markup=True
"Select a file to view details", id="details_technical", markup=True
)
yield Static(
"", id="details_catalog", markup=False
)
yield Static("", id="proposed", markup=True)
yield Footer()
@@ -73,7 +84,7 @@ class RenamerApp(App):
def scan_files(self):
logging.info("scan_files called")
if not self.scan_dir or not self.scan_dir.exists() or not self.scan_dir.is_dir():
details = self.query_one("#details", Static)
details = self.query_one("#details_technical", Static)
details.update("Error: Directory does not exist or is not a directory")
return
tree = self.query_one("#file_tree", Tree)
@@ -105,7 +116,11 @@ class RenamerApp(App):
def _start_loading_animation(self):
loading = self.query_one("#loading", LoadingIndicator)
loading.display = True
details = self.query_one("#details", Static)
mode = self.settings.get("mode")
if mode == "technical":
details = self.query_one("#details_technical", Static)
else:
details = self.query_one("#details_catalog", Static)
details.update("Retrieving media data")
proposed = self.query_one("#proposed", Static)
proposed.update("")
@@ -119,7 +134,10 @@ class RenamerApp(App):
if node.data and isinstance(node.data, Path):
if node.data.is_dir():
self._stop_loading_animation()
details = self.query_one("#details", Static)
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("")
@@ -133,12 +151,20 @@ class RenamerApp(App):
time.sleep(1) # Minimum delay to show loading
try:
# Initialize extractors and formatters
extractor = MediaExtractor(file_path)
extractor = MediaExtractor.create(file_path, self.cache, self.settings.get("cache_ttl_extractors"))
mode = self.settings.get("mode")
if mode == "technical":
formatter = MediaFormatter(extractor)
full_info = formatter.file_info_panel()
else: # catalog
formatter = CatalogFormatter(extractor)
full_info = formatter.format_catalog_info()
# Update UI
self.call_later(
self._update_details,
MediaFormatter(extractor).file_info_panel(),
full_info,
ProposedNameFormatter(extractor).rename_line_formatted(file_path),
)
except Exception as e:
@@ -150,9 +176,18 @@ class RenamerApp(App):
def _update_details(self, full_info: str, display_string: str):
self._stop_loading_animation()
details = self.query_one("#details", Static)
details.update(full_info)
details_technical = self.query_one("#details_technical", Static)
details_catalog = self.query_one("#details_catalog", Static)
mode = self.settings.get("mode")
if mode == "technical":
details_technical.display = True
details_catalog.display = False
details_technical.update(full_info)
else:
details_technical.display = False
details_catalog.display = True
details_catalog.update(full_info)
proposed = self.query_one("#proposed", Static)
proposed.update(display_string)
@@ -170,6 +205,11 @@ class RenamerApp(App):
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():
# Clear cache for this file
cache_key_base = str(node.data)
# Invalidate all keys for this file (we can improve this later)
for key in ["title", "year", "source", "extension", "video_tracks", "audio_tracks", "subtitle_tracks"]:
self.cache.invalidate(f"{cache_key_base}_{key}")
self._start_loading_animation()
threading.Thread(
target=self._extract_and_show_details, args=(node.data,)
@@ -178,12 +218,29 @@ class RenamerApp(App):
async def action_help(self):
self.push_screen(HelpScreen())
async def action_settings(self):
self.push_screen(SettingsScreen())
async def action_toggle_mode(self):
current_mode = self.settings.get("mode")
new_mode = "catalog" if current_mode == "technical" else "technical"
self.settings.set("mode", new_mode)
self.notify(f"Switched to {new_mode} mode", severity="information", timeout=2)
# Refresh current file display if any
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()
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)
extractor = MediaExtractor.create(node.data, self.cache, self.settings.get("cache_ttl_extractors"))
proposed_formatter = ProposedNameFormatter(extractor)
new_name = str(proposed_formatter)
logging.info(f"Proposed new name: {new_name!r} for file: {node.data}")
@@ -216,6 +273,11 @@ class RenamerApp(App):
"""Update the tree node for a renamed file."""
logging.info(f"update_renamed_file called with old_path={old_path}, new_path={new_path}")
# Clear cache for old file
cache_key_base = str(old_path)
for key in ["title", "year", "source", "extension", "video_tracks", "audio_tracks", "subtitle_tracks"]:
self.cache.invalidate(f"{cache_key_base}_{key}")
tree = self.query_one("#file_tree", Tree)
logging.info(f"Before update: cursor_node.data = {tree.cursor_node.data if tree.cursor_node else None}")