This commit is contained in:
nose
2025-11-27 10:59:01 -08:00
parent e9b505e609
commit 9eff65d1af
30 changed files with 2099 additions and 1095 deletions

View File

@@ -5,7 +5,7 @@ import sys
import shutil
import sqlite3
import requests
from helper.logger import log
from helper.logger import log, debug
from urllib.parse import urlsplit, urlunsplit, unquote
from collections import deque
from pathlib import Path
@@ -1312,7 +1312,7 @@ def _read_sidecar_metadata(sidecar_path: Path) -> tuple[Optional[str], List[str]
def rename_by_metadata(file_path: Path, tags: Iterable[str]) -> Optional[Path]:
def rename(file_path: Path, tags: Iterable[str]) -> Optional[Path]:
"""Rename a file based on title: tag in the tags list.
If a title: tag is present, renames the file and any .tags/.metadata sidecars.
@@ -1350,13 +1350,13 @@ def rename_by_metadata(file_path: Path, tags: Iterable[str]) -> Optional[Path]:
if new_path.exists():
try:
new_path.unlink()
log(f"[rename_by_metadata] Replaced existing file: {new_name}", file=sys.stderr)
debug(f"Replaced existing file: {new_name}", file=sys.stderr)
except Exception as e:
log(f"[rename_by_metadata] Warning: Could not replace target file {new_name}: {e}", file=sys.stderr)
debug(f"Warning: Could not replace target file {new_name}: {e}", file=sys.stderr)
return None
file_path.rename(new_path)
log(f"[rename_by_metadata] Renamed file: {old_name}{new_name}", file=sys.stderr)
debug(f"Renamed file: {old_name}{new_name}", file=sys.stderr)
# Rename the .tags sidecar if it exists
old_tags_path = file_path.parent / (old_name + '.tags')
@@ -1369,21 +1369,21 @@ def rename_by_metadata(file_path: Path, tags: Iterable[str]) -> Optional[Path]:
pass
else:
old_tags_path.rename(new_tags_path)
log(f"[rename_by_metadata] Renamed sidecar: {old_tags_path.name}{new_tags_path.name}", file=sys.stderr)
debug(f"Renamed sidecar: {old_tags_path.name}{new_tags_path.name}", file=sys.stderr)
# Rename the .metadata sidecar if it exists
old_metadata_path = file_path.parent / (old_name + '.metadata')
if old_metadata_path.exists():
new_metadata_path = file_path.parent / (new_name + '.metadata')
if new_metadata_path.exists():
log(f"[rename_by_metadata] Warning: Target metadata already exists: {new_metadata_path.name}", file=sys.stderr)
debug(f"Warning: Target metadata already exists: {new_metadata_path.name}", file=sys.stderr)
else:
old_metadata_path.rename(new_metadata_path)
log(f"[rename_by_metadata] Renamed metadata: {old_metadata_path.name}{new_metadata_path.name}", file=sys.stderr)
debug(f"Renamed metadata: {old_metadata_path.name}{new_metadata_path.name}", file=sys.stderr)
return new_path
except Exception as exc:
log(f"[rename_by_metadata] Warning: Failed to rename file: {exc}", file=sys.stderr)
debug(f"Warning: Failed to rename file: {exc}", file=sys.stderr)
return None
@@ -1419,10 +1419,10 @@ def write_tags(media_path: Path, tags: Iterable[str], known_urls: Iterable[str],
if db_tags:
db.add_tags(media_path, db_tags)
log(f"Added tags to database for {media_path.name}")
debug(f"Added tags to database for {media_path.name}")
return
except Exception as e:
log(f"Failed to add tags to database: {e}", file=sys.stderr)
debug(f"Failed to add tags to database: {e}", file=sys.stderr)
# Fall through to sidecar creation as fallback
# Create sidecar path
@@ -1449,7 +1449,7 @@ def write_tags(media_path: Path, tags: Iterable[str], known_urls: Iterable[str],
if lines:
sidecar.write_text("\n".join(lines) + "\n", encoding="utf-8")
log(f"Wrote tags to {sidecar}")
debug(f"Tags: {sidecar}")
# Clean up legacy files
for legacy_path in [media_path.with_name(media_path.name + '.tags'),
media_path.with_name(media_path.name + '.tags.txt')]:
@@ -1464,7 +1464,7 @@ def write_tags(media_path: Path, tags: Iterable[str], known_urls: Iterable[str],
except FileNotFoundError:
pass
except OSError as exc:
log(f"Failed to write tag sidecar {sidecar}: {exc}", file=sys.stderr)
debug(f"Failed to write tag sidecar {sidecar}: {exc}", file=sys.stderr)
def write_metadata(media_path: Path, hash_value: Optional[str] = None, known_urls: Optional[Iterable[str]] = None, relationships: Optional[Iterable[str]] = None, db=None) -> None:
@@ -1503,10 +1503,10 @@ def write_metadata(media_path: Path, hash_value: Optional[str] = None, known_url
if db_tags:
db.add_tags(media_path, db_tags)
log(f"Added metadata to database for {media_path.name}")
debug(f"Added metadata to database for {media_path.name}")
return
except Exception as e:
log(f"Failed to add metadata to database: {e}", file=sys.stderr)
debug(f"Failed to add metadata to database: {e}", file=sys.stderr)
# Fall through to sidecar creation as fallback
# Create sidecar path
@@ -1535,7 +1535,7 @@ def write_metadata(media_path: Path, hash_value: Optional[str] = None, known_url
# Write metadata file
if lines:
sidecar.write_text("\n".join(lines) + "\n", encoding="utf-8")
log(f"Wrote metadata to {sidecar}")
debug(f"Wrote metadata to {sidecar}")
else:
# Remove if no content
try:
@@ -1543,7 +1543,7 @@ def write_metadata(media_path: Path, hash_value: Optional[str] = None, known_url
except FileNotFoundError:
pass
except OSError as exc:
log(f"Failed to write metadata sidecar {sidecar}: {exc}", file=sys.stderr)
debug(f"Failed to write metadata sidecar {sidecar}: {exc}", file=sys.stderr)
def extract_title(tags: Iterable[str]) -> Optional[str]:
@@ -1892,7 +1892,7 @@ def extract_ytdlp_tags(entry: Dict[str, Any]) -> List[str]:
Example:
>>> entry = {'artist': 'The Beatles', 'album': 'Abbey Road', 'duration': 5247}
>>> tags = extract_ytdlp_tags(entry)
>>> log(tags)
>>> debug(tags)
['artist:The Beatles', 'album:Abbey Road']
"""
tags: List[str] = []
@@ -1986,7 +1986,7 @@ def dedup_tags_by_namespace(tags: List[str], keep_first: bool = True) -> List[st
... 'album:Abbey Road', 'artist:Beatles'
... ]
>>> dedup = dedup_tags_by_namespace(tags)
>>> log(dedup)
>>> debug(dedup)
['artist:Beatles', 'album:Abbey Road', 'tag:rock']
"""
if not tags:
@@ -2053,7 +2053,7 @@ def merge_multiple_tag_lists(
>>> list1 = ['artist:Beatles', 'album:Abbey Road']
>>> list2 = ['artist:Beatles', 'album:Abbey Road', 'tag:rock']
>>> merged = merge_multiple_tag_lists([list1, list2])
>>> log(merged)
>>> debug(merged)
['artist:Beatles', 'album:Abbey Road', 'tag:rock']
"""
if not sources:
@@ -2137,7 +2137,7 @@ def read_tags_from_file(file_path: Path) -> List[str]:
Example:
>>> tags = read_tags_from_file(Path('file.txt.tags'))
>>> log(tags)
>>> debug(tags)
['artist:Beatles', 'album:Abbey Road']
"""
file_path = Path(file_path)
@@ -2271,7 +2271,7 @@ def embed_metadata_in_file(
# Check if FFmpeg is available
ffmpeg_path = shutil.which('ffmpeg')
if not ffmpeg_path:
log(f"⚠️ FFmpeg not found; cannot embed metadata in {file_path.name}", file=sys.stderr)
debug(f"⚠️ FFmpeg not found; cannot embed metadata in {file_path.name}", file=sys.stderr)
return False
# Create temporary file for output
@@ -2294,18 +2294,18 @@ def embed_metadata_in_file(
# Replace original with temp file
file_path.unlink()
temp_file.rename(file_path)
log(f"✅ Embedded metadata in file: {file_path.name}", file=sys.stderr)
debug(f"✅ Embedded metadata in file: {file_path.name}", file=sys.stderr)
return True
else:
# Clean up temp file if it exists
if temp_file.exists():
temp_file.unlink()
log(f"❌ FFmpeg metadata embedding failed for {file_path.name}", file=sys.stderr)
debug(f"❌ FFmpeg metadata embedding failed for {file_path.name}", file=sys.stderr)
if result.stderr:
# Safely decode stderr, ignoring invalid UTF-8 bytes
try:
stderr_text = result.stderr.decode('utf-8', errors='replace')[:200]
log(f"FFmpeg stderr: {stderr_text}", file=sys.stderr)
debug(f"FFmpeg stderr: {stderr_text}", file=sys.stderr)
except Exception:
pass
return False
@@ -2315,7 +2315,7 @@ def embed_metadata_in_file(
temp_file.unlink()
except Exception:
pass
log(f"❌ Error embedding metadata: {exc}", file=sys.stderr)
debug(f"❌ Error embedding metadata: {exc}", file=sys.stderr)
return False
@@ -2402,7 +2402,7 @@ def normalize_tags_from_source(
Example:
>>> entry = {'artist': 'Beatles', 'album': 'Abbey Road'}
>>> tags = normalize_tags_from_source(entry, 'ytdlp')
>>> log(tags)
>>> debug(tags)
['artist:Beatles', 'album:Abbey Road']
"""
if source_type == 'auto':
@@ -2600,10 +2600,10 @@ def imdb(imdb_id: str = typer.Argument(..., help="IMDb identifier (ttXXXXXXX)"))
"""Lookup an IMDb title."""
try:
result = imdb_tag(imdb_id)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(help="Lookup a MusicBrainz entity")
@@ -2614,10 +2614,10 @@ def musicbrainz(
"""Lookup a MusicBrainz entity."""
try:
result = fetch_musicbrainz_tags(mbid, entity)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(name="remote-tags", help="Normalize a remote metadata payload")
@@ -2633,10 +2633,10 @@ def remote_tags(payload: Optional[str] = typer.Option(None, "--payload", help="J
if context and not isinstance(context, dict):
raise ValueError("context must be an object")
result = build_remote_bundle(metadata, existing, context)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(name="remote-fetch", help="Resolve remote metadata bundle")
@@ -2645,10 +2645,10 @@ def remote_fetch(payload: Optional[str] = typer.Option(None, "--payload", help="
try:
payload_data = _load_payload(payload)
result = resolve_remote_metadata(payload_data)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(name="expand-tag", help="Expand metadata references into tags")
@@ -2657,10 +2657,10 @@ def expand_tag(payload: Optional[str] = typer.Option(None, "--payload", help="JS
try:
payload_data = _load_payload(payload)
result = expand_metadata_tag(payload_data)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(name="hydrus-fetch", help="Fetch Hydrus metadata for a file")
@@ -2669,10 +2669,10 @@ def hydrus_fetch(payload: Optional[str] = typer.Option(None, "--payload", help="
try:
payload_data = _load_payload(payload)
result = fetch_hydrus_metadata(payload_data)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(name="hydrus-fetch-url", help="Fetch Hydrus metadata using a source URL")
@@ -2681,10 +2681,10 @@ def hydrus_fetch_url(payload: Optional[str] = typer.Option(None, "--payload", he
try:
payload_data = _load_payload(payload)
result = fetch_hydrus_metadata_by_url(payload_data)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(name="sync-sidecar", help="Synchronise .tags sidecar with supplied data")
@@ -2693,10 +2693,10 @@ def sync_sidecar_cmd(payload: Optional[str] = typer.Option(None, "--payload", he
try:
payload_data = _load_payload(payload)
result = sync_sidecar(payload_data)
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
@app.command(name="update-tag", help="Update or rename a tag")
@@ -2705,10 +2705,10 @@ def update_tag_cmd(payload: Optional[str] = typer.Option(None, "--payload", help
try:
payload_data = _load_payload(payload)
result = apply_tag_mutation(payload_data, 'update')
log(json.dumps(result, ensure_ascii=False), flush=True)
debug(json.dumps(result, ensure_ascii=False), flush=True)
except Exception as exc:
error_payload = {"error": str(exc)}
log(json.dumps(error_payload, ensure_ascii=False), flush=True)
debug(json.dumps(error_payload, ensure_ascii=False), flush=True)
raise typer.Exit(code=1)
def main(argv: Optional[List[str]] = None) -> int:
@@ -3102,7 +3102,7 @@ def fetch_openlibrary_metadata_tags(isbn: Optional[str] = None, olid: Optional[s
metadata_tags.append(subject_clean)
except Exception as e:
log(f"⚠ Failed to fetch OpenLibrary metadata: {e}")
debug(f"⚠ Failed to fetch OpenLibrary metadata: {e}")
return metadata_tags