Martin Kirchgessner
2025-06-13 Liquidshop #5
Liquidsoap runs Radio Campus Grenoble since 2021.
Actually, it's
cf. Liquidshop #3
programs = fallback(track_sensitive=false,
live, # input.harbor
retransmit, # input.http
recorded_shows, # switch([playlist, ...])
autodj, # request.dynamic(picking in Beets)
)
radio = fallback(
delay(780. , playlist("/home/radio/jingles")),
programs
)
Even with examples
Although dropping music and recorded shows in folders is fine.
harbor.http.register[.simple]
🚀
harbor.http.static
Chosen for the project reboot as "RadioZ"
Simplified excerpts from radioz.liq
programs = fallback(track_sensitive=false,
live, # input.harbor
retransmit, # input.http
carts, # request.queue(id="carts")
autodj, # request.queue("autodj")
)
radio = fallback(
delay(780. , request.queue(id="jingles")),
programs
)
harbor.http.register(
port=api_port,
method="POST",
"/queue/:queue_name",
queue_POST
)
def queue_POST(req, res) =
queue_name = req.query["queue_name"]
let json.parse (posted:{
path: string,
artist: string?,
title: string?,
radioz_sound_id: int?,
}) = req.body()
annotated = "annotate:pushed=\"#{time()}\"" ^
",radioz_sound_id=\"#{null.get(posted.radioz_sound_id)}\"" ^
",title=\"#{null.get(posted.title)}\"" ^
",artist=\"#{null.get(posted.artist)}\":" ^
posted.path
r = request.create(annotated)
queues[queue_name].push(r)
res.status_code(200)
res.json({ok=request.id(r)})
end
radio.on_metadata(
fun(md) -> thread.run(fast=false, {
post_to_log(md)
})
)
def post_to_log(md)
data = json()
data.add("title", md["title"])
data.add("artist", md["artist"])
data.add("radioz_sound_id", md["radioz_sound_id"])
data.add("on_air", time.string('%Y-%m-%d %H:%M:%S'))
data.add("source", current_source_id())
_ = post_to_radioz("/metadata_log", data)
end
current_source_id = ref("starting")
def transition_source(transitioning_from, transitioning_to)
current_source_id := source.id(transitioning_to)
transitioning_to
end
radio = fallback([jingles, programs],
transitions=[transition_source, transition_source],
)
Would love to crossfade here, but #4179🙏
thread.run(every=1., delay=1., fast=false, post_to_live)
def post_to_live()
data = json()
data.add("source", current_source_id())
data.add("remaining", string.float(main_output.remaining()))
data.add("elapsed", string.float(main_output.elapsed()))
data.add("time", time.string('%Y-%m-%dT%H:%M:%S%z'))
fill_next(data, "next_jingle", jingles_queue)
fill_next(data, "next_autodj", autodj_queue)
fill_next(data, "next_cart", carts_queue)
_ = post_to_radioz("/live", data)
end
harbor.http.register(
port=api_port,
method="DELETE",
"/live",
live_DELETE
)
def live_DELETE(req, res) =
main_source.skip()
res.status_code(200)
end
request
is a liquidsoap module
def queue_POST(request, response) =
...
r = request.create(annotated) # !!!!
...
Use (req, res)
The scheduler can query the Liquidsoap process
The Liquidsoap process can query the scheduler
👌
path_to_config = environment.get("RADIOZ_CONFIG")
let yaml.parse (conf:{
scheduler:{
bind:string,
},
liq_config:{
api_port:float,
token:string,
start_sound:string,
},
}
}) = file.contents(path_to_config)
def post_to_radioz(endpoint, data)
res = http.post(
"http://#{conf.scheduler.bind}#{endpoint}",
headers=[
("Content-Type", "application/json; charset=UTF-8"),
("X-Auth-Token", conf.liq_config.token),
],
data=json.stringify(data)
)
if res.status_code != 200
then
log(label="Warning", "Error while posting #{endpoint} to webapp: #{res} #{res.status_code} #{res.status_message}")
end
res
end
harbor.http.middleware.register(check_token)
def check_token(req, res, next)
if req.headers["x-auth-token"] != conf.liq_config.token then
res.status_code(401) # Unauthorized
else
next(req, res)
end
end
For end-users
def auth_function(login) =
res = post_to_radioz("/can_live", login)
if res.status_code == 200 then
log("Access granted to #{login.user}")
true
else
log("Access denied to #{login.user}")
false
end
end
live = input.harbor(auth=auth_function, port=9000, "live")
Pushing tracks & fading to live seamlessly.
So we could have an auto-reload !
file.watch(current_script_path(), restart)
def http_log(req, res, next)
started_at = time()
next(req, res)
duration = time() - started_at
log(label="http", "#{req.method} #{req.path} #{res.status_code.current()} #{duration}")
end
harbor.http.middleware.register(http_log)
⚠️ errors here will block the response silently ⚠️
Follow progress @ sr.ht/~martink/radioz