From 3ed9d8b5898dcace9524ff1c4d050956acdcc77a Mon Sep 17 00:00:00 2001 From: binimum Date: Sun, 5 Apr 2026 15:22:50 +0000 Subject: [PATCH] refactor: streamline image processing and cover URL handling --- .github/workflows/editors-picks.yml | 21 ++++----- fix-gen.py | 22 ++++++++++ gen-editors-picks.py | 68 +++-------------------------- js/api.js | 20 +++++++++ js/music-api.js | 11 +++++ js/player.js | 33 +++++++++++--- js/ui.js | 57 +++++++++++++++++++++--- 7 files changed, 149 insertions(+), 83 deletions(-) create mode 100644 fix-gen.py diff --git a/.github/workflows/editors-picks.yml b/.github/workflows/editors-picks.yml index 0ce8f3f..d645fb2 100644 --- a/.github/workflows/editors-picks.yml +++ b/.github/workflows/editors-picks.yml @@ -37,10 +37,6 @@ jobs: with: python-version: '3.x' - - name: Install webp - if: steps.backoff.outputs.skip == 'false' - run: sudo apt-get update && sudo apt-get install -y webp - - name: Archive current editors picks if: steps.backoff.outputs.skip == 'false' run: | @@ -68,14 +64,19 @@ jobs: with open("public/editors-picks.json") as f: current = json.load(f) - # Replace cover URLs with original UUIDs - url_pattern = re.compile(r'^https://monochrome\.tf/editors-picks-images/([a-f0-9-]+)\.webp$') + # Replace cover URLs with original UUIDs (handles both legacy and intermediate formats) + url_pattern1 = re.compile(r'^https://monochrome\.tf/editors-picks-images/([a-f0-9-]+)\.webp$') + url_pattern2 = re.compile(r'^https://wsrv\.nl/\?url=https://resources\.tidal\.com/images/([a-f0-9/]+)/320x320\.jpg&w=250&h=250&output=webp$') for item in current: for field in ['cover', 'picture', 'image']: if field in item and item[field]: - match = url_pattern.match(item[field]) - if match: - item[field] = match.group(1) + m1 = url_pattern1.match(item[field]) + if m1: + item[field] = m1.group(1) + continue + m2 = url_pattern2.match(item[field]) + if m2: + item[field] = m2.group(1).replace("/", "-") with open(archive_path, "w") as f: json.dump(current, f, indent=4) @@ -103,7 +104,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add public/editors-picks.json public/editors-picks-old/ public/editors-picks-images/ + git add public/editors-picks.json public/editors-picks-old/ git diff --staged --quiet && echo "No changes to commit." && exit 0 git commit -m "chore: update editors picks" git push diff --git a/fix-gen.py b/fix-gen.py new file mode 100644 index 0000000..7ef2989 --- /dev/null +++ b/fix-gen.py @@ -0,0 +1,22 @@ +import re +with open("gen-editors-picks.py", "r") as f: content = f.read() + +content = re.sub(r"IMAGES_DIR = \"public/editors-picks-images\"\n+", "", content) + +# Remove clear_images_dir definition +content = re.sub(r"def clear_images_dir\(\):[\s\S]*?os\.makedirs[^\n]*\n+", "", content) +content = re.sub(r"clear_images_dir\(\)\n+", "", content) + +# Remove the import line for subprocess, shutil if present +content = re.sub(r"import subprocess\n", "", content) +content = re.sub(r"import shutil\n", "", content) + +# Replace download_and_process_cover +new_func = """def download_and_process_cover(cover_uuid): + url = f"https://resources.tidal.com/images/{uuid_to_path_segments(cover_uuid)}/320x320.jpg" + return f"https://wsrv.nl/?url={url}&w=250&h=250&output=webp" +""" +content = re.sub(r"def download_and_process_cover\(cover_uuid\):[\s\S]*?(?=def process_cover)", new_func + "\n\n", content) + +with open("gen-editors-picks.py", "w") as f: f.write(content) + diff --git a/gen-editors-picks.py b/gen-editors-picks.py index e6404e0..0da586f 100644 --- a/gen-editors-picks.py +++ b/gen-editors-picks.py @@ -8,12 +8,9 @@ import sys import hashlib import time import os -import subprocess -import shutil import tempfile INPUT_FILE = "editors-picks-input.txt" -IMAGES_DIR = "public/editors-picks-images" COUNTRY = "US" # Tidal internal token replace when expired @@ -90,59 +87,6 @@ def fetch_podcast(feed_id): # ── Image processing ─────────────────────────────────────────────────────────── -def clear_images_dir(): - if os.path.exists(IMAGES_DIR): - shutil.rmtree(IMAGES_DIR) - os.makedirs(IMAGES_DIR, exist_ok=True) - - -def is_uuid_cover(cover_value): - if not cover_value: - return False - return bool(re.match(r'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$', cover_value)) - - -def uuid_to_path_segments(uuid): - return uuid.replace('-', '/') - - -def download_and_process_cover(cover_uuid): - url = f"https://resources.tidal.com/images/{uuid_to_path_segments(cover_uuid)}/320x320.jpg" - - with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp: - tmp_path = tmp.name - - try: - req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) - with urllib.request.urlopen(req) as resp: - with open(tmp_path, 'wb') as f: - shutil.copyfileobj(resp, f) - - output_path = os.path.join(IMAGES_DIR, f"{cover_uuid}.webp") - - subprocess.run( - ['cwebp', '-q', '50', tmp_path, '-o', output_path], - check=True, - capture_output=True - ) - - return f"https://monochrome.tf/editors-picks-images/{cover_uuid}.webp" - except Exception as e: - print(f"Error processing cover {cover_uuid}: {e}", file=sys.stderr) - return None - finally: - if os.path.exists(tmp_path): - os.remove(tmp_path) - - -def process_cover(cover_value): - if not cover_value: - return cover_value - if is_uuid_cover(cover_value): - return download_and_process_cover(cover_value) - return cover_value - - # ── Transformers ────────────────────────────────────────────────────────────── def transform_album(d): @@ -155,7 +99,7 @@ def transform_album(d): "name": d.get("artist", {}).get("name"), }, "releaseDate": d.get("releaseDate"), - "cover": process_cover(d.get("cover")), + "cover": d.get("cover"), "explicit": d.get("explicit"), "audioQuality": d.get("audioQuality"), "mediaMetadata": d.get("mediaMetadata"), @@ -167,7 +111,7 @@ def transform_artist(d): "type": "artist", "id": d.get("id"), "name": d.get("name"), - "picture": process_cover(d.get("picture")), + "picture": d.get("picture"), } @@ -184,7 +128,7 @@ def transform_track(d): "album": { "id": album.get("id"), "title": album.get("title"), - "cover": process_cover(album.get("cover")), + "cover": album.get("cover"), }, "duration": d.get("duration"), "explicit": d.get("explicit"), @@ -200,7 +144,7 @@ def transform_playlist(d): "type": "playlist", "id": d.get("uuid"), "title": d.get("title"), - "cover": process_cover(cover), + "cover": cover, "numberOfTracks": d.get("numberOfTracks", 0), } @@ -213,7 +157,7 @@ def transform_userplaylist(d): "type": "user-playlist", "id": d.get("uuid"), "name": d.get("title"), - "cover": process_cover(cover), + "cover": cover, "numberOfTracks": d.get("numberOfTracks", 0), "username": creator.get("name"), } @@ -258,8 +202,6 @@ def read_items(path): # ── Main ────────────────────────────────────────────────────────────────────── -clear_images_dir() - FETCHERS = { "album": (fetch_album, transform_album), "artist": (fetch_artist, transform_artist), diff --git a/js/api.js b/js/api.js index d5ae23d..cf8bc17 100644 --- a/js/api.js +++ b/js/api.js @@ -1952,6 +1952,16 @@ export class LosslessAPI { return `https://resources.tidal.com/images/${formattedId}/${size}x${size}.jpg`; } + getCoverSrcset(id) { + if (!id || (typeof id === 'string' && (id.startsWith('http') || id.startsWith('blob:') || id.startsWith('assets/')))) { + return ''; + } + + const formattedId = String(id).replace(/-/g, '/'); + const baseUrl = `https://resources.tidal.com/images/${formattedId}`; + return `${baseUrl}/160x160.jpg 160w, ${baseUrl}/320x320.jpg 320w, ${baseUrl}/640x640.jpg 640w`; + } + getArtistPictureUrl(id, size = '320') { if (!id) { return `https://picsum.photos/seed/${Math.random()}/${size}`; @@ -1965,6 +1975,16 @@ export class LosslessAPI { return `https://resources.tidal.com/images/${formattedId}/${size}x${size}.jpg`; } + getArtistPictureSrcset(id) { + if (!id || (typeof id === 'string' && (id.startsWith('blob:') || id.startsWith('assets/')))) { + return ''; + } + + const formattedId = String(id).replace(/-/g, '/'); + const baseUrl = `https://resources.tidal.com/images/${formattedId}`; + return `${baseUrl}/160x160.jpg 160w, ${baseUrl}/320x320.jpg 320w, ${baseUrl}/640x640.jpg 640w`; + } + getVideoCoverUrl(imageId, size = '1280') { if (!imageId) { return null; diff --git a/js/music-api.js b/js/music-api.js index 0888c9f..1590452 100644 --- a/js/music-api.js +++ b/js/music-api.js @@ -223,6 +223,13 @@ export class MusicAPI { return this.tidalAPI.getCoverUrl(this.stripProviderPrefix(id), size); } + getCoverSrcset(id) { + if (typeof id === 'string' && id.startsWith('blob:')) { + return ''; + } + return this.tidalAPI.getCoverSrcset(this.stripProviderPrefix(id)); + } + getVideoCoverUrl(imageId, size = '1280') { if (!imageId) { return null; @@ -260,6 +267,10 @@ export class MusicAPI { return this.tidalAPI.getArtistPictureUrl(this.stripProviderPrefix(id), size); } + getArtistPictureSrcset(id) { + return this.tidalAPI.getArtistPictureSrcset(this.stripProviderPrefix(id)); + } + extractStreamUrlFromManifest(manifest) { return this.tidalAPI.extractStreamUrlFromManifest(manifest); } diff --git a/js/player.js b/js/player.js index 5b779a0..3f4032a 100644 --- a/js/player.js +++ b/js/player.js @@ -307,8 +307,10 @@ export class Player { if (coverEl) { const videoCoverUrl = track.videoUrl || track.videoCoverUrl || track.album?.videoCoverUrl || null; + const coverId = track.image || track.cover || track.album?.cover; const coverUrl = - videoCoverUrl || this.api.getCoverUrl(track.image || track.cover || track.album?.cover); + videoCoverUrl || this.api.getCoverUrl(coverId); + const coverSrcset = videoCoverUrl ? null : this.api.getCoverSrcset(coverId); if (videoCoverUrl) { if (coverEl.tagName === 'IMG') { @@ -326,14 +328,24 @@ export class Player { coverEl.src = videoCoverUrl; } } else { + const setImgSrcset = (img) => { + if (img.getAttribute('src') !== coverUrl) img.src = coverUrl; + if (coverSrcset) { + img.setAttribute('srcset', coverSrcset); + img.setAttribute('sizes', '(max-width: 640px) 160px, (max-width: 1024px) 320px, 640px'); + } else { + img.removeAttribute('srcset'); + img.removeAttribute('sizes'); + } + }; if (coverEl.tagName === 'VIDEO') { const img = document.createElement('img'); - img.src = coverUrl; img.className = coverEl.className; img.id = coverEl.id; + setImgSrcset(img); coverEl.replaceWith(img); } else { - coverEl.src = coverUrl; + setImgSrcset(coverEl); } } } @@ -870,8 +882,19 @@ export class Player { } else { if (coverEl) { coverEl.style.display = 'block'; - const coverUrl = this.api.getCoverUrl(track.image || track.cover || track.album?.cover); - if (coverEl.src !== coverUrl) coverEl.src = coverUrl; + const coverId = track.image || track.cover || track.album?.cover; + const coverUrl = this.api.getCoverUrl(coverId); + const coverSrcset = this.api.getCoverSrcset(coverId); + if (coverEl.getAttribute('src') !== coverUrl) { + coverEl.src = coverUrl; + if (coverSrcset) { + coverEl.setAttribute('srcset', coverSrcset); + coverEl.setAttribute('sizes', '(max-width: 640px) 160px, (max-width: 1024px) 320px, 640px'); + } else { + coverEl.removeAttribute('srcset'); + coverEl.removeAttribute('sizes'); + } + } } if (this.audio) { const isInFullscreen = document.getElementById('fullscreen-cover-overlay')?.style.display === 'flex'; diff --git a/js/ui.js b/js/ui.js index 6d4d6e2..9ea45bf 100644 --- a/js/ui.js +++ b/js/ui.js @@ -543,11 +543,27 @@ export class UIRenderer { `; } - getCoverHTML(cover, alt, className = 'card-image', loading = 'lazy', videoCoverUrl = null) { - const imageUrl = this.api.getCoverUrl(cover); + getCoverHTML(cover, alt, className = 'card-image', loading = 'lazy', videoCoverUrl = null, isEditorsPick = false, type = 'album') { + let size = '320'; + if (this.currentPage === 'search' || className === 'track-item-cover') { + size = '80'; + } else if (type === 'artist') { + size = '160'; + } + + const imageUrl = type === 'artist' ? this.api.getArtistPictureUrl(cover, size) : this.api.getCoverUrl(cover, size); + if (videoCoverUrl) { return ``; } + + if (isEditorsPick && cover && typeof cover === 'string' && !cover.startsWith('http') && !cover.startsWith('blob:') && !cover.startsWith('assets/')) { + const formattedId = String(cover).replace(/-/g, '/'); + const tidalUrl = `https://resources.tidal.com/images/${formattedId}/320x320.jpg`; + const wsrvUrl = `https://wsrv.nl/?url=${encodeURIComponent(tidalUrl)}&w=250&h=250&output=webp`; + return `${alt}`; + } + return `${alt}`; } @@ -658,7 +674,15 @@ export class UIRenderer { createUserPlaylistCardHTML(playlist, customSubtitle = null) { let imageHTML = ''; if (playlist.cover) { - imageHTML = `${playlist.name}`; + imageHTML = this.getCoverHTML( + playlist.cover, + escapeHtml(playlist.name), + 'card-image', + playlist._lazy === false ? 'eager' : 'lazy', + null, + playlist._isEditorsPick || false, + 'album' + ); } else { const tracks = playlist.tracks || []; let uniqueCovers = playlist.images || []; @@ -750,7 +774,9 @@ export class UIRenderer { escapeHtml(album.title), 'card-image', album._lazy === false ? 'eager' : 'lazy', - album.videoCoverUrl + album.videoCoverUrl, + album._isEditorsPick || false, + 'album' ), actionButtonsHTML: `