Wire up a web manifest, service worker and icon routes so the player can be installed on mobile. The static manifest stays generic; the page regenerates it at runtime from STATION_NAME so an instance keeps its name. The service worker only caches the app shell, never the live stream or the playback APIs. Icons (192/512, maskable and apple-touch) are rasterized from the favicon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
514 lines
19 KiB
Text
514 lines
19 KiB
Text
#!/usr/bin/liquidsoap
|
|
|
|
# radieo — couche diffusion.
|
|
# La source principale est pilotée par le daemon d'ingestion via GET /next.
|
|
# Le dossier /cache sert de secours quand le daemon n'a rien à proposer
|
|
# (daemon indisponible, file momentanément vide…). Si tout est vide : silence.
|
|
# Le harbor sert aussi une petite page web (/) et le titre courant (/nowplaying).
|
|
|
|
# --- Journalisation : tout sur la sortie standard (pratique en conteneur) ---
|
|
settings.log.stdout := true
|
|
settings.log.file := false
|
|
settings.log.level := 3
|
|
|
|
# --- Harbor : écoute sur toutes les interfaces du conteneur (IPv4 + IPv6) ---
|
|
# `::` seul suffit : sous Linux (bindv6only=0 par défaut) la socket IPv6 accepte
|
|
# aussi les clients IPv4 (adresses mappées). Ajouter "0.0.0.0" en plus ferait
|
|
# double bind sur le même port IPv4 → EADDRINUSE.
|
|
settings.harbor.bind_addrs := ["::"]
|
|
|
|
# URL du daemon d'ingestion (nom de service résolu par docker-compose).
|
|
ingest_url = "http://ingest:8080/next"
|
|
|
|
# Callback appelé par request.dynamic pour obtenir le prochain morceau.
|
|
# Renvoie une requête à jouer, ou null si rien n'est disponible (→ secours).
|
|
def next_track() =
|
|
resp = http.get(ingest_url, timeout=5.0)
|
|
body = string.trim(resp)
|
|
if resp.status_code == 200 and body != "" then
|
|
request.create(body)
|
|
else
|
|
null
|
|
end
|
|
end
|
|
|
|
# Source principale : pilotée par le daemon. prefetch=1 pour anticiper le
|
|
# prochain morceau ; retry_delay pour ne pas marteler le daemon en cas de vide.
|
|
main = request.dynamic(next_track, prefetch=1, retry_delay=1.0)
|
|
|
|
# Filtre du secours : ne garder que les vrais fichiers audio et ignorer les
|
|
# fichiers cachés (.gitkeep, téléchargements .part en cours). Ceinture et
|
|
# bretelles : la m3u ne liste déjà que des fichiers audio réels.
|
|
audio_ext = [".mp3", ".flac", ".ogg", ".opus", ".m4a", ".aac", ".wav"]
|
|
def audio_only(r) =
|
|
u = string.case(lower=true, request.uri(r))
|
|
base = path.basename(u)
|
|
is_audio = list.exists(fun(e) -> string.contains(suffix=e, u), audio_ext)
|
|
is_audio and not string.contains(prefix=".", base)
|
|
end
|
|
|
|
# Secours : uniquement les morceaux DÉJÀ passés à l'antenne. Le daemon les
|
|
# expose en m3u (/fallback.m3u) ; on la récupère périodiquement dans un fichier
|
|
# LOCAL que playlist surveille. Le buffer de préchargement (morceaux pas encore
|
|
# diffusés) en est exclu : au démarrage à froid la liste est vide → silence
|
|
# assumé plutôt qu'une boucle sur 2-3 titres.
|
|
# Passer par un fichier local (plutôt qu'une URL directe dans playlist) évite la
|
|
# détection de type hasardeuse du résolveur http, et garde la dernière liste
|
|
# valide si l'ingest devient injoignable.
|
|
fallback_url = "http://ingest:8080/fallback.m3u"
|
|
fallback_file = "/tmp/fallback.m3u"
|
|
file.write(data="#EXTM3U\n", atomic=true, fallback_file) # amorce vide au boot
|
|
|
|
def refresh_fallback() =
|
|
resp = http.get(fallback_url, timeout=5.0)
|
|
if resp.status_code == 200 then
|
|
file.write(data=resp, atomic=true, fallback_file)
|
|
end
|
|
end
|
|
thread.run(fast=false, every={30.}, refresh_fallback)
|
|
|
|
backup = playlist(
|
|
mode="randomize", reload_mode="watch", mime_type="audio/x-mpegurl",
|
|
check_next=audio_only, fallback_file
|
|
)
|
|
# Les morceaux du secours sont rejoués depuis le cache local (déjà diffusés) et
|
|
# n'ont pas d'annotation d'origine. On les étiquette explicitement pour que le
|
|
# player affiche « via le cache local » plutôt que rien quand on retombe sur ce
|
|
# filet (ingest injoignable, file vide pendant le préchargement…).
|
|
backup = metadata.map(fun(_) -> [("origin", "cache")], backup)
|
|
|
|
# File de rejeu ponctuel : normalement vide (donc non prête, transparente). Le
|
|
# endpoint /restart-track y pousse le morceau courant pour le rejouer depuis le
|
|
# début à l'antenne. Placée en tête du fallback, elle préempte la source
|
|
# principale dès qu'un morceau y est poussé.
|
|
requeue = request.queue(id="requeue")
|
|
|
|
# fallback préfère la file de rejeu, puis la source principale, et bascule sur le
|
|
# cache si rien n'est prêt. track_sensitive=true : on ne coupe pas un morceau en
|
|
# cours (le rejeu ne prend donc effet qu'à la prochaine frontière, provoquée
|
|
# explicitement par source.skip dans /restart-track).
|
|
music = fallback(track_sensitive=true, [requeue, main, backup])
|
|
|
|
# --- Jingles : intercalés toutes les 2 chansons -----------------------------
|
|
# Le dossier /jingles (monté depuis ./jingles) est parcouru et lu au hasard.
|
|
# reload_mode="watch" : un jingle ajouté/retiré est pris en compte à chaud.
|
|
# Dossier vide → source jamais prête → le switch retombe simplement sur la
|
|
# musique, sans jingle et sans plantage.
|
|
# ATTENTION : playlist explore récursivement les sous-dossiers. Pour la rotation
|
|
# par défaut on ne veut QUE les fichiers directement dans /jingles ; les
|
|
# sous-dossiers spéciaux (midi, gouter, moment) sont gérés à part. On
|
|
# filtre donc sur le dossier parent en plus du test audio habituel.
|
|
def jingle_top_level(r) =
|
|
audio_only(r) and path.dirname(request.uri(r)) == "/jingles"
|
|
end
|
|
jingles = playlist(
|
|
mode="randomize", reload_mode="watch", check_next=jingle_top_level, "/jingles"
|
|
)
|
|
|
|
# Jingles spéciaux, joués une seule fois juste après une heure donnée (au
|
|
# prochain jingle qui suit ce moment), plutôt que le jingle par défaut.
|
|
jingles_midi = playlist(
|
|
mode="randomize", reload_mode="watch", check_next=audio_only, "/jingles/midi"
|
|
)
|
|
jingles_gouter = playlist(
|
|
mode="randomize", reload_mode="watch", check_next=audio_only, "/jingles/gouter"
|
|
)
|
|
jingles_moment = playlist(
|
|
mode="randomize", reload_mode="watch", check_next=audio_only, "/jingles/moment"
|
|
)
|
|
|
|
# Fenêtre (en minutes) après l'heure cible pendant laquelle le jingle spécial
|
|
# reste éligible : assez courte pour rester "juste après", assez large pour
|
|
# laisser passer au moins une occasion de jingle (toutes les ~2 chansons).
|
|
special_jingle_window = 10
|
|
|
|
# Identifiant du jour courant (année * 366 + jour de l'année), pour ne
|
|
# déclencher chaque créneau spécial qu'une seule fois par jour.
|
|
def day_key() =
|
|
t = time.local()
|
|
t.year * 1000 + t.year_day
|
|
end
|
|
|
|
# Construit un couple (prédicat, source) pour le switch : le prédicat devient
|
|
# vrai une seule fois par jour, à partir de hour:minute et pendant
|
|
# special_jingle_window minutes.
|
|
def make_special_slot(hour, minute, jingle_source) =
|
|
last_day = ref(-1)
|
|
slot_minutes = hour * 60 + minute
|
|
def due() =
|
|
t = time.local()
|
|
now_minutes = t.hour * 60 + t.min
|
|
in_window = now_minutes >= slot_minutes and now_minutes < slot_minutes + special_jingle_window
|
|
if in_window and last_day() != day_key() then
|
|
last_day := day_key()
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
(due, jingle_source)
|
|
end
|
|
|
|
special_jingle_slots = [
|
|
make_special_slot(11, 0, jingles_midi),
|
|
make_special_slot(15, 0, jingles_moment),
|
|
make_special_slot(16, 30, jingles_gouter),
|
|
make_special_slot(21, 0, jingles_moment),
|
|
]
|
|
|
|
# On compte les morceaux de musique réellement diffusés : on_track ne se
|
|
# déclenche que sur la source effectivement tirée par le switch (les sources
|
|
# non sélectionnées ne sont pas consommées), donc les jingles ne comptent pas.
|
|
song_count = ref(0)
|
|
music.on_track(synchronous=false, fun(_) -> song_count := song_count() + 1)
|
|
|
|
# Prédicat du switch : vrai quand 2 chansons ont été diffusées depuis le dernier
|
|
# jingle. Il remet le compteur à zéro au passage pour repartir sur un cycle neuf.
|
|
def time_for_jingle() =
|
|
if song_count() >= 2 then
|
|
song_count := 0
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
# switch track_sensitive : la décision est prise aux frontières de morceaux, on
|
|
# ne coupe donc jamais un titre. Si les jingles ne sont pas prêts (dossier vide),
|
|
# le switch enchaîne directement sur la musique. Les créneaux spéciaux sont
|
|
# testés en premier : un jingle "juste après" une heure donnée prend le pas sur
|
|
# le cycle normal des 2 chansons, une seule fois par jour.
|
|
radio = switch(
|
|
track_sensitive=true,
|
|
list.append(special_jingle_slots, [(time_for_jingle, jingles), ({true}, music)])
|
|
)
|
|
|
|
# Transition douce entre les morceaux : fondu enchaîné de 3 s. La fin du
|
|
# morceau courant se fond dans le début du suivant.
|
|
radio = crossfade(duration=3.0, fade_in=3.0, fade_out=3.0, radio)
|
|
|
|
# mksafe garantit un flux continu : silence plutôt que plantage si tout est vide.
|
|
radio = mksafe(radio)
|
|
|
|
# --- Métadonnées « en cours de lecture » -----------------------------------
|
|
# On mémorise les dernières métadonnées vues sur le flux réellement diffusé,
|
|
# ainsi qu'un historique borné des titres passés à l'antenne (le plus récent
|
|
# en tête). L'historique reflète ce qui a vraiment été diffusé.
|
|
now_playing = ref([])
|
|
history = ref([])
|
|
history_max = 25
|
|
radio.on_metadata(
|
|
synchronous=false,
|
|
fun(m) -> begin
|
|
now_playing := m
|
|
# `file` : nom de base du fichier à l'antenne, servant de jeton de
|
|
# téléchargement (/download?file=…). Vide si la métadonnée manque.
|
|
entry = {title=m["title"], artist=m["artist"], url=m["url"], origin=m["origin"], file=path.basename(m["filename"])}
|
|
head = list.hd(default={title="", artist="", url="", origin="", file=""}, history())
|
|
is_dup = head.title == entry.title and head.artist == entry.artist
|
|
if not is_dup and (entry.title != "" or entry.artist != "") then
|
|
history := list.prefix(history_max, list.add(entry, history()))
|
|
end
|
|
end
|
|
)
|
|
|
|
# --- Sortie : flux MP3 sur http://<hote>:8000/radio.mp3 ---
|
|
output.harbor(
|
|
%mp3(bitrate=192),
|
|
port=8000,
|
|
mount="radio.mp3",
|
|
radio
|
|
)
|
|
|
|
# --- 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())
|
|
)
|
|
|
|
# File d'attente des prochains morceaux, relayée depuis le daemon d'ingestion
|
|
# (le player n'a pas d'accès direct au réseau interne). Comme /ingest/status, on
|
|
# renvoie une valeur neutre — ici une liste vide — si le daemon est injoignable,
|
|
# pour ne pas casser le player.
|
|
ingest_queue_url = "http://ingest:8080/queue"
|
|
harbor.http.register(
|
|
port=8000, method="GET", "/queue",
|
|
fun(_, resp) -> begin
|
|
resp.content_type("application/json; charset=utf-8")
|
|
body = http.get(ingest_queue_url, timeout=5.0)
|
|
if body.status_code == 200 then
|
|
resp.data(string.trim(body) ^ "\n")
|
|
else
|
|
resp.data("[]")
|
|
end
|
|
end
|
|
)
|
|
|
|
# État du préchargement, relayé depuis le daemon d'ingestion (reverse proxy) :
|
|
# le player n'a pas accès direct au réseau interne, on lui expose donc l'info
|
|
# {ready, prefetch} via le même harbor que le flux. Si le daemon est injoignable
|
|
# on renvoie un objet neutre plutôt qu'une erreur, pour ne pas casser le player.
|
|
ingest_status_url = "http://ingest:8080/status"
|
|
harbor.http.register(
|
|
port=8000, method="GET", "/ingest/status",
|
|
fun(_, resp) -> begin
|
|
resp.content_type("application/json; charset=utf-8")
|
|
body = http.get(ingest_status_url, timeout=5.0)
|
|
if body.status_code == 200 then
|
|
resp.data(string.trim(body) ^ "\n")
|
|
else
|
|
resp.data("{}")
|
|
end
|
|
end
|
|
)
|
|
|
|
# Mettre une URL yt-dlp en file d'attente (piste seule, ou playlist/album
|
|
# entier). Le player n'a pas accès au réseau interne : on relaie la demande vers
|
|
# l'ingest, qui résout l'URL et la place en file prioritaire (le prochain /next
|
|
# la servira). On renvoie tel quel son code et son corps JSON ({queued: N} ou
|
|
# une erreur). Timeout large : résoudre une grosse playlist peut prendre du
|
|
# temps. NB : la variable locale s'appelle `link`, pas `url`, pour ne pas
|
|
# masquer le module `url` (url.encode).
|
|
ingest_enqueue_url = "http://ingest:8080/enqueue"
|
|
harbor.http.register(
|
|
port=8000, method="POST", "/enqueue",
|
|
fun(req, resp) -> begin
|
|
link = list.assoc(default="", "url", req.query)
|
|
if link == "" then
|
|
resp.status_code(400)
|
|
resp.data("missing url")
|
|
else
|
|
body = http.post(
|
|
data="", timeout=60.0, "#{ingest_enqueue_url}?url=#{url.encode(link)}"
|
|
)
|
|
resp.status_code(body.status_code)
|
|
resp.content_type("application/json; charset=utf-8")
|
|
resp.data(string.trim(body) ^ "\n")
|
|
end
|
|
end
|
|
)
|
|
|
|
# Retirer un morceau de la file d'attente. Symétrique de /enqueue : le player
|
|
# n'a pas accès au réseau interne, on relaie donc la demande (l'`id` opaque
|
|
# fourni par /queue) vers l'ingest, qui retire l'entrée correspondante. On
|
|
# renvoie tel quel son code et son corps JSON ({removed: true} ou une erreur).
|
|
ingest_dequeue_url = "http://ingest:8080/dequeue"
|
|
harbor.http.register(
|
|
port=8000, method="POST", "/dequeue",
|
|
fun(req, resp) -> begin
|
|
id = list.assoc(default="", "id", req.query)
|
|
if id == "" then
|
|
resp.status_code(400)
|
|
resp.data("missing id")
|
|
else
|
|
body = http.post(
|
|
data="", timeout=10.0, "#{ingest_dequeue_url}?id=#{url.encode(id)}"
|
|
)
|
|
resp.status_code(body.status_code)
|
|
resp.content_type("application/json; charset=utf-8")
|
|
resp.data(string.trim(body) ^ "\n")
|
|
end
|
|
end
|
|
)
|
|
|
|
# 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=<nom>` :
|
|
# 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
|
|
)
|
|
|
|
# Partage Subsonic à la demande. Un morceau de la bibliothèque Subsonic n'a pas
|
|
# d'URL publique : son lien « source » pointe ici avec l'id du morceau. On
|
|
# demande alors à l'ingest (qui détient les identifiants Subsonic) de créer un
|
|
# partage public via createShare, puis on redirige l'auditeur vers l'URL
|
|
# renvoyée. Le partage n'est donc créé que si quelqu'un clique réellement sur le
|
|
# lien — jamais à chaque morceau joué. 404 si l'id manque, 502 si l'ingest ne
|
|
# peut pas partager (partage désactivé côté serveur, injoignable…).
|
|
ingest_share_url = "http://ingest:8080/share"
|
|
harbor.http.register(
|
|
port=8000, method="GET", "/share",
|
|
fun(req, resp) -> begin
|
|
song = list.assoc(default="", "song", req.query)
|
|
if song == "" then
|
|
resp.status_code(404)
|
|
resp.data("missing song id")
|
|
else
|
|
body = http.post(data="", timeout=10.0, "#{ingest_share_url}?id=#{url.encode(song)}")
|
|
share = json.parse(default={url=""}, string.trim(body))
|
|
if body.status_code == 200 and share.url != "" then
|
|
resp.status_code(302)
|
|
resp.header("Location", share.url)
|
|
resp.data("")
|
|
else
|
|
resp.status_code(502)
|
|
resp.data("share unavailable")
|
|
end
|
|
end
|
|
end
|
|
)
|