This commit is contained in:
nose
2025-12-12 21:55:38 -08:00
parent e2ffcab030
commit 85750247cc
78 changed files with 5726 additions and 6239 deletions

View File

@@ -58,10 +58,7 @@ _CURRENT_RELATIONSHIP_TRACKER = FileRelationshipTracker()
def prepare_ffmpeg_metadata(payload: Optional[Dict[str, Any]]) -> Dict[str, str]:
"""Derive ffmpeg/mutagen metadata tags from a generic metadata payload.
This is not Hydrus-specific; it is used by exporters/converters.
"""
"""Build ffmpeg/mutagen metadata map from payload."""
if not isinstance(payload, dict):
return {}
@@ -275,29 +272,17 @@ def build_ffmpeg_command(
def field(obj: Any, name: str, value: Any = None) -> Any:
"""Get or set a field on dict or object.
Args:
obj: Dict or object to access
name: Field name
value: If None, gets the field; if not None, sets it and returns the value
Returns:
The field value (when getting) or the value (when setting)
"""
if value is None:
# Get mode
if isinstance(obj, dict):
return obj.get(name)
else:
return getattr(obj, name, None)
else:
# Set mode
if isinstance(obj, dict):
obj[name] = value
else:
setattr(obj, name, value)
return value
"""Get or set a field on dict or object."""
if value is None:
if isinstance(obj, dict):
return obj.get(name)
return getattr(obj, name, None)
if isinstance(obj, dict):
obj[name] = value
else:
setattr(obj, name, value)
return value
@@ -1602,78 +1587,61 @@ def _read_sidecar_metadata(sidecar_path: Path) -> tuple[Optional[str], List[str]
def rename(file_path: Path, tags: Iterable[str]) -> Optional[Path]:
"""Rename a file based on title: tag in the tags list.
"""Rename a file based on a title: tag.
If a title: tag is present, renames the file and any .tag/.metadata sidecars.
Args:
file_path: Path to the file to potentially rename
tags: Iterable of tag strings (should contain title: tag if rename needed)
Returns:
New path if renamed, None if not renamed or error occurred
"""
# Extract title from tags
new_title = None
for tag in tags:
if isinstance(tag, str) and tag.lower().startswith('title:'):
new_title = tag.split(':', 1)[1].strip()
break
if not new_title or not file_path.exists():
return None
try:
old_name = file_path.name
old_suffix = file_path.suffix
# Create new filename: title + extension
new_name = f"{new_title}{old_suffix}"
new_path = file_path.parent / new_name
# Don't rename if already the same name
if new_path == file_path:
return None
# If target exists, delete it first (replace mode)
if new_path.exists():
try:
new_path.unlink()
debug(f"Replaced existing file: {new_name}", file=sys.stderr)
except Exception as e:
debug(f"Warning: Could not replace target file {new_name}: {e}", file=sys.stderr)
return None
file_path.rename(new_path)
debug(f"Renamed file: {old_name}{new_name}", file=sys.stderr)
# Rename the .tag sidecar if it exists
old_tags_path = file_path.parent / (old_name + '.tag')
if old_tags_path.exists():
new_tags_path = file_path.parent / (new_name + '.tag')
if new_tags_path.exists():
try:
new_tags_path.unlink()
except Exception:
pass
else:
old_tags_path.rename(new_tags_path)
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():
debug(f"Warning: Target metadata already exists: {new_metadata_path.name}", file=sys.stderr)
else:
old_metadata_path.rename(new_metadata_path)
debug(f"Renamed metadata: {old_metadata_path.name}{new_metadata_path.name}", file=sys.stderr)
return new_path
except Exception as exc:
debug(f"Warning: Failed to rename file: {exc}", file=sys.stderr)
return None
"""
new_title: Optional[str] = None
for tag in tags:
if isinstance(tag, str) and tag.lower().startswith("title:"):
new_title = tag.split(":", 1)[1].strip()
break
if not new_title or not file_path.exists():
return None
old_name = file_path.name
old_suffix = file_path.suffix
new_name = f"{new_title}{old_suffix}"
new_path = file_path.with_name(new_name)
if new_path == file_path:
return None
def _rename_sidecar(ext: str) -> None:
old_sidecar = file_path.parent / (old_name + ext)
if not old_sidecar.exists():
return
new_sidecar = file_path.parent / (new_name + ext)
if new_sidecar.exists():
try:
new_sidecar.unlink()
except Exception as exc:
debug(f"Warning: Could not replace target sidecar {new_sidecar.name}: {exc}", file=sys.stderr)
return
old_sidecar.rename(new_sidecar)
debug(f"Renamed sidecar: {old_sidecar.name} -> {new_sidecar.name}", file=sys.stderr)
try:
if new_path.exists():
try:
new_path.unlink()
debug(f"Replaced existing file: {new_name}", file=sys.stderr)
except Exception as exc:
debug(f"Warning: Could not replace target file {new_name}: {exc}", file=sys.stderr)
return None
file_path.rename(new_path)
debug(f"Renamed file: {old_name} -> {new_name}", file=sys.stderr)
_rename_sidecar(".tag")
_rename_sidecar(".metadata")
return new_path
except Exception as exc:
debug(f"Warning: Failed to rename file: {exc}", file=sys.stderr)
return None
def write_tags(media_path: Path, tags: Iterable[str], url: Iterable[str], hash_value: Optional[str] = None, db=None) -> None:
@@ -2096,26 +2064,7 @@ def apply_tag_mutation(payload: Dict[str, Any], operation: str = 'add') -> Dict[
def extract_ytdlp_tags(entry: Dict[str, Any]) -> List[str]:
"""Extract meaningful metadata tags from yt-dlp entry.
This is the UNIFIED API for extracting tags from yt-dlp metadata.
All modules (download_data, merge_file, etc.) should use this function
instead of implementing their own extraction logic.
Extracts meaningful tags (artist, album, creator, genre, track, etc.)
while excluding technical fields (filesize, duration, format, etc.).
Args:
entry: yt-dlp entry metadata dictionary from download
Returns:
List of normalized tag strings in format "namespace:value"
Example:
>>> entry = {'artist': 'The Beatles', 'album': 'Abbey Road', 'duration': 5247}
>>> tags = extract_ytdlp_tags(entry)
>>> debug(tags)
['artist:The Beatles', 'album:Abbey Road']
"""
"""
tags: List[str] = []
seen_namespaces: Set[str] = set()
@@ -2186,7 +2135,7 @@ def extract_ytdlp_tags(entry: Dict[str, Any]) -> List[str]:
def dedup_tags_by_namespace(tags: List[str], keep_first: bool = True) -> List[str]:
"""Deduplicate tags by namespace, keeping consistent order.
This is the UNIFIED API for tag deduplication used across all cmdlets.
This is the UNIFIED API for tag deduplication used across all cmdlet.
Replaces custom deduplication logic in merge_file.py and other modules.
Groups tags by namespace (e.g., "artist", "album", "tag") and keeps
@@ -2345,7 +2294,7 @@ def merge_multiple_tag_lists(
def read_tags_from_file(file_path: Path) -> List[str]:
"""Read and normalize tags from .tag sidecar file.
This is the UNIFIED API for reading .tag files across all cmdlets.
This is the UNIFIED API for reading .tag files across all cmdlet.
Handles normalization, deduplication, and format validation.
Args:
@@ -2397,33 +2346,7 @@ def embed_metadata_in_file(
tags: List[str],
file_kind: str = ''
) -> bool:
"""Embed metadata tags into a media file using FFmpeg.
Extracts metadata from tags (namespace:value format) and writes to the file's
metadata using FFmpeg with -c copy (no re-encoding).
Supported tag namespaces:
- title, artist, album, track/track_number, date/year, genre, composer, comment
For audio files, applies sensible defaults:
- If no album, uses title as album
- If no track, defaults to 1
- album_artist is set to artist value
Args:
file_path: Path to media file
tags: List of tags in format ['namespace:value', ...] (e.g., ['artist:Beatles', 'album:Abbey Road'])
file_kind: Type of file: 'audio', 'video', or '' for auto-detect (optional)
Returns:
True if successful, False otherwise
Raises:
None (logs errors to stderr)
Example:
>>> tags = ['artist:Beatles', 'album:Abbey Road', 'track:1']
>>> success = embed_metadata_in_file(Path('song.mp3'), tags, file_kind='audio')
"""
"""
if not tags:
return True
@@ -2550,7 +2473,7 @@ def write_tags_to_file(
) -> bool:
"""Write tags to .tag sidecar file.
This is the UNIFIED API for writing .tag files across all cmdlets.
This is the UNIFIED API for writing .tag files across all cmdlet.
Uses consistent format and handles file creation/overwriting.
Args: