diff --git a/tower/lisael_refresh.py b/tower/lisael_refresh.py new file mode 100644 index 0000000..796258f --- /dev/null +++ b/tower/lisael_refresh.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Weekly refresh: latest Radio France youth episodes -> staging dir that +lisael_server.py pushes to the box. Atomic swap so a failed run keeps the old set.""" +import io, json, os, shutil, ssl, subprocess, sys, tempfile, urllib.request +import xml.etree.ElementTree as ET +from PIL import Image + +STAGING = os.environ.get("LISAEL_STAGING", "/home/clems/lisael-content/podcasts") +LVGLIMG = os.environ.get("LISAEL_LVGLIMG", "/home/clems/lisael-content/LVGLImage.py") +UA = {"User-Agent": "Mozilla/5.0"} +NS = {"itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd"} +FEEDS = { + "oli": ("Une histoire et Oli", "https://radiofrance-podcast.net/podcast09/rss_19721.xml", 3), + "odyssees": ("Les Odyssees", "https://radiofrance-podcast.net/podcast09/rss_20108.xml", 3), + "salutinfo": ("Salut l'info", "https://radiofrance-podcast.net/podcast09/rss_20689.xml", 2), + "bestioles": ("Bestioles", + "https://radiofrance-podcast.net/podcast09/35099478-7c72-4f9e-a6de-1b928400e9e5/" + "podcast_a80ecbd5-df3d-4c9d-bee7-4e3d9efc1974.xml", 2), +} +SUBS = {"’": "'", "“": '"', "”": '"', "–": "-", + "—": "-", "…": "...", "«": '"', "»": '"'} + +def fetch(u, t=180): + return urllib.request.urlopen(urllib.request.Request(u, headers=UA), + timeout=t, context=ssl.create_default_context()).read() + +def ext_for(url, mime): + u = url.split("?")[0].lower() + return "m4a" if (u.endswith(".m4a") or "mp4" in (mime or "") or "x-m4a" in (mime or "")) else "mp3" + +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 + +def clean(s): + for a, b in SUBS.items(): + s = s.replace(a, b) + return " ".join(s.split()).strip() + +def main(): + tmp = tempfile.mkdtemp(prefix="lisael_stg_") + manifest = [] + for key, (show, rss, n) in FEEDS.items(): + try: + root = ET.fromstring(fetch(rss)) + except Exception as e: + print("RSS fail", key, e); continue + eps = [] + for i, it in enumerate(root.findall(".//item")[:n], 1): + enc = it.find("enclosure") + if enc is None or not enc.get("url"): + continue + url = enc.get("url"); fname = f"{key}_{i:02d}.{ext_for(url, enc.get('type',''))}" + try: + open(os.path.join(tmp, fname), "wb").write(fetch(url)) + except Exception as e: + print("dl fail", fname, e); continue + eps.append({"file": fname, "title": clean(it.findtext("title") or f"{show} {i}")}) + if not eps: + continue + cover = "" + try: + cu = cover_url(root) + if cu: + im = Image.open(io.BytesIO(fetch(cu))).convert("RGB").resize((160, 160)) + png = os.path.join(tmp, f"{key}.png"); im.save(png) + subprocess.run([sys.executable, LVGLIMG, "--ofmt", "BIN", "--cf", "RGB565", + "-o", tmp, png], check=True) + cover = f"{key}.bin"; os.remove(png) + except Exception as e: + print("cover fail", key, e) + manifest.append({"show": show, "cover": cover, "episodes": eps}) + + json.dump(manifest, open(os.path.join(tmp, "manifest.json"), "w", encoding="utf-8"), + ensure_ascii=False, indent=2) + os.makedirs(os.path.dirname(STAGING), exist_ok=True) + bak = STAGING + ".old" + if os.path.exists(STAGING): + shutil.rmtree(bak, ignore_errors=True); os.rename(STAGING, bak) + os.rename(tmp, STAGING) + shutil.rmtree(bak, ignore_errors=True) + print(f"staging OK: {len(manifest)} shows, {len(os.listdir(STAGING))} files in {STAGING}") + +if __name__ == "__main__": + main()