From 20c8bff85c13b954d3ebea8967ca74a25d4cdfff Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:04:37 -0700 Subject: [PATCH] Upgrade artwork to highest available resolution for Spotify and iTunes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/itunes_client.py | 18 +++++++++--------- core/spotify_client.py | 22 +++++++++++++++++++--- web_server.py | 9 ++++++++- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/core/itunes_client.py b/core/itunes_client.py index 8cd4d774..2c22afcd 100644 --- a/core/itunes_client.py +++ b/core/itunes_client.py @@ -83,8 +83,8 @@ class Track: # Extract album image (highest quality) album_image_url = None if 'artworkUrl100' in track_data: - # Replace 100x100 with 600x600 for higher quality - album_image_url = track_data['artworkUrl100'].replace('100x100bb', '600x600bb') + # Replace 100x100 with 3000x3000 for highest available quality + album_image_url = track_data['artworkUrl100'].replace('100x100bb', '3000x3000bb') # Get artist name(s) - prefer clean name from ID lookup if available if clean_artist_name: @@ -136,7 +136,7 @@ class Artist: # iTunes artist search doesn't reliably return images image_url = None if 'artworkUrl100' in artist_data: - image_url = artist_data['artworkUrl100'].replace('100x100bb', '600x600bb') + image_url = artist_data['artworkUrl100'].replace('100x100bb', '3000x3000bb') # Build external URLs external_urls = {} @@ -173,8 +173,8 @@ class Album: # Get highest quality artwork image_url = None if album_data.get('artworkUrl100'): - image_url = album_data['artworkUrl100'].replace('100x100bb', '600x600bb') - + image_url = album_data['artworkUrl100'].replace('100x100bb', '3000x3000bb') + # Build external URLs external_urls = {} if 'collectionViewUrl' in album_data: @@ -633,12 +633,12 @@ class iTunesClient: # Reconstruct Spotify-compatible format from cached raw iTunes data image_url = None if cached.get('artworkUrl100'): - image_url = cached['artworkUrl100'].replace('100x100bb', '600x600bb') + image_url = cached['artworkUrl100'].replace('100x100bb', '3000x3000bb') images = [] if image_url: 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'], 'height': 100, 'width': 100} ] @@ -681,13 +681,13 @@ class iTunesClient: # Normalize to Spotify-compatible format image_url = None 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) images = [] if image_url: 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'], 'height': 100, 'width': 100} ] diff --git a/core/spotify_client.py b/core/spotify_client.py index ffc71209..8fd09f9d 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -1,6 +1,7 @@ import spotipy from spotipy.oauth2 import SpotifyOAuth, SpotifyClientCredentials from typing import Dict, List, Optional, Any +import re import time import threading from functools import wraps @@ -11,6 +12,21 @@ from core.metadata_cache import get_metadata_cache 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 _last_api_call_time = 0 _api_call_lock = threading.Lock() @@ -352,7 +368,7 @@ class Track: if 'album' in track_data and 'images' in track_data['album']: images = track_data['album']['images'] if images: - album_image_url = images[0]['url'] + album_image_url = _upgrade_spotify_image_url(images[0]['url']) return cls( id=track_data['id'], @@ -384,7 +400,7 @@ class Artist: # Get the largest image URL if available image_url = None 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( id=artist_data['id'], @@ -413,7 +429,7 @@ class Album: # Get the largest image URL if available image_url = None 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( id=album_data['id'], diff --git a/web_server.py b/web_server.py index 62f471e8..2c2aa0a7 100644 --- a/web_server.py +++ b/web_server.py @@ -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") return - # Fallback to Spotify/iTunes/Deezer URL (typically 640x640) + # Fallback to Spotify/iTunes/Deezer URL if not image_data: art_url = album_info.get('album_image_url') # 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 if art_url: 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: print("No cover art URL available for download.") return