feat: compress editors picks images to webp

This commit is contained in:
edideaur 2026-04-05 01:05:22 +00:00 committed by GitHub
parent d9d6a7d7d1
commit f1e961d4a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 87 additions and 11 deletions

View file

@ -25,7 +25,7 @@ jobs:
echo "Files changed in this commit:"
echo "$CHANGED"
if echo "$CHANGED" | grep -qE '^public/editors-picks\.json$|^public/editors-picks-old/'; then
echo "Detected changes to generated files in this commit backing off to avoid overwriting manual edits."
echo "Detected changes to generated files in this commit - backing off to avoid overwriting manual edits."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
@ -37,6 +37,10 @@ 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: |
@ -60,9 +64,19 @@ jobs:
label = m.group(1).strip()
break
# Copy current picks to archive
# Copy current picks to archive, replacing monochrome URLs with UUIDs
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$')
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)
with open(archive_path, "w") as f:
json.dump(current, f, indent=4)
@ -89,7 +103,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/
git add public/editors-picks.json public/editors-picks-old/ public/editors-picks-images/
git diff --staged --quiet && echo "No changes to commit." && exit 0
git commit -m "chore: update editors picks"
git push

View file

@ -7,11 +7,16 @@ import re
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
# Tidal internal token replace when expired
TIDAL_TOKEN = "eyJraWQiOiJ2OU1GbFhqWSIsImFsZyI6IkVTMjU2In0.eyJ0eXBlIjoibzJfYWNjZXNzIiwic2NvcGUiOiIiLCJnVmVyIjowLCJzVmVyIjowLCJjaWQiOjEzNTU3LCJhdCI6IklOVEVSTkFMIiwiZXhwIjoxNzc1MjQ2NzQ2LCJpc3MiOiJodHRwczovL2F1dGgudGlkYWwuY29tL3YxIn0.ksUE4yhQ39IG7oHWk8DyJ91dwIoDVWGzvTAnpeDJ5p-_Gp0F_yO858xDO11AINBaahQCq0jlbqWqIaTqCTOjqg"
TIDAL_HEADERS = {
@ -83,6 +88,61 @@ def fetch_podcast(feed_id):
return podcast_get(f"/podcasts/byfeedid?id={feed_id}&pretty")
# ── 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)}/640x640.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', '-resize', '500', '500', 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):
@ -95,7 +155,7 @@ def transform_album(d):
"name": d.get("artist", {}).get("name"),
},
"releaseDate": d.get("releaseDate"),
"cover": d.get("cover"),
"cover": process_cover(d.get("cover")),
"explicit": d.get("explicit"),
"audioQuality": d.get("audioQuality"),
"mediaMetadata": d.get("mediaMetadata"),
@ -107,7 +167,7 @@ def transform_artist(d):
"type": "artist",
"id": d.get("id"),
"name": d.get("name"),
"picture": d.get("picture"),
"picture": process_cover(d.get("picture")),
}
@ -124,7 +184,7 @@ def transform_track(d):
"album": {
"id": album.get("id"),
"title": album.get("title"),
"cover": album.get("cover"),
"cover": process_cover(album.get("cover")),
},
"duration": d.get("duration"),
"explicit": d.get("explicit"),
@ -140,7 +200,7 @@ def transform_playlist(d):
"type": "playlist",
"id": d.get("uuid"),
"title": d.get("title"),
"cover": cover,
"cover": process_cover(cover),
"numberOfTracks": d.get("numberOfTracks", 0),
}
@ -153,7 +213,7 @@ def transform_userplaylist(d):
"type": "user-playlist",
"id": d.get("uuid"),
"name": d.get("title"),
"cover": cover,
"cover": process_cover(cover),
"numberOfTracks": d.get("numberOfTracks", 0),
"username": creator.get("name"),
}
@ -198,6 +258,8 @@ def read_items(path):
# ── Main ──────────────────────────────────────────────────────────────────────
clear_images_dir()
FETCHERS = {
"album": (fetch_album, transform_album),
"artist": (fetch_artist, transform_artist),
@ -212,7 +274,7 @@ picks = []
for item_type, item_id in items:
if item_type not in FETCHERS:
print(f"Unknown type '{item_type}' for id {item_id!r} skipping", file=sys.stderr)
print(f"Unknown type '{item_type}' for id {item_id!r} - skipping", file=sys.stderr)
continue
fetch_fn, transform_fn = FETCHERS[item_type]
data = fetch_fn(item_id)

View file

@ -145,7 +145,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
if (explicit !== undefined) {
if (isMp4) {
// rtng is a byte item must be set directly on the Mp4Tag
// rtng is a byte item - must be set directly on the Mp4Tag
const mp4Tag = underlying.tag();
mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0));
} else {