mirror of
https://github.com/mistergibson/radio.git
synced 2026-09-08 22:09:51 -07:00
Minor tweaks
This commit is contained in:
parent
0c5f864007
commit
952c9ae5a9
9 changed files with 1408 additions and 2024 deletions
188
station.liq
188
station.liq
|
|
@ -1,116 +1,108 @@
|
|||
# station.liq - Main liquidsoap configuration
|
||||
# All paths are resolved relative to this file's location.
|
||||
# Move the whole directory tree and nothing else needs changing.
|
||||
#!/usr/bin/env liquidsoap
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Resolve root directory from script location
|
||||
# -------------------------------------------------------
|
||||
let ROOT = configure.bindir()
|
||||
# ---------------------------------------------------------------
|
||||
# station.liq - Liquidsoap configuration for radio automation
|
||||
# Auto-detects its own location so all paths are relative.
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
let MUSIC_DIR = ROOT ^ "/music"
|
||||
let PODCASTS_DIR = ROOT ^ "/podcasts"
|
||||
let PLAYLISTS_DIR = ROOT ^ "/playlists"
|
||||
let SCHEDULE_FILE = ROOT ^ "/schedule.txt"
|
||||
let CONFIG_FILE = ROOT ^ "/config.json"
|
||||
configure.bindir()
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Load Icecast credentials from config.json
|
||||
# -------------------------------------------------------
|
||||
let cfg = json.from_file(CONFIG_FILE)
|
||||
let ic_host = json.(cfg.icecast.host)
|
||||
let ic_port = int_of_string(json.(cfg.icecast.port))
|
||||
let ic_mount = json.(cfg.icecast.mount)
|
||||
let ic_user = json.(cfg.icecast.username)
|
||||
let ic_pass = json.(cfg.icecast.password)
|
||||
set("log.file.path", "#{bindir()}/logs/liquidsoap.log")
|
||||
set("log.stdout", true)
|
||||
set("log.level", 3)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Background music source
|
||||
# -------------------------------------------------------
|
||||
let music =
|
||||
playlist.recursive(
|
||||
mode="random",
|
||||
duration=3600,
|
||||
path=MUSIC_DIR,
|
||||
extensions=["mp3", "m4a", "ogg", "flac"]
|
||||
# --- Load Icecast credentials from config.json ---
|
||||
let json.parse (cfg : {
|
||||
icecast: {
|
||||
host: string,
|
||||
port: int,
|
||||
mount: string,
|
||||
username: string,
|
||||
password: string
|
||||
},
|
||||
gpodder: {
|
||||
enable: bool,
|
||||
host: string,
|
||||
username: string,
|
||||
password: string
|
||||
}
|
||||
}) = file.contents("#{bindir()}/config.json")
|
||||
|
||||
let ic_host = cfg.icecast.host
|
||||
let ic_port = cfg.icecast.port
|
||||
let ic_mount = cfg.icecast.mount
|
||||
let ic_username = cfg.icecast.username
|
||||
let ic_password = cfg.icecast.password
|
||||
|
||||
# --- Background music library (recursive scan for mp3/m4a) ---
|
||||
music_dir = "#{bindir()}/music"
|
||||
music_playlist =
|
||||
request.cue(
|
||||
playlist(
|
||||
recurse=true,
|
||||
pattern="\\.(mp3|m4a)$",
|
||||
"#{music_dir}"
|
||||
)
|
||||
)
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Scheduled content via request queue
|
||||
# -------------------------------------------------------
|
||||
def q = ref []
|
||||
# --- Scheduled content: request queue fed by schedule.txt ---
|
||||
sched_queue = request.queue(id="scheduler")
|
||||
|
||||
def push_request(req) =
|
||||
q := req :: (!q)
|
||||
# --- Jingles / announcements (optional) ---
|
||||
jingle_dir = "#{bindir()}/jingles"
|
||||
announce_dir = "#{bindir()}/announcements"
|
||||
|
||||
# Build a show source from its .pls file (written by update_playlists.py)
|
||||
def show_source(slug) =
|
||||
let pls_path = PLAYLISTS_DIR ^ "/" ^ slug ^ ".pls"
|
||||
if file.test(pls_path) then
|
||||
log("# Playing scheduled show: #{slug}")
|
||||
Some(request.create(pls_path))
|
||||
def has_audio(dir) =
|
||||
try
|
||||
let l = list.filter(fun(f) -> regexp("\\.(mp3|m4a)$").test(f), ls.dir(dir))
|
||||
list.length(l) > 0
|
||||
catch _ do
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
jingles_src =
|
||||
if has_audio(jingle_dir) then
|
||||
mksafe(request.cue(playlist(recurse=true, pattern="\\.(mp3|m4a)$", jingle_dir)))
|
||||
else
|
||||
log("! No playlist found for show: #{slug}, skipping")
|
||||
None
|
||||
null()
|
||||
end
|
||||
|
||||
# Parse schedule.txt and register cron-triggered tasks
|
||||
def load_schedule() =
|
||||
try
|
||||
if not(file.test(SCHEDULE_FILE)) then
|
||||
log("WARNING: schedule.txt not found at #{SCHEDULE_FILE}")
|
||||
else
|
||||
let lines = file.lines(SCHEDULE_FILE)
|
||||
List.iter(fun line ->
|
||||
let t = String.strip(line)
|
||||
if t != "" && not(String.starts_with(t, "#")) then
|
||||
let p = String.split(t, sep=" ")
|
||||
|> List.filter(fun x -> String.strip(x) != "")
|
||||
if List.length(p) >= 7 then
|
||||
let cron_expr = String.concat(sep=" ", List.take(p, 5))
|
||||
let stype = List.nth(p, 5)
|
||||
let target = List.nth(p, 6)
|
||||
match stype with
|
||||
| "show" ->
|
||||
cron.add(id=target, cron_expr, fun () ->
|
||||
match show_source(target) with
|
||||
| Some(r) -> push_request r
|
||||
| None -> ()
|
||||
)
|
||||
| "stream" ->
|
||||
let runlen_str =
|
||||
if List.length(p) > 7 then List.nth(p, 7) else "3600"
|
||||
let runlen = int_of_string(runlen_str)
|
||||
cron.add(id=target, cron_expr, fun () ->
|
||||
log("Triggering stream: #{target} (#{runlen}s)")
|
||||
# For external streams, create a request with duration cap
|
||||
let s = single.file(fallback=false, target)
|
||||
push_request(request.create(target))
|
||||
)
|
||||
| _ ->
|
||||
log("Unknown schedule type '#{stype}' for target '#{target}', ignoring")
|
||||
end
|
||||
else
|
||||
log("Malformed schedule line (need at least 7 fields): #{t}")
|
||||
end
|
||||
end
|
||||
) lines
|
||||
log("Schedule loaded from #{SCHEDULE_FILE}")
|
||||
with e ->
|
||||
log("ERROR loading schedule: #{e}")
|
||||
announcements_src =
|
||||
if has_audio(announce_dir) then
|
||||
mksafe(request.cue(playlist(recurse=true, pattern="\\.(mp3|m4a)$", announce_dir)))
|
||||
else
|
||||
null()
|
||||
end
|
||||
|
||||
# Load schedule once at startup
|
||||
thread.run(at=now(), fun () -> load_schedule())
|
||||
# --- Assemble the stream ---
|
||||
primary = fallback(track_sensitive=false, [sched_queue, music_playlist])
|
||||
|
||||
# -------------------------------------------------------
|
||||
# Output: scheduled content over background music fallback
|
||||
# -------------------------------------------------------
|
||||
content =
|
||||
match jingles_src with
|
||||
| null() => primary
|
||||
| src => random(weights=[1, 8], [src, primary])
|
||||
end
|
||||
|
||||
final_content =
|
||||
match announcements_src with
|
||||
| null() => content
|
||||
| src => fallback(track_sensitive=false, [src, content])
|
||||
end
|
||||
|
||||
radio = normalize(final_content)
|
||||
|
||||
# --- Output to Icecast ---
|
||||
output.icecast(
|
||||
%mp3(bitrate=128),
|
||||
%mp3(bitrate=128, samplerate=44100, stereo=true),
|
||||
host=ic_host,
|
||||
port=ic_port,
|
||||
user=ic_user,
|
||||
password=ic_pass,
|
||||
user=ic_username,
|
||||
password=ic_password,
|
||||
mount=ic_mount,
|
||||
fallback=music,
|
||||
request.queue(q)
|
||||
name="Radio Station",
|
||||
description="Automated internet radio",
|
||||
genre="Various",
|
||||
public=true,
|
||||
radio
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue