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 "Files changed in this commit:"
echo "$CHANGED" echo "$CHANGED"
if echo "$CHANGED" | grep -qE '^public/editors-picks\.json$|^public/editors-picks-old/'; then 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" echo "skip=true" >> "$GITHUB_OUTPUT"
else else
echo "skip=false" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT"
@ -37,6 +37,10 @@ jobs:
with: with:
python-version: '3.x' 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 - name: Archive current editors picks
if: steps.backoff.outputs.skip == 'false' if: steps.backoff.outputs.skip == 'false'
run: | run: |
@ -60,9 +64,19 @@ jobs:
label = m.group(1).strip() label = m.group(1).strip()
break break
# Copy current picks to archive # Copy current picks to archive, replacing monochrome URLs with UUIDs
with open("public/editors-picks.json") as f: with open("public/editors-picks.json") as f:
current = json.load(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: with open(archive_path, "w") as f:
json.dump(current, f, indent=4) json.dump(current, f, indent=4)
@ -89,7 +103,7 @@ jobs:
run: | run: |
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" 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 diff --staged --quiet && echo "No changes to commit." && exit 0
git commit -m "chore: update editors picks" git commit -m "chore: update editors picks"
git push git push

View file

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

View file

@ -145,7 +145,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
if (explicit !== undefined) { if (explicit !== undefined) {
if (isMp4) { 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(); const mp4Tag = underlying.tag();
mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0)); mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0));
} else { } else {