Upgrade artwork to highest available resolution for Spotify and iTunes

Spotify album art: replace the 4-char size segment after '0000ab67616d'
with '82c1' to request the original uploaded master (up to 2000px+).
Applied via _upgrade_spotify_image_url() in Track, Artist, and Album
dataclass constructors and as a catch-all in _download_cover_art.
Scoped to the ab67616d album art prefix only — artist images use a
different prefix (ab676161) where the trick does not apply.

iTunes/Apple Music: replace '100x100bb' with '3000x3000bb' in all
artworkUrl100 replacements across Track, Artist, Album, and the
get_album images arrays. Also applied as a catch-all in _download_cover_art.

Deezer already uses cover_xl at its maximum — no changes needed there.
This commit is contained in:
Broque Thomas 2026-04-13 15:04:37 -07:00
parent f851a67d6b
commit 20c8bff85c
3 changed files with 36 additions and 13 deletions

View file

@ -83,8 +83,8 @@ class Track:
# Extract album image (highest quality) # Extract album image (highest quality)
album_image_url = None album_image_url = None
if 'artworkUrl100' in track_data: if 'artworkUrl100' in track_data:
# Replace 100x100 with 600x600 for higher quality # Replace 100x100 with 3000x3000 for highest available quality
album_image_url = track_data['artworkUrl100'].replace('100x100bb', '600x600bb') album_image_url = track_data['artworkUrl100'].replace('100x100bb', '3000x3000bb')
# Get artist name(s) - prefer clean name from ID lookup if available # Get artist name(s) - prefer clean name from ID lookup if available
if clean_artist_name: if clean_artist_name:
@ -136,7 +136,7 @@ class Artist:
# iTunes artist search doesn't reliably return images # iTunes artist search doesn't reliably return images
image_url = None image_url = None
if 'artworkUrl100' in artist_data: if 'artworkUrl100' in artist_data:
image_url = artist_data['artworkUrl100'].replace('100x100bb', '600x600bb') image_url = artist_data['artworkUrl100'].replace('100x100bb', '3000x3000bb')
# Build external URLs # Build external URLs
external_urls = {} external_urls = {}
@ -173,8 +173,8 @@ class Album:
# Get highest quality artwork # Get highest quality artwork
image_url = None image_url = None
if album_data.get('artworkUrl100'): if album_data.get('artworkUrl100'):
image_url = album_data['artworkUrl100'].replace('100x100bb', '600x600bb') image_url = album_data['artworkUrl100'].replace('100x100bb', '3000x3000bb')
# Build external URLs # Build external URLs
external_urls = {} external_urls = {}
if 'collectionViewUrl' in album_data: if 'collectionViewUrl' in album_data:
@ -633,12 +633,12 @@ class iTunesClient:
# Reconstruct Spotify-compatible format from cached raw iTunes data # Reconstruct Spotify-compatible format from cached raw iTunes data
image_url = None image_url = None
if cached.get('artworkUrl100'): if cached.get('artworkUrl100'):
image_url = cached['artworkUrl100'].replace('100x100bb', '600x600bb') image_url = cached['artworkUrl100'].replace('100x100bb', '3000x3000bb')
images = [] images = []
if image_url: if image_url:
images = [ images = [
{'url': image_url, 'height': 600, 'width': 600}, {'url': image_url, 'height': 3000, 'width': 3000},
{'url': cached['artworkUrl100'].replace('100x100bb', '300x300bb'), 'height': 300, 'width': 300}, {'url': cached['artworkUrl100'].replace('100x100bb', '300x300bb'), 'height': 300, 'width': 300},
{'url': cached['artworkUrl100'], 'height': 100, 'width': 100} {'url': cached['artworkUrl100'], 'height': 100, 'width': 100}
] ]
@ -681,13 +681,13 @@ class iTunesClient:
# Normalize to Spotify-compatible format # Normalize to Spotify-compatible format
image_url = None image_url = None
if album_data.get('artworkUrl100'): if album_data.get('artworkUrl100'):
image_url = album_data['artworkUrl100'].replace('100x100bb', '600x600bb') image_url = album_data['artworkUrl100'].replace('100x100bb', '3000x3000bb')
# Build images array like Spotify (multiple sizes) # Build images array like Spotify (multiple sizes)
images = [] images = []
if image_url: if image_url:
images = [ images = [
{'url': image_url, 'height': 600, 'width': 600}, {'url': image_url, 'height': 3000, 'width': 3000},
{'url': album_data['artworkUrl100'].replace('100x100bb', '300x300bb'), 'height': 300, 'width': 300}, {'url': album_data['artworkUrl100'].replace('100x100bb', '300x300bb'), 'height': 300, 'width': 300},
{'url': album_data['artworkUrl100'], 'height': 100, 'width': 100} {'url': album_data['artworkUrl100'], 'height': 100, 'width': 100}
] ]

View file

@ -1,6 +1,7 @@
import spotipy import spotipy
from spotipy.oauth2 import SpotifyOAuth, SpotifyClientCredentials from spotipy.oauth2 import SpotifyOAuth, SpotifyClientCredentials
from typing import Dict, List, Optional, Any from typing import Dict, List, Optional, Any
import re
import time import time
import threading import threading
from functools import wraps from functools import wraps
@ -11,6 +12,21 @@ from core.metadata_cache import get_metadata_cache
logger = get_logger("spotify_client") logger = get_logger("spotify_client")
def _upgrade_spotify_image_url(url: str) -> str:
"""Upgrade a Spotify CDN image URL to the highest available resolution.
Album art URLs use the prefix 'ab67616d' and encode size as the 4-char hex
segment following '0000' (e.g. 'b273' = 640x640, '1e02' = 300x300).
Replacing it with '82c1' requests the original uploaded master (up to 2000px+).
This only applies to album art (ab67616d prefix). Artist images use a different
prefix (ab676161) and the 82c1 trick does not work for them those are left
as-is since Spotify already returns them sorted largest-first.
"""
if not url or 'i.scdn.co' not in url:
return url
return re.sub(r'(/image/ab67616d)0000[0-9a-f]{4}', r'\g<1>000082c1', url)
# Global rate limiting variables # Global rate limiting variables
_last_api_call_time = 0 _last_api_call_time = 0
_api_call_lock = threading.Lock() _api_call_lock = threading.Lock()
@ -352,7 +368,7 @@ class Track:
if 'album' in track_data and 'images' in track_data['album']: if 'album' in track_data and 'images' in track_data['album']:
images = track_data['album']['images'] images = track_data['album']['images']
if images: if images:
album_image_url = images[0]['url'] album_image_url = _upgrade_spotify_image_url(images[0]['url'])
return cls( return cls(
id=track_data['id'], id=track_data['id'],
@ -384,7 +400,7 @@ class Artist:
# Get the largest image URL if available # Get the largest image URL if available
image_url = None image_url = None
if artist_data.get('images') and len(artist_data['images']) > 0: if artist_data.get('images') and len(artist_data['images']) > 0:
image_url = artist_data['images'][0]['url'] image_url = _upgrade_spotify_image_url(artist_data['images'][0]['url'])
return cls( return cls(
id=artist_data['id'], id=artist_data['id'],
@ -413,7 +429,7 @@ class Album:
# Get the largest image URL if available # Get the largest image URL if available
image_url = None image_url = None
if album_data.get('images') and len(album_data['images']) > 0: if album_data.get('images') and len(album_data['images']) > 0:
image_url = album_data['images'][0]['url'] image_url = _upgrade_spotify_image_url(album_data['images'][0]['url'])
return cls( return cls(
id=album_data['id'], id=album_data['id'],

View file

@ -19127,7 +19127,7 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
print(f"CAA upgrade failed — keeping existing cover.jpg") print(f"CAA upgrade failed — keeping existing cover.jpg")
return return
# Fallback to Spotify/iTunes/Deezer URL (typically 640x640) # Fallback to Spotify/iTunes/Deezer URL
if not image_data: if not image_data:
art_url = album_info.get('album_image_url') art_url = album_info.get('album_image_url')
# If album_info lacks the URL, try the context's spotify_album # If album_info lacks the URL, try the context's spotify_album
@ -19141,6 +19141,13 @@ def _download_cover_art(album_info: dict, target_dir: str, context: dict = None)
art_url = images[0].get('url') if isinstance(images[0], dict) else None art_url = images[0].get('url') if isinstance(images[0], dict) else None
if art_url: if art_url:
print(f"Using cover art URL from spotify_album context") print(f"Using cover art URL from spotify_album context")
# Upgrade to highest available resolution before fetching
if art_url and 'i.scdn.co' in art_url:
from core.spotify_client import _upgrade_spotify_image_url
art_url = _upgrade_spotify_image_url(art_url)
elif art_url and 'mzstatic.com' in art_url:
import re as _re
art_url = _re.sub(r'\d+x\d+bb', '3000x3000bb', art_url)
if not art_url: if not art_url:
print("No cover art URL available for download.") print("No cover art URL available for download.")
return return