Add Cover Art Archive as high-resolution album art source
Album art embedding now tries Cover Art Archive first (using the MusicBrainz release ID from source ID embedding) before falling back to Spotify/iTunes/Deezer URLs. CAA provides original-quality artwork, often 1200x1200 or higher vs Spotify's 640x640 max. Reordered _embed_source_ids to run before _embed_album_art_metadata so the MusicBrainz release ID is available for the CAA lookup. Also fixed hardcoded 640x640 FLAC picture dimensions — now detects actual size from image bytes. Falls back to existing behavior if CAA fails or no release ID exists.
This commit is contained in:
parent
7cff379aa7
commit
a803e8399c
2 changed files with 79 additions and 14 deletions
|
|
@ -15786,13 +15786,15 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
|
||||||
if metadata.get('disc_number'):
|
if metadata.get('disc_number'):
|
||||||
audio_file['disk'] = [(metadata['disc_number'], 0)]
|
audio_file['disk'] = [(metadata['disc_number'], 0)]
|
||||||
|
|
||||||
|
# ── Embed source IDs (Spotify, MusicBrainz, etc.) on the same object ──
|
||||||
|
# Runs before album art so MusicBrainz release ID is available for
|
||||||
|
# Cover Art Archive high-resolution lookup.
|
||||||
|
_embed_source_ids(audio_file, metadata)
|
||||||
|
|
||||||
# ── Embed album art on the same object ──
|
# ── Embed album art on the same object ──
|
||||||
if config_manager.get('metadata_enhancement.embed_album_art', True):
|
if config_manager.get('metadata_enhancement.embed_album_art', True):
|
||||||
_embed_album_art_metadata(audio_file, metadata)
|
_embed_album_art_metadata(audio_file, metadata)
|
||||||
|
|
||||||
# ── Embed source IDs (Spotify, MusicBrainz, etc.) on the same object ──
|
|
||||||
_embed_source_ids(audio_file, metadata)
|
|
||||||
|
|
||||||
# ── Embed audio quality tag ──
|
# ── Embed audio quality tag ──
|
||||||
quality = context.get('_audio_quality', '')
|
quality = context.get('_audio_quality', '')
|
||||||
if quality and config_manager.get('metadata_enhancement.tags.quality_tag', True) is not False:
|
if quality and config_manager.get('metadata_enhancement.tags.quality_tag', True) is not False:
|
||||||
|
|
@ -16014,17 +16016,64 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
def _embed_album_art_metadata(audio_file, metadata: dict):
|
def _get_image_dimensions(data: bytes):
|
||||||
"""Downloads and embeds high-quality Spotify album art into the file."""
|
"""Extract width/height from JPEG or PNG image data without PIL."""
|
||||||
try:
|
try:
|
||||||
art_url = metadata.get('album_art_url')
|
if data[:8] == b'\x89PNG\r\n\x1a\n':
|
||||||
if not art_url:
|
# PNG: width and height at bytes 16-23
|
||||||
print("🎨 No album art URL available for embedding.")
|
import struct
|
||||||
return
|
w, h = struct.unpack('>II', data[16:24])
|
||||||
|
return w, h
|
||||||
|
if data[:2] == b'\xff\xd8':
|
||||||
|
# JPEG: scan for SOF0/SOF2 marker
|
||||||
|
import struct
|
||||||
|
i = 2
|
||||||
|
while i < len(data) - 9:
|
||||||
|
if data[i] != 0xFF:
|
||||||
|
break
|
||||||
|
marker = data[i + 1]
|
||||||
|
if marker in (0xC0, 0xC2):
|
||||||
|
h, w = struct.unpack('>HH', data[i + 5:i + 9])
|
||||||
|
return w, h
|
||||||
|
length = struct.unpack('>H', data[i + 2:i + 4])[0]
|
||||||
|
i += 2 + length
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None, None
|
||||||
|
|
||||||
with urllib.request.urlopen(art_url, timeout=10) as response:
|
|
||||||
image_data = response.read()
|
def _embed_album_art_metadata(audio_file, metadata: dict):
|
||||||
mime_type = response.info().get_content_type()
|
"""Downloads and embeds album art — tries Cover Art Archive (full resolution)
|
||||||
|
first if MusicBrainz release ID is available, falls back to Spotify/iTunes URL."""
|
||||||
|
try:
|
||||||
|
image_data = None
|
||||||
|
mime_type = None
|
||||||
|
|
||||||
|
# Try Cover Art Archive first (often 1200x1200+, original quality)
|
||||||
|
release_mbid = metadata.get('musicbrainz_release_id')
|
||||||
|
if release_mbid:
|
||||||
|
try:
|
||||||
|
caa_url = f"https://coverartarchive.org/release/{release_mbid}/front"
|
||||||
|
req = urllib.request.Request(caa_url, headers={'Accept': 'image/*'})
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as response:
|
||||||
|
image_data = response.read()
|
||||||
|
mime_type = response.info().get_content_type() or 'image/jpeg'
|
||||||
|
if image_data and len(image_data) > 1000:
|
||||||
|
print(f"🎨 Cover art from Cover Art Archive ({len(image_data) // 1024}KB)")
|
||||||
|
else:
|
||||||
|
image_data = None # Too small, likely an error page
|
||||||
|
except Exception:
|
||||||
|
image_data = None # Fall through to Spotify/iTunes URL
|
||||||
|
|
||||||
|
# Fallback to Spotify/iTunes/Deezer URL (typically 640x640)
|
||||||
|
if not image_data:
|
||||||
|
art_url = metadata.get('album_art_url')
|
||||||
|
if not art_url:
|
||||||
|
print("🎨 No album art URL available for embedding.")
|
||||||
|
return
|
||||||
|
with urllib.request.urlopen(art_url, timeout=10) as response:
|
||||||
|
image_data = response.read()
|
||||||
|
mime_type = response.info().get_content_type()
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
print("❌ Failed to download album art data.")
|
print("❌ Failed to download album art data.")
|
||||||
|
|
@ -16039,8 +16088,10 @@ def _embed_album_art_metadata(audio_file, metadata: dict):
|
||||||
picture.data = image_data
|
picture.data = image_data
|
||||||
picture.type = 3
|
picture.type = 3
|
||||||
picture.mime = mime_type
|
picture.mime = mime_type
|
||||||
picture.width = 640
|
# Detect actual dimensions from image data
|
||||||
picture.height = 640
|
_img_w, _img_h = _get_image_dimensions(image_data)
|
||||||
|
picture.width = _img_w or 640
|
||||||
|
picture.height = _img_h or 640
|
||||||
picture.depth = 24
|
picture.depth = 24
|
||||||
audio_file.add_picture(picture)
|
audio_file.add_picture(picture)
|
||||||
# MP4/M4A
|
# MP4/M4A
|
||||||
|
|
@ -16209,6 +16260,10 @@ def _embed_source_ids(audio_file, metadata: dict):
|
||||||
if yr.isdigit():
|
if yr.isdigit():
|
||||||
release_year = yr
|
release_year = yr
|
||||||
|
|
||||||
|
# Store release MBID in metadata for downstream use (e.g. Cover Art Archive)
|
||||||
|
if _rc_mbid:
|
||||||
|
metadata['musicbrainz_release_id'] = _rc_mbid
|
||||||
|
|
||||||
# Write release year to file tags if not already present
|
# Write release year to file tags if not already present
|
||||||
if release_year and 'ORIGINALDATE' not in id_tags:
|
if release_year and 'ORIGINALDATE' not in id_tags:
|
||||||
id_tags['ORIGINALDATE'] = release_year
|
id_tags['ORIGINALDATE'] = release_year
|
||||||
|
|
@ -19009,6 +19064,15 @@ def get_version_info():
|
||||||
"title": "What's New in SoulSync",
|
"title": "What's New in SoulSync",
|
||||||
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
||||||
"sections": [
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "🎨 High-Resolution Cover Art from Cover Art Archive",
|
||||||
|
"description": "Album art now sourced from Cover Art Archive when available — often 1200x1200+ original quality",
|
||||||
|
"features": [
|
||||||
|
"• Tries Cover Art Archive first using MusicBrainz release ID (full resolution)",
|
||||||
|
"• Falls back to Spotify/iTunes/Deezer URL (640x640) if CAA unavailable",
|
||||||
|
"• Source ID embedding now runs before art embedding to make release ID available"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "🎵 Embedded Lyrics in Audio Files",
|
"title": "🎵 Embedded Lyrics in Audio Files",
|
||||||
"description": "Lyrics are now embedded directly in audio file tags alongside the .lrc sidecar file",
|
"description": "Lyrics are now embedded directly in audio file tags alongside the .lrc sidecar file",
|
||||||
|
|
|
||||||
|
|
@ -3403,6 +3403,7 @@ function closeHelperSearch() {
|
||||||
const WHATS_NEW = {
|
const WHATS_NEW = {
|
||||||
'2.1': [
|
'2.1': [
|
||||||
// Newest features first
|
// Newest features first
|
||||||
|
{ title: 'High-Res Cover Art', desc: 'Album art now sourced from Cover Art Archive (1200x1200+) when MusicBrainz release ID is available' },
|
||||||
{ title: 'Embedded Lyrics', desc: 'Lyrics now embedded directly in audio file tags — Navidrome, Jellyfin, and Plex can display them' },
|
{ title: 'Embedded Lyrics', desc: 'Lyrics now embedded directly in audio file tags — Navidrome, Jellyfin, and Plex can display them' },
|
||||||
{ title: 'Fix AcoustID False Positives', desc: 'High-confidence fingerprints no longer quarantine correct files when titles are in different languages' },
|
{ title: 'Fix AcoustID False Positives', desc: 'High-confidence fingerprints no longer quarantine correct files when titles are in different languages' },
|
||||||
{ title: 'Fix Junk Tags Surviving', desc: 'Soulseek source tags are now wiped to disk immediately — no more album fragmentation from partial metadata' },
|
{ title: 'Fix Junk Tags Surviving', desc: 'Soulseek source tags are now wiped to disk immediately — no more album fragmentation from partial metadata' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue