feat: compress editors picks images to webp
This commit is contained in:
parent
d9d6a7d7d1
commit
f1e961d4a9
3 changed files with 87 additions and 11 deletions
20
.github/workflows/editors-picks.yml
vendored
20
.github/workflows/editors-picks.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue