dfdfdf
This commit is contained in:
219
metadata.py
219
metadata.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user