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 "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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue