Compare commits
10 commits
a38e81854f
...
4daa5e756f
Author | SHA1 | Date | |
---|---|---|---|
|
4daa5e756f | ||
|
0e61fa549d | ||
|
7c71092ac3 | ||
|
cc871ca5ac | ||
|
2f6fe34c6b | ||
|
092894c7f9 | ||
|
95c24cd23d | ||
|
62d2cd93c4 | ||
|
70fa77dc9d | ||
|
ce8b9b18d5 |
5 changed files with 140 additions and 55 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
# Liquidsoap file for storing API codes and such.
|
||||
secrets.liq
|
||||
# Keep log files out.
|
||||
*.log
|
||||
|
|
23
restful.liq
Normal file
23
restful.liq
Normal file
|
@ -0,0 +1,23 @@
|
|||
def get_http_data(headers, url) =
|
||||
if string.contains(prefix='https', url) then
|
||||
let (status, _, data) = https.get(headers=headers, url)
|
||||
let (_, status_code, _) = status
|
||||
(status_code, data)
|
||||
else
|
||||
let (status, _, data) = http.get(headers=headers, url)
|
||||
let (_, status_code, _) = status
|
||||
(status_code, data)
|
||||
end
|
||||
end
|
||||
|
||||
def post_http_data(headers, postdata, url) =
|
||||
if string.contains(prefix='https', url) then
|
||||
let (status, _, data) = https.post(headers=headers, data=postdata, url)
|
||||
let (_, status_code, _) = status
|
||||
(status_code, data)
|
||||
else
|
||||
let (status, _, data) = http.post(headers=headers, data=postdata, url)
|
||||
let (_, status_code, _) = status
|
||||
(status_code, data)
|
||||
end
|
||||
end
|
50
secrets.liq
Normal file
50
secrets.liq
Normal file
|
@ -0,0 +1,50 @@
|
|||
###########################
|
||||
## THINGS TO CHANGE ##
|
||||
###########################
|
||||
|
||||
# The radio station details.
|
||||
radio_name = '<NAME_OF_RADIO_STATION>'
|
||||
radio_description = '<STANDARD_ISSUE_TAGLINE>'
|
||||
radio_genre = '<UNFAIRLY_CONSTRAINED_GENRE_TYPE>'
|
||||
|
||||
# Daemon settings
|
||||
log_file = '<ABSOLUTE_PATH_TO_LOG_FILE>'
|
||||
pid_file = '<ABSOLUTE_PATH_TO_PID_FILE>'
|
||||
|
||||
# This is the file that will play when something catastophic occurs.
|
||||
# Make sure it's an absolute path!
|
||||
fallback_audio = '<ABSOLUTE_PATH_TO_BACKUP_AUDIO_FILE>'
|
||||
|
||||
# The URL to the radio's webpage.
|
||||
radio_url_main = 'https://<WEBPAGE_URL>'
|
||||
|
||||
# The stream server (IP or FQDN)
|
||||
stream_address = '<STREAMING_SERVER_IP>'
|
||||
stream_port = 8000
|
||||
stream_password = '<STREAMING_SERVER_PASSWORD>'
|
||||
|
||||
# The authorization token for the DJ account.
|
||||
dj_token = '<DJ_ACCOUNT_AUTHORIZATION_TOKEN>'
|
||||
|
||||
###########################
|
||||
## THINGS TO LEAVE ALONE ##
|
||||
###########################
|
||||
|
||||
# This is the annotate string that incorporates the fallback audio if
|
||||
# we somehow have a problem communicating with the web server.
|
||||
fallback_annotate = 'annotate:artist="#{radio_name}",title="Stream is down :(",game="We\'ll be up again soon!":#{fallback_audio}'
|
||||
|
||||
# These are specific URL endpoints for the radio controls API.
|
||||
url_next = '#{radio_url_main}/api/next/'
|
||||
url_played = '#{radio_url_main}/api/played/'
|
||||
|
||||
# Headers for the API calls
|
||||
api_headers_next = [
|
||||
("Content-Type", "application/json; charset=utf-8"),
|
||||
("Authorization", "Token #{dj_token}"),
|
||||
("Accept", "application/vnd.liquidsoap.annotate")
|
||||
]
|
||||
api_headers_played = [
|
||||
("Content-Type", "application/json; charset=utf-8"),
|
||||
("Authorization", "Token #{dj_token}")
|
||||
]
|
75
spradio.liq
75
spradio.liq
|
@ -1,7 +1,16 @@
|
|||
%include "secrets.liq"
|
||||
%include "tweaks.liq"
|
||||
%include "restful.liq"
|
||||
|
||||
set("init.daemon", true)
|
||||
set("init.daemon.change_user", true)
|
||||
set("init.daemon.change_user.group", "liquidsoap")
|
||||
set("init.daemon.change_user.user", "liquidsoap")
|
||||
set("init.daemon.pidfile", true)
|
||||
set("init.daemon.pidfile.path", pid_file)
|
||||
|
||||
set("log.file", true)
|
||||
set("log.file.path", "/home/liquidsoap/spradio.log")
|
||||
set("log.file.path", log_file)
|
||||
set("log.stdout", true)
|
||||
set("log.level", 4)
|
||||
|
||||
|
@ -13,57 +22,11 @@ set("scheduler.non_blocking_queues", 3)
|
|||
|
||||
set("audio.converter.samplerate.libsamplerate.quality", "best")
|
||||
|
||||
security = single(id="default", fallback_audio)
|
||||
|
||||
# Tweaked custom crossfade to deal with jingles..
|
||||
def smart_cross(~start_next=5.,~fade_in=3.,~fade_out=3.,
|
||||
~default=(fun (a,b) -> sequence([a, b])),
|
||||
~high=-15., ~medium=-32., ~margin=4.,
|
||||
~width=2.,~conservative=false,s)
|
||||
fade.out = fade.out(type="sin", duration=fade_out)
|
||||
fade.in = fade.in(type="sin", duration=fade_in)
|
||||
add = fun (a,b) -> add(normalize=false, [b, a])
|
||||
log = log(label="smart_cross")
|
||||
|
||||
def transition(a,b,ma,mb,sa,sb)
|
||||
list.iter(fun(x)-> log(level=4, "Before: #{x}"), ma)
|
||||
list.iter(fun(x)-> log(level=4, "After : #{x}"), mb)
|
||||
|
||||
if ma["type"] == "J" or mb["type"] == "J" then
|
||||
log("Old or new file is a jingle: sequenced transition.")
|
||||
sequence([sa, sb])
|
||||
elsif
|
||||
# Do not fade if it's already very low.
|
||||
b >= a + margin and a <= medium and b <= high
|
||||
then
|
||||
log("new >= old + margin, old <= medium and new <= high.")
|
||||
log("Do not fade if it's already very low.")
|
||||
log("Transition: crossed, no fade.")
|
||||
add(sa, sb)
|
||||
|
||||
# What to do with a loud end and a quiet beginning ?
|
||||
# A good idea is to use a jingle to separate the two tracks,
|
||||
# but that's another story.
|
||||
else
|
||||
# Otherwise, A and B are just too loud to overlap nicely,
|
||||
# or the difference between them is too large and overlapping would
|
||||
# completely mask one of them.
|
||||
# log("No transition: using default.")
|
||||
# default(sa, sb)
|
||||
log("Transition: crossed, fade-in, fade-out.")
|
||||
add(fade.out(sa), fade.in(sb))
|
||||
end
|
||||
end
|
||||
|
||||
cross(width=width, duration=start_next,
|
||||
conservative=conservative, transition,s)
|
||||
end
|
||||
|
||||
def next_song() =
|
||||
log = log(label="next_song")
|
||||
|
||||
let (status, _, data) = http.get(headers=api_headers_next, url_next)
|
||||
let (_, status_code, _) = status
|
||||
log("Requesting the next song from #{url_next}")
|
||||
let (status_code, data) = get_http_data(api_headers_next, url_next)
|
||||
|
||||
log(data)
|
||||
|
||||
|
@ -96,10 +59,9 @@ def just_played(m) =
|
|||
log = log(label="just_played")
|
||||
|
||||
if m["req_id"] != "" then
|
||||
log("Just played request ID: "^quote(m["req_id"]))
|
||||
log('Letting server know we played request ID #{m["req_id"]} here: #{url_played}')
|
||||
played_song = json_of(compact=true, [("song_request", int_of_string(m["req_id"]))])
|
||||
let (status, _, data) = http.post(headers=api_headers_played, data=played_song, url_played)
|
||||
let (_, status_code, _) = status
|
||||
let (status_code, data) = post_http_data(api_headers_played, played_song, url_played)
|
||||
|
||||
if status_code == 204 then
|
||||
log('Successfully reported that the song was played.')
|
||||
|
@ -111,18 +73,25 @@ def just_played(m) =
|
|||
end
|
||||
end
|
||||
|
||||
security = single(id="default", fallback_audio)
|
||||
|
||||
radio = request.dynamic(id="main", default_duration=120., length=60., next_song)
|
||||
radio = fallback(track_sensitive=false, [radio, security])
|
||||
radio = map_metadata(update=false, change_meta, radio)
|
||||
radio = on_metadata(id="main", just_played, radio)
|
||||
radio = amplify(1., override="replay_gain", radio)
|
||||
radio = smart_cross(radio)
|
||||
radio = fallback(track_sensitive=false, [radio, security])
|
||||
|
||||
output.icecast(%mp3,
|
||||
encoding="UTF-8", protocol="http",
|
||||
name=radio_name, description=radio_description, genre=radio_genre, url=radio_url_main,
|
||||
host=stream_address, port=stream_port, password=stream_password, mount="stream128.mp3",
|
||||
radio)
|
||||
output.icecast(%vorbis(samplerate=44100, channels=2, quality=0.9),
|
||||
encoding="UTF-8", protocol="http",
|
||||
name=radio_name, description=radio_description, genre=radio_genre, url=radio_url_main,
|
||||
host=stream_address, port=stream_port, password=stream_password, mount="stream320.ogg",
|
||||
radio)
|
||||
output.icecast(%vorbis(samplerate=44100, channels=2, quality=0.4),
|
||||
encoding="UTF-8", protocol="http",
|
||||
name=radio_name, description=radio_description, genre=radio_genre, url=radio_url_main,
|
||||
|
|
43
tweaks.liq
Normal file
43
tweaks.liq
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Tweaked custom crossfade to deal with jingles..
|
||||
def smart_cross(~start_next=5.,~fade_in=3.,~fade_out=3.,
|
||||
~default=(fun (a,b) -> sequence([a, b])),
|
||||
~high=-15., ~medium=-32., ~margin=4.,
|
||||
~width=2.,~conservative=false,s)
|
||||
fade.out = fade.out(type="sin", duration=fade_out)
|
||||
fade.in = fade.in(type="sin", duration=fade_in)
|
||||
add = fun (a,b) -> add(normalize=false, [b, a])
|
||||
log = log(label="smart_cross")
|
||||
|
||||
def transition(a,b,ma,mb,sa,sb)
|
||||
list.iter(fun(x)-> log(level=4, "Before: #{x}"), ma)
|
||||
list.iter(fun(x)-> log(level=4, "After : #{x}"), mb)
|
||||
|
||||
if ma["type"] == "J" or mb["type"] == "J" then
|
||||
log("Old or new file is a jingle: sequenced transition.")
|
||||
sequence([sa, sb])
|
||||
elsif
|
||||
# Do not fade if it's already very low.
|
||||
b >= a + margin and a <= medium and b <= high
|
||||
then
|
||||
log("new >= old + margin, old <= medium and new <= high.")
|
||||
log("Do not fade if it's already very low.")
|
||||
log("Transition: crossed, no fade.")
|
||||
add(sa, sb)
|
||||
|
||||
# What to do with a loud end and a quiet beginning ?
|
||||
# A good idea is to use a jingle to separate the two tracks,
|
||||
# but that's another story.
|
||||
else
|
||||
# Otherwise, A and B are just too loud to overlap nicely,
|
||||
# or the difference between them is too large and overlapping would
|
||||
# completely mask one of them.
|
||||
# log("No transition: using default.")
|
||||
# default(sa, sb)
|
||||
log("Transition: crossed, fade-in, fade-out.")
|
||||
add(fade.out(sa), fade.in(sb))
|
||||
end
|
||||
end
|
||||
|
||||
cross(width=width, duration=start_next,
|
||||
conservative=conservative, transition,s)
|
||||
end
|
Loading…
Reference in a new issue