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 `
`;
+ }
+
return `
`;
}
@@ -658,7 +674,15 @@ export class UIRenderer {
createUserPlaylistCardHTML(playlist, customSubtitle = null) {
let imageHTML = '';
if (playlist.cover) {
- imageHTML = `
`;
+ 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: `