# radieo — couche web : ressources statiques et API de lecture locale. # # Inclus par radio.liq APRÈS la définition du pipeline : ce fichier ne fait # qu'enregistrer des routes sur le harbor du flux (port 8000) et s'appuie sur les # symboles du pipeline (`radio`, `now_playing`, `history`, `requeue`, # `song_count`, `audio_ext`). Les routes qui relaient le daemon d'ingestion sont # à part, dans ingest_proxy.liq. # --- Page web et API de lecture (mêmes port/harbor que le flux) --- home_html = file.contents("/etc/liquidsoap/index.html") favicon_svg = file.contents("/etc/liquidsoap/favicon.svg") # Ressources de la webapp installable (PWA) : manifeste, service worker et jeu # d'icônes. Chargées une fois au démarrage puis servies telles quelles. manifest_json = file.contents("/etc/liquidsoap/manifest.webmanifest") sw_js = file.contents("/etc/liquidsoap/sw.js") icon_192 = file.contents("/etc/liquidsoap/icon-192.png") icon_512 = file.contents("/etc/liquidsoap/icon-512.png") icon_maskable_512 = file.contents("/etc/liquidsoap/icon-maskable-512.png") apple_touch_icon = file.contents("/etc/liquidsoap/apple-touch-icon.png") harbor.http.register( port=8000, method="GET", "/", fun(_, resp) -> begin resp.content_type("text/html; charset=utf-8") resp.html(home_html) end ) harbor.http.register( port=8000, method="GET", "/favicon.svg", fun(_, resp) -> begin resp.content_type("image/svg+xml; charset=utf-8") resp.data(favicon_svg) end ) harbor.http.register( port=8000, method="GET", "/manifest.webmanifest", fun(_, resp) -> begin resp.content_type("application/manifest+json; charset=utf-8") resp.data(manifest_json) end ) # Le service worker doit être servi depuis la racine pour couvrir tout le site # (portée par défaut = son emplacement). Pas de cache HTTP agressif : le # navigateur revérifie régulièrement le script. harbor.http.register( port=8000, method="GET", "/sw.js", fun(_, resp) -> begin resp.content_type("text/javascript; charset=utf-8") resp.header("Cache-Control", "no-cache") resp.data(sw_js) end ) harbor.http.register( port=8000, method="GET", "/icon-192.png", fun(_, resp) -> begin resp.content_type("image/png") resp.data(icon_192) end ) harbor.http.register( port=8000, method="GET", "/icon-512.png", fun(_, resp) -> begin resp.content_type("image/png") resp.data(icon_512) end ) harbor.http.register( port=8000, method="GET", "/icon-maskable-512.png", fun(_, resp) -> begin resp.content_type("image/png") resp.data(icon_maskable_512) end ) harbor.http.register( port=8000, method="GET", "/apple-touch-icon.png", fun(_, resp) -> begin resp.content_type("image/png") resp.data(apple_touch_icon) end ) harbor.http.register( port=8000, method="GET", "/nowplaying", fun(_, resp) -> begin m = now_playing() resp.json({title=m["title"], artist=m["artist"], url=m["url"], origin=m["origin"]}) end ) # Historique des titres passés (le plus récent en tête, morceau courant inclus). harbor.http.register( port=8000, method="GET", "/history", fun(_, resp) -> resp.json(history()) ) # Passer au morceau suivant : on saute le morceau en cours sur la source # diffusée. request.dynamic a déjà préchargé le suivant, donc l'enchaînement # est immédiat (le prochain /next est demandé au daemon dans la foulée). harbor.http.register( port=8000, method="POST", "/skip", fun(_, resp) -> begin source.skip(radio) resp.json({skipped=true}) end ) # Rejouer le morceau courant depuis le début, pour TOUTE l'antenne (le flux est # partagé : il n'existe pas de position par auditeur). On repousse le fichier # courant — pris dans le cache, donc forcément local et présent — en tête via la # file de rejeu, puis on saute le morceau en cours pour l'y enchaîner aussitôt. # On remet le compteur de chansons à zéro pour qu'un jingle ne vole pas la place # du rejeu à cette frontière. Un morceau introuvable (jingle en cours, cache # évincé) renvoie 409. harbor.http.register( port=8000, method="POST", "/restart-track", fun(_, resp) -> begin base = path.basename(list.assoc(default="", "filename", now_playing())) full = "/cache/#{base}" if base != "" and file.exists(full) then song_count := 0 requeue.push.uri(full) source.skip(radio) resp.json({restarted=true}) else resp.status_code(409) resp.json({restarted=false}) end end ) # Télécharger un morceau. Sans paramètre : le titre en cours. Avec `?file=` : # n'importe quel fichier encore présent dans le cache (les titres passés listés # par /history exposent ce jeton). Le cache étant borné par le LRU, un morceau # évincé renvoie simplement 404. def content_type_of(name) = low = string.case(lower=true, name) if string.contains(suffix=".flac", low) then "audio/flac" elsif string.contains(suffix=".ogg", low) then "audio/ogg" elsif string.contains(suffix=".opus", low) then "audio/opus" elsif string.contains(suffix=".m4a", low) or string.contains(suffix=".aac", low) then "audio/mp4" elsif string.contains(suffix=".wav", low) then "audio/wav" else "audio/mpeg" end end # Sert un fichier en pièce jointe après validation. `path.basename` neutralise # toute tentative de remontée de répertoire (…/…) ; on n'autorise que de vrais # fichiers audio du cache, jamais les fichiers cachés (.part en cours, .gitkeep). def serve_attachment(resp, name) = base = path.basename(name) low = string.case(lower=true, base) is_audio = list.exists(fun(e) -> string.contains(suffix=e, low), audio_ext) full = "/cache/#{base}" if base == "" or string.contains(prefix=".", base) or not is_audio or not file.exists(full) then resp.status_code(404) resp.data("track not available") else resp.content_type(content_type_of(base)) resp.header("Content-Disposition", "attachment; filename=\"#{base}\"") resp.data(file.contents(full)) end end harbor.http.register( port=8000, method="GET", "/download", fun(req, resp) -> begin requested = list.assoc(default="", "file", req.query) name = if requested != "" then requested else list.assoc(default="", "filename", now_playing()) end serve_attachment(resp, name) end )