When Libretime streams a webstream, the ID3 data is put into a single title field, leaving the artist field blank. When processing the ID3 data, Libretime concatenates the blank 'artist' field with the title, leaving '- title'. ShoutCAST rejects title updates that begin with punctuation for stylistic reasons (thanks guys) and so effectively ignores all ID3 data coming with the stream, falling back to the 'Station off-air' message. This PR uses the title field alone if the artist field is blank, but both when the artist data is available e.g. when streaming an MP3.
419 lines
13 KiB
Text
419 lines
13 KiB
Text
def notify(m)
|
||
command = "timeout --signal=KILL 45 pyponotify --media-id=#{m['schedule_table_id']} &"
|
||
log(command)
|
||
system(command)
|
||
end
|
||
|
||
def notify_queue(m)
|
||
f = !dynamic_metadata_callback
|
||
ignore(f(m))
|
||
notify(m)
|
||
end
|
||
|
||
def notify_stream(m)
|
||
json_str = string.replace(pattern="\n",(fun (s) -> ""), json_of(m))
|
||
#if a string has a single apostrophe in it, let's comment it out by ending the string before right before it
|
||
#escaping the apostrophe, and then starting a new string right after it. This is why we use 3 apostrophes.
|
||
json_str = string.replace(pattern="'",(fun (s) -> "'\''"), json_str)
|
||
command = "timeout --signal=KILL 45 pyponotify --webstream='#{json_str}' --media-id=#{!current_dyn_id} &"
|
||
|
||
if !current_dyn_id != "-1" then
|
||
log(command)
|
||
system(command)
|
||
end
|
||
end
|
||
|
||
# A function applied to each metadata chunk
|
||
def append_title(m) =
|
||
log("Using stream_format #{!stream_metadata_type}")
|
||
|
||
if list.mem_assoc("mapped", m) then
|
||
#protection against applying this function twice. It shouldn't be happening
|
||
#and bug file with Liquidsoap.
|
||
m
|
||
else
|
||
if !stream_metadata_type == 1 then
|
||
[("title", "#{!show_name} - #{m['artist']} - #{m['title']}"), ("mapped", "true")]
|
||
elsif !stream_metadata_type == 2 then
|
||
[("title", "#{!station_name} - #{!show_name}"), ("mapped", "true")]
|
||
else
|
||
if "#{m['artist']}" == "" then
|
||
[("title", "#{m['title']}"), ("mapped", "true")]
|
||
else
|
||
[("title", "#{m['artist']} - #{m['title']}"), ("mapped", "true")]
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
def crossfade_airtime(s)
|
||
#duration is automatically overwritten by metadata fields passed in
|
||
#with audio
|
||
s = fade.in(type="log", duration=0., s)
|
||
s = fade.out(type="log", duration=0., s)
|
||
fader = fun (a,b) -> add(normalize=false,[b,a])
|
||
cross(fader,s)
|
||
end
|
||
|
||
def transition(a,b) =
|
||
log("transition called...")
|
||
add(normalize=false,
|
||
[ sequence([ blank(duration=0.01),
|
||
fade.initial(duration=!default_dj_fade, b) ]),
|
||
fade.final(duration=!default_dj_fade, a) ])
|
||
end
|
||
|
||
# we need this function for special transition case(from default to queue)
|
||
# we don't want the trasition fade to have effect on the first song that would
|
||
# be played siwtching out of the default(silent) source
|
||
def transition_default(a,b) =
|
||
log("transition called...")
|
||
if !just_switched then
|
||
just_switched := false
|
||
add(normalize=false,
|
||
[ sequence([ blank(duration=0.01),
|
||
fade.initial(duration=!default_dj_fade, b) ]),
|
||
fade.final(duration=!default_dj_fade, a) ])
|
||
else
|
||
just_switched := false
|
||
b
|
||
end
|
||
end
|
||
|
||
|
||
# Define a transition that fades out the
|
||
# old source, adds a single, and then
|
||
# plays the new source
|
||
def to_live(old,new) =
|
||
# Fade out old source
|
||
old = fade.final(old)
|
||
# Compose this in sequence with
|
||
# the new source
|
||
sequence([old,new])
|
||
end
|
||
|
||
|
||
def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, description, genre, user, s, stream, connected, name, channels) =
|
||
source = ref s
|
||
def on_error(msg)
|
||
connected := "false"
|
||
command = "timeout --signal=KILL 45 pyponotify --error='#{msg}' --stream-id=#{stream} --time=#{!time} &"
|
||
system(command)
|
||
log(command)
|
||
5.
|
||
end
|
||
def on_connect()
|
||
connected := "true"
|
||
command = "timeout --signal=KILL 45 pyponotify --connect --stream-id=#{stream} --time=#{!time} &"
|
||
system(command)
|
||
log(command)
|
||
end
|
||
|
||
stereo = (channels == "stereo")
|
||
|
||
if output_type == "icecast" then
|
||
user_ref = ref user
|
||
if user == "" then
|
||
user_ref := "source"
|
||
end
|
||
output_mono = output.icecast(host = host,
|
||
port = port,
|
||
password = pass,
|
||
mount = mount_point,
|
||
fallible = true,
|
||
url = url,
|
||
description = description,
|
||
name = name,
|
||
genre = genre,
|
||
user = !user_ref,
|
||
on_error = on_error,
|
||
on_connect = on_connect)
|
||
|
||
output_stereo = output.icecast(host = host,
|
||
port = port,
|
||
password = pass,
|
||
mount = mount_point,
|
||
fallible = true,
|
||
url = url,
|
||
description = description,
|
||
name = name,
|
||
genre = genre,
|
||
user = !user_ref,
|
||
on_error = on_error,
|
||
on_connect = on_connect)
|
||
if type == "mp3" then
|
||
%include "mp3.liq"
|
||
end
|
||
if type == "ogg" then
|
||
%include "ogg.liq"
|
||
end
|
||
|
||
%ifencoder %opus
|
||
if type == "opus" then
|
||
%include "opus.liq"
|
||
end
|
||
%endif
|
||
|
||
# FDK-AAC is the only good AAC encoder. libvoaac is deprecated and aacplus is subpar.
|
||
# The difference in compression quality is clearly audible. -- Albert
|
||
# %ifencoder %aac
|
||
# if type == "aac" then
|
||
# %include "aac.liq"
|
||
# end
|
||
# %endif
|
||
|
||
# %ifencoder %aacplus
|
||
# if type == "aacplus" then
|
||
# %include "aacplus.liq"
|
||
# end
|
||
# %endif
|
||
|
||
%ifencoder %fdkaac
|
||
if type == "aac" then
|
||
%include "fdkaac.liq"
|
||
end
|
||
%endif
|
||
else
|
||
user_ref = ref user
|
||
if user == "" then
|
||
user_ref := "source"
|
||
end
|
||
|
||
output_mono = output.shoutcast(id = "shoutcast_stream_#{stream}",
|
||
host = host,
|
||
port = port,
|
||
password = pass,
|
||
fallible = true,
|
||
url = url,
|
||
genre = genre,
|
||
name = description,
|
||
user = !user_ref,
|
||
on_error = on_error,
|
||
on_connect = on_connect)
|
||
|
||
output_stereo = output.shoutcast(id = "shoutcast_stream_#{stream}",
|
||
host = host,
|
||
port = port,
|
||
password = pass,
|
||
fallible = true,
|
||
url = url,
|
||
genre = genre,
|
||
name = description,
|
||
user = !user_ref,
|
||
on_error = on_error,
|
||
on_connect = on_connect)
|
||
|
||
if type == "mp3" then
|
||
%include "mp3.liq"
|
||
end
|
||
|
||
%ifencoder %aac
|
||
if type == "aac" then
|
||
%include "aac.liq"
|
||
end
|
||
%endif
|
||
|
||
%ifencoder %aacplus
|
||
if type == "aacplus" then
|
||
%include "aacplus.liq"
|
||
end
|
||
%endif
|
||
end
|
||
end
|
||
|
||
# Add a skip function to a source
|
||
# when it does not have one
|
||
# by default
|
||
#def add_skip_command(s)
|
||
# # A command to skip
|
||
# def skip(_)
|
||
# # get playing (active) queue and flush it
|
||
# l = list.hd(server.execute("queue.secondary_queue"))
|
||
# l = string.split(separator=" ",l)
|
||
# list.iter(fun (rid) -> ignore(server.execute("queue.remove #{rid}")), l)
|
||
#
|
||
# l = list.hd(server.execute("queue.primary_queue"))
|
||
# l = string.split(separator=" ", l)
|
||
# if list.length(l) > 0 then
|
||
# source.skip(s)
|
||
# "Skipped"
|
||
# else
|
||
# "Not skipped"
|
||
# end
|
||
# end
|
||
# # Register the command:
|
||
# server.register(namespace="source",
|
||
# usage="skip",
|
||
# description="Skip the current song.",
|
||
# "skip",fun(s) -> begin log("source.skip") skip(s) end)
|
||
#end
|
||
|
||
def clear_queue(s)
|
||
source.skip(s)
|
||
end
|
||
|
||
def set_dynamic_source_id(id) =
|
||
current_dyn_id := id
|
||
string_of(!current_dyn_id)
|
||
end
|
||
|
||
def get_dynamic_source_id() =
|
||
string_of(!current_dyn_id)
|
||
end
|
||
|
||
#cc-4633
|
||
|
||
|
||
# NOTE
|
||
# A few values are hardcoded and may be dependent:
|
||
# - the delay in gracetime is linked with the buffer duration of input.http
|
||
# (delay should be a bit less than buffer)
|
||
# - crossing duration should be less than buffer length
|
||
# (at best, a higher duration will be ineffective)
|
||
|
||
# HTTP input with "restart" command that waits for "stop" to be effected
|
||
# before "start" command is issued. Optionally it takes a new URL to play,
|
||
# which makes it a convenient replacement for "url".
|
||
# In the future, this may become a core feature of the HTTP input.
|
||
# TODO If we stop and restart quickly several times in a row,
|
||
# the data bursts accumulate and create buffer overflow.
|
||
# Flushing the buffer on restart could be a good idea, but
|
||
# it would also create an interruptions while the buffer is
|
||
# refilling... on the other hand, this would avoid having to
|
||
# fade using both cross() and switch().
|
||
def input.http_restart(~id,~initial_url="http://dummy/url")
|
||
|
||
source = audio_to_stereo(input.http(buffer=5.,max=15.,id=id,autostart=false,initial_url))
|
||
|
||
def stopped()
|
||
"stopped" == list.hd(server.execute("#{id}.status"))
|
||
end
|
||
|
||
server.register(namespace=id,
|
||
"restart",
|
||
usage="restart [url]",
|
||
fun (url) -> begin
|
||
if url != "" then
|
||
log(string_of(server.execute("#{id}.url #{url}")))
|
||
end
|
||
log(string_of(server.execute("#{id}.stop")))
|
||
add_timeout(0.5,
|
||
{ if stopped() then
|
||
log(string_of(server.execute("#{id}.start"))) ;
|
||
(-1.)
|
||
else 0.5 end})
|
||
"OK"
|
||
end)
|
||
|
||
# Dummy output should be useless if HTTP stream is meant
|
||
# to be listened to immediately. Otherwise, apply it.
|
||
#
|
||
# output.dummy(fallible=true,source)
|
||
|
||
source
|
||
|
||
end
|
||
|
||
# Transitions between URL changes in HTTP streams.
|
||
def cross_http(~debug=true,~http_input_id,source)
|
||
|
||
id = http_input_id
|
||
last_url = ref ""
|
||
change = ref false
|
||
|
||
def on_m(m)
|
||
notify_stream(m)
|
||
changed = m["source_url"] != !last_url
|
||
log("URL now #{m['source_url']} (change: #{changed})")
|
||
if changed then
|
||
if !last_url != "" then change := true end
|
||
last_url := m["source_url"]
|
||
end
|
||
end
|
||
|
||
# We use both metadata and status to know about the current URL.
|
||
# Using only metadata may be more precise is crazy corner cases,
|
||
# but it's also asking too much: the metadata may not pass through
|
||
# before the crosser is instantiated.
|
||
# Using only status in crosser misses some info, eg. on first URL.
|
||
source = on_metadata(on_m,source)
|
||
|
||
cross_d = 3.
|
||
|
||
def crosser(a,b)
|
||
url = list.hd(server.execute('#{id}.url'))
|
||
status = list.hd(server.execute('#{id}.status'))
|
||
on_m([("source_url",url)])
|
||
if debug then
|
||
log("New track inside HTTP stream")
|
||
log(" status: #{status}")
|
||
log(" need to cross: #{!change}")
|
||
log(" remaining #{source.remaining(a)} sec before, \
|
||
#{source.remaining(b)} sec after")
|
||
end
|
||
if !change then
|
||
change := false
|
||
# In principle one should avoid crossing on a live stream
|
||
# it'd be okay to do it here (eg. use add instead of sequence)
|
||
# because it's only once per URL, but be cautious.
|
||
sequence([fade.out(duration=cross_d,a),fade.in(b)])
|
||
else
|
||
# This is done on tracks inside a single stream.
|
||
# Do NOT cross here or you'll gradually empty the buffer!
|
||
sequence([a,b])
|
||
end
|
||
end
|
||
|
||
# Setting conservative=true would mess with the delayed switch below
|
||
cross(duration=cross_d,conservative=false,crosser,source)
|
||
|
||
end
|
||
|
||
# Custom fallback between http and default source with fading of
|
||
# beginning and end of HTTP stream.
|
||
# It does not take potential URL changes into account, as long as
|
||
# they do not interrupt streaming (thanks to the HTTP buffer).
|
||
def http_fallback(~http_input_id,~http,~default)
|
||
|
||
id = http_input_id
|
||
|
||
# We use a custom switching predicate to trigger switching (and thus,
|
||
# transitions) before the end of a track (rather, end of HTTP stream).
|
||
# It is complexified because we don't want to trigger switching when
|
||
# HTTP disconnects for just an instant, when changing URL: for that
|
||
# we use gracetime below.
|
||
|
||
def gracetime(~delay=3.,f)
|
||
last_true = ref 0.
|
||
{ if f() then
|
||
last_true := gettimeofday()
|
||
true
|
||
else
|
||
gettimeofday() < !last_true+delay
|
||
end }
|
||
end
|
||
|
||
def connected()
|
||
status = list.hd(server.execute("#{id}.status"))
|
||
not(list.mem(status,["polling","stopped"]))
|
||
end
|
||
connected = gracetime(connected)
|
||
|
||
def to_live(a,b) =
|
||
log("TRANSITION to live")
|
||
add(normalize=false,
|
||
[fade.initial(b),fade.final(a)])
|
||
end
|
||
def to_static(a,b) =
|
||
log("TRANSITION to static")
|
||
sequence([fade.out(a),fade.initial(b)])
|
||
end
|
||
|
||
switch(
|
||
track_sensitive=false,
|
||
transitions=[to_live,to_static],
|
||
[(# make sure it is connected, and not buffering
|
||
{connected() and source.is_ready(http) and !webstream_enabled}, http),
|
||
({true},default)])
|
||
|
||
end
|