7e69182d7a
Fetches Radio France RSS feeds to build show cover images (160x160 RGB565 LVGL .bin) and writes a v2 manifest.json grouped by show, preserving episode titles from the existing flat v1 manifest. Runs with the IDF Python venv (pillow + pypng already installed).
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
|
# Build v2 manifest (grouped by show) + LVGL .bin covers for the Lisael Box.
|
|
import json, os, re, subprocess, sys, urllib.request, xml.etree.ElementTree as ET
|
|
from PIL import Image
|
|
import io
|
|
|
|
OUT = "/tmp/lisael_episodes"
|
|
LVGLIMG = "/tmp/lisael-box/managed_components/lvgl__lvgl/scripts/LVGLImage.py"
|
|
UA = {"User-Agent": "Mozilla/5.0"}
|
|
NS = {"itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd"}
|
|
|
|
# key -> (show title, RSS url)
|
|
FEEDS = {
|
|
"oli": ("Une histoire et Oli", "https://radiofrance-podcast.net/podcast09/rss_19721.xml"),
|
|
"odyssees": ("Les Odyssees", "https://radiofrance-podcast.net/podcast09/rss_20108.xml"),
|
|
"salutinfo": ("Salut l'info", "https://radiofrance-podcast.net/podcast09/rss_20689.xml"),
|
|
"bestioles": ("Bestioles",
|
|
"https://radiofrance-podcast.net/podcast09/35099478-7c72-4f9e-a6de-1b928400e9e5/"
|
|
"podcast_a80ecbd5-df3d-4c9d-bee7-4e3d9efc1974.xml"),
|
|
}
|
|
|
|
def fetch(url, t=60):
|
|
return urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=t).read()
|
|
|
|
def cover_url(root):
|
|
el = root.find(".//channel/itunes:image", NS)
|
|
if el is not None and el.get("href"):
|
|
return el.get("href")
|
|
el = root.find(".//channel/image/url")
|
|
return el.text if el is not None else None
|
|
|
|
titles = {}
|
|
v1 = os.path.join(OUT, "manifest.json.v1")
|
|
if os.path.exists(v1):
|
|
for e in json.load(open(v1, encoding="utf-8")):
|
|
titles[e["file"]] = e["title"]
|
|
|
|
manifest = []
|
|
for key, (show, rss) in FEEDS.items():
|
|
eps = []
|
|
for f in sorted(os.listdir(OUT)):
|
|
if re.match(rf"^{key}_\d+\.(m4a|mp3)$", f):
|
|
eps.append({"file": f, "title": titles.get(f, f)})
|
|
if not eps:
|
|
print(f"[{key}] no episodes on disk, skip"); continue
|
|
|
|
cover_name = ""
|
|
try:
|
|
root = ET.fromstring(fetch(rss))
|
|
cu = cover_url(root)
|
|
if cu:
|
|
img = Image.open(io.BytesIO(fetch(cu))).convert("RGB").resize((160, 160))
|
|
png = os.path.join(OUT, f"{key}.png")
|
|
img.save(png)
|
|
subprocess.run([sys.executable, LVGLIMG, "--ofmt", "BIN", "--cf", "RGB565",
|
|
"-o", OUT, png], check=True)
|
|
cover_name = f"{key}.bin"
|
|
print(f"[{key}] cover -> {cover_name}")
|
|
else:
|
|
print(f"[{key}] no itunes:image in feed")
|
|
except Exception as ex:
|
|
print(f"[{key}] cover failed: {ex}")
|
|
|
|
manifest.append({"show": show, "cover": cover_name, "episodes": eps})
|
|
|
|
json.dump(manifest, open(os.path.join(OUT, "manifest.json"), "w", encoding="utf-8"),
|
|
ensure_ascii=False, indent=2)
|
|
print(f"manifest.json: {len(manifest)} shows")
|