Add metadata extraction from local audio files for proper tag preservation
- Add extract_tags() to metadata_handler.py: reads tags from FLAC, MP3, OGG, M4A - Support ID3v2 (MP3), Vorbis comments (FLAC/OGG), MP4 tags (M4A) - Extract cover art from METADATA_BLOCK_PICTURE (FLAC), APIC (MP3), covr (M4A) - Update _run_convert to use extract_tags instead of guessing from filename - Fix tag reading in ipod_nano7_db.get_audio_info() using EasyID3 for MP3
This commit is contained in:
parent
761f896ea8
commit
1b4c092a04
@ -15,9 +15,9 @@ import shutil
|
|||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
from mutagen import File as MutagenFile
|
from mutagen import File as MutagenFile
|
||||||
|
from mutagen.easyid3 import EasyID3
|
||||||
from mutagen.mp4 import MP4
|
from mutagen.mp4 import MP4
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
from mutagen.id3 import ID3
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -117,51 +117,118 @@ class Nano7Database:
|
|||||||
else:
|
else:
|
||||||
info["audio_format"] = 1
|
info["audio_format"] = 1
|
||||||
|
|
||||||
# Extract tags
|
# Extract tags - handle different formats
|
||||||
for tag_name, file_key in [
|
ext = os.path.splitext(filepath)[1].lower()
|
||||||
("title", "title"),
|
|
||||||
("artist", "artist"),
|
|
||||||
("album", "album"),
|
|
||||||
("albumartist", "album_artist"),
|
|
||||||
("composer", "composer"),
|
|
||||||
("genre", "genre"),
|
|
||||||
("comment", "comment"),
|
|
||||||
]:
|
|
||||||
if tag_name in audio:
|
|
||||||
val = audio[tag_name]
|
|
||||||
if isinstance(val, list):
|
|
||||||
val = val[0]
|
|
||||||
if isinstance(val, str):
|
|
||||||
info[file_key] = val
|
|
||||||
|
|
||||||
# Track/disc numbers
|
# Use EasyID3 for MP3 files (supports easy key access)
|
||||||
for tag_name, file_key in [
|
if ext == ".mp3":
|
||||||
("tracknumber", "track_number"),
|
try:
|
||||||
("discnumber", "disc_number"),
|
easy = EasyID3(filepath)
|
||||||
]:
|
for tag_name, file_key in [
|
||||||
if tag_name in audio:
|
("title", "title"),
|
||||||
val = audio[tag_name]
|
("artist", "artist"),
|
||||||
if isinstance(val, list):
|
("album", "album"),
|
||||||
val = val[0]
|
("albumartist", "albumartist"),
|
||||||
if isinstance(val, str):
|
("composer", "composer"),
|
||||||
parts = val.split("/")
|
("genre", "genre"),
|
||||||
info[file_key] = int(parts[0]) if parts[0].isdigit() else 0
|
("comment", "comment"),
|
||||||
if len(parts) > 1:
|
]:
|
||||||
info[file_key + "_count"] = (
|
if tag_name in easy:
|
||||||
int(parts[1]) if parts[1].isdigit() else 0
|
val = easy[tag_name]
|
||||||
)
|
if isinstance(val, list) and val:
|
||||||
elif isinstance(val, int):
|
info[file_key] = str(val[0])
|
||||||
info[file_key] = val
|
|
||||||
|
|
||||||
# Year
|
# Track/disc numbers
|
||||||
if "date" in audio:
|
for tag_name, file_key in [
|
||||||
val = audio["date"]
|
("tracknumber", "track_number"),
|
||||||
if isinstance(val, list):
|
("discnumber", "disc_number"),
|
||||||
val = val[0]
|
]:
|
||||||
if isinstance(val, str) and len(val) >= 4:
|
if tag_name in easy:
|
||||||
info["year"] = int(val[:4])
|
val = easy[tag_name]
|
||||||
elif isinstance(val, int):
|
if isinstance(val, list) and val:
|
||||||
info["year"] = val
|
val = str(val[0])
|
||||||
|
if '/' in val:
|
||||||
|
parts = val.split('/')
|
||||||
|
info[file_key] = int(parts[0]) if parts[0].isdigit() else 0
|
||||||
|
elif val.isdigit():
|
||||||
|
info[file_key] = int(val)
|
||||||
|
|
||||||
|
# Date/year
|
||||||
|
if "date" in easy:
|
||||||
|
val = easy["date"]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
date_str = str(val[0])
|
||||||
|
if len(date_str) >= 4:
|
||||||
|
info["year"] = int(date_str[:4])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"EasyID3 failed for {filepath}: {e}")
|
||||||
|
|
||||||
|
# MP4/M4A uses MutagenFile directly
|
||||||
|
elif ext in (".m4a", ".aac", ".mp4"):
|
||||||
|
if audio and audio.tags:
|
||||||
|
tags = audio.tags
|
||||||
|
for tag_name, mp4_key, file_key in [
|
||||||
|
("title", "\xa9nam", "title"),
|
||||||
|
("artist", "\xa9ART", "artist"),
|
||||||
|
("album", "\xa9alb", "album"),
|
||||||
|
]:
|
||||||
|
if mp4_key in tags:
|
||||||
|
val = tags[mp4_key]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
info[file_key] = str(val[0])
|
||||||
|
if "trkn" in tags:
|
||||||
|
trkn = tags["trkn"]
|
||||||
|
if isinstance(trkn, list) and trkn:
|
||||||
|
t = trkn[0]
|
||||||
|
if isinstance(t, tuple) and len(t) >= 1:
|
||||||
|
info["track_number"] = t[0]
|
||||||
|
else:
|
||||||
|
# FLAC/OGG/Vorbis - use MutagenFile with tag access
|
||||||
|
if audio and audio.tags:
|
||||||
|
tags = audio.tags
|
||||||
|
for tag_name, file_key in [
|
||||||
|
("title", "title"),
|
||||||
|
("artist", "artist"),
|
||||||
|
("album", "album"),
|
||||||
|
("albumartist", "album_artist"),
|
||||||
|
("composer", "composer"),
|
||||||
|
("genre", "genre"),
|
||||||
|
("comment", "comment"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
if tag_name in tags:
|
||||||
|
val = tags[tag_name]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
info[file_key] = str(val[0])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for tag_name, file_key in [
|
||||||
|
("tracknumber", "track_number"),
|
||||||
|
("discnumber", "disc_number"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
if tag_name in tags:
|
||||||
|
val = tags[tag_name]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
val = str(val[0])
|
||||||
|
if '/' in val:
|
||||||
|
parts = val.split('/')
|
||||||
|
info[file_key] = int(parts[0]) if parts[0].isdigit() else 0
|
||||||
|
elif val.isdigit():
|
||||||
|
info[file_key] = int(val)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if "date" in tags:
|
||||||
|
val = tags["date"]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
date_str = str(val[0])
|
||||||
|
if len(date_str) >= 4:
|
||||||
|
info["year"] = int(date_str[:4])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|||||||
27
src/main.py
27
src/main.py
@ -108,27 +108,16 @@ class WorkerThread(QThread):
|
|||||||
# Filter out tracks without download paths
|
# Filter out tracks without download paths
|
||||||
valid_tracks = [track for track in tracks if hasattr(track, 'download_path') and track.download_path and os.path.exists(track.download_path)]
|
valid_tracks = [track for track in tracks if hasattr(track, 'download_path') and track.download_path and os.path.exists(track.download_path)]
|
||||||
|
|
||||||
# Process local files into TrackInfo objects
|
# Process local files: extract real metadata from tags
|
||||||
|
metadata_handler = MetadataHandler()
|
||||||
for f in local_files:
|
for f in local_files:
|
||||||
if os.path.exists(f):
|
if os.path.exists(f):
|
||||||
filename = os.path.basename(f)
|
# Try to extract metadata from file tags first
|
||||||
title = os.path.splitext(filename)[0]
|
track = metadata_handler.extract_tags(f)
|
||||||
# Try to parse "Artist - Title" format
|
if track:
|
||||||
if " - " in title:
|
# Ensure download_path is set
|
||||||
artist, track_title = title.split(" - ", 1)
|
track.download_path = f
|
||||||
else:
|
valid_tracks.append(track)
|
||||||
artist = "Unknown"
|
|
||||||
track_title = title
|
|
||||||
track = TrackInfo(
|
|
||||||
video_id=f"local_{hash(f)}",
|
|
||||||
title=track_title,
|
|
||||||
artist=artist,
|
|
||||||
album="",
|
|
||||||
thumbnail_url="",
|
|
||||||
duration=0,
|
|
||||||
download_path=f
|
|
||||||
)
|
|
||||||
valid_tracks.append(track)
|
|
||||||
|
|
||||||
if not valid_tracks:
|
if not valid_tracks:
|
||||||
self.finished_signal.emit(False, "No valid tracks to convert", None)
|
self.finished_signal.emit(False, "No valid tracks to convert", None)
|
||||||
|
|||||||
@ -5,14 +5,18 @@ Handles metadata injection and album art embedding
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import struct
|
||||||
|
import base64
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from mutagen import File as MutagenFile
|
||||||
from mutagen.mp4 import MP4, MP4Cover
|
from mutagen.mp4 import MP4, MP4Cover
|
||||||
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB, TRCK
|
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB, TRCK
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
@ -227,6 +231,308 @@ class MetadataHandler:
|
|||||||
logger.warning(f"Unsupported file format: {file_ext}")
|
logger.warning(f"Unsupported file format: {file_ext}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def extract_tags(self, file_path: str) -> Optional[TrackInfo]:
|
||||||
|
"""
|
||||||
|
Extract metadata from any supported audio file.
|
||||||
|
|
||||||
|
Uses mutagen to read tags from FLAC, MP3, OGG, OPUS, M4A, WAV, APE, etc.
|
||||||
|
Also extracts embedded cover art if present.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to audio file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TrackInfo with extracted metadata, or None if file can't be read
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
import base64
|
||||||
|
|
||||||
|
logger.info(f"Extracting metadata from: {file_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
audio = MutagenFile(file_path)
|
||||||
|
if audio is None:
|
||||||
|
logger.warning(f"Cannot read tags from: {file_path}")
|
||||||
|
return self._parse_filename_trackinfo(
|
||||||
|
os.path.splitext(os.path.basename(file_path))[0], file_path
|
||||||
|
)
|
||||||
|
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
base_name = os.path.splitext(filename)[0]
|
||||||
|
|
||||||
|
tags = audio.tags
|
||||||
|
|
||||||
|
# Extract common fields - handle both vorbis (FLAC/OGG) and MP4 tags
|
||||||
|
title = self._safe_get_tag(tags, 'title', '\xa9nam')
|
||||||
|
artist = self._safe_get_tag(tags, 'artist', '\xa9ART')
|
||||||
|
album = self._safe_get_tag(tags, 'album', '\xa9alb')
|
||||||
|
album_artist = self._safe_get_tag(tags, 'albumartist', 'aART')
|
||||||
|
genre = self._safe_get_tag(tags, 'genre', '\xa9gen')
|
||||||
|
date_str = self._safe_get_tag(tags, 'date', '\xa9day')
|
||||||
|
|
||||||
|
# Track number
|
||||||
|
track_number = 0
|
||||||
|
track_count = 0
|
||||||
|
for key in ('tracknumber', 'TRK', 'trkn'):
|
||||||
|
val = self._safe_get_tag_value(tags, key)
|
||||||
|
if val is not None:
|
||||||
|
if '/' in val:
|
||||||
|
parts = val.split('/')
|
||||||
|
track_number = int(parts[0]) if parts[0].isdigit() else 0
|
||||||
|
track_count = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
track_number = int(val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if track_number:
|
||||||
|
break
|
||||||
|
|
||||||
|
# M4A trkn is stored as list of tuples - check directly
|
||||||
|
if not track_number and tags is not None:
|
||||||
|
try:
|
||||||
|
if 'trkn' in tags:
|
||||||
|
trkn = tags['trkn']
|
||||||
|
if isinstance(trkn, list) and trkn:
|
||||||
|
t = trkn[0]
|
||||||
|
if isinstance(t, tuple) and len(t) >= 1:
|
||||||
|
track_number = t[0]
|
||||||
|
if len(t) >= 2:
|
||||||
|
track_count = t[1]
|
||||||
|
elif isinstance(trkn, tuple) and len(trkn) >= 1:
|
||||||
|
track_number = trkn[0]
|
||||||
|
if len(trkn) >= 2:
|
||||||
|
track_count = trkn[1]
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Disc number
|
||||||
|
disc_number = 0
|
||||||
|
for key in ('discnumber', 'DISC', 'disk'):
|
||||||
|
val = self._safe_get_tag_value(tags, key)
|
||||||
|
if val is not None:
|
||||||
|
if '/' in val:
|
||||||
|
try:
|
||||||
|
disc_number = int(val.split('/')[0])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
disc_number = int(val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if disc_number:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Duration
|
||||||
|
duration = int(audio.info.length) if hasattr(audio, 'info') and audio.info.length else 0
|
||||||
|
|
||||||
|
# Extract cover art
|
||||||
|
cover_data = self._extract_cover(audio, tags)
|
||||||
|
thumbnail_path = None
|
||||||
|
if cover_data:
|
||||||
|
thumbnail_path = os.path.join(self.temp_dir, f"cover_{hash(file_path)}.jpg")
|
||||||
|
self._save_cover_as_jpeg(cover_data, thumbnail_path)
|
||||||
|
|
||||||
|
# Parse year from date
|
||||||
|
year = 0
|
||||||
|
if date_str:
|
||||||
|
try:
|
||||||
|
year = int(str(date_str)[:4])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If essential tags are missing, fall back to filename
|
||||||
|
if not title:
|
||||||
|
fallback = self._parse_filename_trackinfo(base_name)
|
||||||
|
title = title or fallback.title
|
||||||
|
artist = artist or fallback.artist
|
||||||
|
|
||||||
|
return TrackInfo(
|
||||||
|
video_id=f"local_{hash(file_path)}",
|
||||||
|
title=title or base_name,
|
||||||
|
artist=artist or "Unknown Artist",
|
||||||
|
album=album or "",
|
||||||
|
thumbnail_url="",
|
||||||
|
duration=duration,
|
||||||
|
track_number=track_number if track_number else None,
|
||||||
|
download_path=file_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to extract tags from {file_path}: {e}")
|
||||||
|
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
return self._parse_filename_trackinfo(base_name, file_path)
|
||||||
|
|
||||||
|
def _safe_get_tag(self, tags, easy_key: str, mp4_key: str) -> Optional[str]:
|
||||||
|
"""Get a tag value safely, handling vorbis, ID3, and MP4 tag formats."""
|
||||||
|
if tags is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try MP4 key first (for MP4/M4A files)
|
||||||
|
try:
|
||||||
|
if mp4_key in tags:
|
||||||
|
val = tags[mp4_key]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
return str(val[0])
|
||||||
|
return str(val)
|
||||||
|
except ValueError:
|
||||||
|
pass # vorbis dict raises ValueError for invalid keys
|
||||||
|
|
||||||
|
# Try vorbis comment key (case-insensitive)
|
||||||
|
try:
|
||||||
|
easy_lower = easy_key.lower()
|
||||||
|
for k in tags.keys():
|
||||||
|
if k.lower() == easy_lower:
|
||||||
|
val = tags[k]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
return str(val[0])
|
||||||
|
return str(val)
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try ID3 frame mapping for common tags
|
||||||
|
id3_map = {
|
||||||
|
'title': 'TIT2',
|
||||||
|
'artist': 'TPE1',
|
||||||
|
'album': 'TALB',
|
||||||
|
'albumartist': 'TPE2',
|
||||||
|
'genre': 'TCON',
|
||||||
|
'date': 'TDRC',
|
||||||
|
'comment': 'COMM',
|
||||||
|
}
|
||||||
|
frame_id = id3_map.get(easy_key.lower())
|
||||||
|
if frame_id and hasattr(tags, 'getall'):
|
||||||
|
try:
|
||||||
|
frames = tags.getall(frame_id)
|
||||||
|
if frames:
|
||||||
|
return str(frames[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _safe_get_tag_value(self, tags, key: str) -> Optional[str]:
|
||||||
|
"""Get raw tag value safely."""
|
||||||
|
if tags is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Direct check
|
||||||
|
try:
|
||||||
|
if key in tags:
|
||||||
|
val = tags[key]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
return str(val[0])
|
||||||
|
return str(val)
|
||||||
|
except (ValueError, KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Case-insensitive for vorbis
|
||||||
|
try:
|
||||||
|
key_lower = key.lower()
|
||||||
|
for k in tags.keys():
|
||||||
|
if k.lower() == key_lower:
|
||||||
|
val = tags[k]
|
||||||
|
if isinstance(val, list) and val:
|
||||||
|
return str(val[0])
|
||||||
|
return str(val)
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ID3 frame mapping
|
||||||
|
id3_map = {
|
||||||
|
'tracknumber': 'TRCK',
|
||||||
|
'discnumber': 'TPOS',
|
||||||
|
}
|
||||||
|
frame_id = id3_map.get(key.lower())
|
||||||
|
if frame_id and hasattr(tags, 'getall'):
|
||||||
|
try:
|
||||||
|
frames = tags.getall(frame_id)
|
||||||
|
if frames:
|
||||||
|
return str(frames[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_cover(self, audio, tags) -> Optional[bytes]:
|
||||||
|
"""Extract cover art data from audio file tags."""
|
||||||
|
# MP3 (ID3v2) - APIC frames
|
||||||
|
if hasattr(tags, 'getall'):
|
||||||
|
try:
|
||||||
|
for frame in tags.getall('APIC'):
|
||||||
|
return frame.data
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# FLAC / Vorbis comments - METADATA_BLOCK_PICTURE
|
||||||
|
try:
|
||||||
|
if 'metadata_block_picture' in tags:
|
||||||
|
pic_data = tags['metadata_block_picture']
|
||||||
|
if isinstance(pic_data, list):
|
||||||
|
pic_data = pic_data[0]
|
||||||
|
pic_data = str(pic_data)
|
||||||
|
decoded = base64.b64decode(pic_data)
|
||||||
|
# Parse the picture block: type(4), mime_len(4), mime, desc_len(4), desc, w(4), h(4), d(4), colors(4), data_len(4), data
|
||||||
|
pos = 0
|
||||||
|
pos += 4 # picture type
|
||||||
|
mime_len = struct.unpack('>I', decoded[pos:pos+4])[0]
|
||||||
|
pos += 4 + mime_len # mime string
|
||||||
|
desc_len = struct.unpack('>I', decoded[pos:pos+4])[0]
|
||||||
|
pos += 4 + desc_len # description
|
||||||
|
pos += 20 # width(4) + height(4) + depth(4) + colors(4) + data_len(4)
|
||||||
|
return decoded[pos:]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# MP4/M4A - covr
|
||||||
|
try:
|
||||||
|
if 'covr' in tags:
|
||||||
|
cover_list = tags['covr']
|
||||||
|
if isinstance(cover_list, list) and cover_list:
|
||||||
|
return bytes(cover_list[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_cover_as_jpeg(self, cover_data: bytes, output_path: str) -> None:
|
||||||
|
"""Save cover art data as JPEG."""
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
logger.debug(f"Saving cover art: {len(cover_data)} bytes")
|
||||||
|
stream = BytesIO(cover_data)
|
||||||
|
img = Image.open(stream)
|
||||||
|
img = img.convert('RGB')
|
||||||
|
max_size = 600
|
||||||
|
if img.width > max_size or img.height > max_size:
|
||||||
|
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
|
||||||
|
img.save(output_path, 'JPEG', quality=85)
|
||||||
|
logger.debug(f"Cover art saved to: {output_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not save cover art: {e}")
|
||||||
|
logger.debug(f"Cover data length: {len(cover_data)}, first bytes: {cover_data[:4].hex()}")
|
||||||
|
|
||||||
|
def _parse_filename_trackinfo(self, filename: str, file_path: Optional[str] = None) -> TrackInfo:
|
||||||
|
"""Create TrackInfo by parsing filename as fallback."""
|
||||||
|
title = filename
|
||||||
|
artist = "Unknown"
|
||||||
|
|
||||||
|
if " - " in filename:
|
||||||
|
artist, title = filename.split(" - ", 1)
|
||||||
|
|
||||||
|
return TrackInfo(
|
||||||
|
video_id=f"local_{hash(file_path or filename)}",
|
||||||
|
title=title,
|
||||||
|
artist=artist,
|
||||||
|
album="",
|
||||||
|
thumbnail_url="",
|
||||||
|
duration=0,
|
||||||
|
download_path=file_path,
|
||||||
|
)
|
||||||
|
|
||||||
def enhance_metadata_with_musicbrainz(self, track_info: TrackInfo) -> TrackInfo:
|
def enhance_metadata_with_musicbrainz(self, track_info: TrackInfo) -> TrackInfo:
|
||||||
"""
|
"""
|
||||||
Enhance track metadata using MusicBrainz API
|
Enhance track metadata using MusicBrainz API
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user