Skip to content

Instantly share code, notes, and snippets.

@LunaSquee
Last active February 16, 2023 01:52
Show Gist options
  • Save LunaSquee/274bc22c77c68232c6aa90635bdcd0fc to your computer and use it in GitHub Desktop.
Save LunaSquee/274bc22c77c68232c6aa90635bdcd0fc to your computer and use it in GitHub Desktop.
Liquidsoap radio + youtube-dl queueing (Node.js as helper)
// $ node queue <file name or youtube URL>
const net = require('net')
let client = net.connect(1234, 'localhost')
client.on('connect', function () {
if (process.argv[2]) {
console.log('Queueing ' + process.argv[2])
client.write('queue.push smart:' + process.argv[2] + '\r\n')
}
client.write('queue.queue\r\n')
client.end('quit\r\n')
})
#!/usr/bin/liquidsoap
# Some parts of this code were taken from djazz's Parasprite Radio project.
# Check it out here: https://github.com/daniel-j/parasprite-radio
set("log.file.path", "radio.log")
# Start the stream by giving it a playlist
radio = mksafe(playlist("~/radio/playlist.m3u"))
# Use the telnet server for requests
set("server.telnet", true)
set("server.telnet.bind_addr", "0.0.0.0")
set("server.telnet.port", 1234)
set("harbor.bind_addr", "0.0.0.0")
set("harbor.verbose", false)
set("harbor.reverse_dns", true)
live_meta = ref []
live_token = ref ""
skip_source = ref blank()
# Main queue for the radio
queue = audio_to_stereo(request.equeue(id="queue", conservative=true, length=60., timeout=1000.))
# Fallback to the radio when the queue is empty
radio = fallback([queue, radio])
# Remove songs marked as temporary
def track_end_cleanup(time, m)
if m["temporary"] == "true" and m["filename"] != "" then
print("rm "^quote(m["filename"]))
system("rm "^quote(m["filename"]))
end
end
# since liquidsoap doesn't have list[key] = value
# this keeps the assoc list clean of duplicate keys
# usage: list.set_pair((key, value), list)
def list.set_pair(p, l)
l = list.remove_assoc(fst(p), l)
list.add(p, l)
end
# same as above but takes a list of pairs instead
def list.set_list(new, l)
outl = ref l
list.iter(fun(p) -> begin
outl := list.remove_assoc(fst(p), !outl)
end, new)
list.append(new, !outl)
end
def add_skip_command(~command,s)
# Register the command:
server.register(
usage=command,
description="Skip the current song in source.",
command,
fun(_) -> begin
print("Skipping...")
source.skip(s)
"OK!"
end
)
end
# Register a custom protocol to better queue songs
def smart_protocol(arg,delay)
res = get_process_lines("node smart "^quote(arg))
print(res)
res
end
add_protocol("smart", smart_protocol)
# Add skip commands
add_skip_command(command="queue.skip", queue)
add_skip_command(command="skip", radio)
skip_source := radio
# Remove songs marked as temporary
radio = on_end(delay=0., track_end_cleanup, radio)
# Crossfade music
radio = smart_crossfade(conservative=true, start_next=3., fade_in=2., fade_out=3., width=5., radio)
# Temporary authentication means
live_pass_word = "hackme"
# Fade to livestream
def to_live(old,new)
old = fade.final(duration=2., old)
new = fade.initial(duration=2., new)
sequence(merge=true, [old,new])
end
# Fade to songs
def to_songs(a,b)
add(normalize=false, [
sequence([
amplify(0.0, fade.final(duration=3.0, b)),
fade.initial(duration=3.0, b)
]),
fade.final(duration=8.0, a)
])
end
def auth_live(user,password)
print("LIVE: A user is connecting...")
current_token = !live_token
if current_token == "" or current_token == user then
if password == live_pass_word then
print("LIVE: User authenticated successfully")
live_token := user
true
else
print("LIVE: Authentication error: Invalid username/password.")
false
end
else
print("LIVE: Another user is already connected!")
false
end
end
def user_connected(headers)
print("LIVE: Headers")
print(headers)
m = list.set_list([
("live_name", headers["ice-name"]),
("artist", !live_token),
("live_description", headers["ice-description"]),
("live_ice_genre", headers["ice-genre"]),
("live_ice_url", headers["ice-url"])
], !live_meta)
# cleanup
def filter(x)
value = snd(x)
if value == "(none)" or value == " " then
false
else
true
end
end
m = list.filter(filter, m)
live_meta := m
end
def user_disconnected()
print("LIVE: User disconnected")
live_token := ""
end
def live_begin(m)
person = !live_token
if person != "" then
source.skip(!skip_source)
print("LIVE: Got track! Streamer: "^person^"; Stream name: "^m["live_name"])
end
end
def live_end(t,m)
print("LIVE: Stopped streaming")
end
# Live input
live = audio_to_stereo(input.harbor(
"/", # supporting shoutcast and icecast (empty mount) sources
id = "live",
buffer = 10.,
max = 15.,
port = 8009,
auth = auth_live,
icy = true, # enables Shoutcast support (untested, uses port_input+1)
icy_metadata_charset = 'UTF-8',
metadata_charset = 'UTF-8',
on_connect = user_connected,
on_disconnect = user_disconnected
))
live = map_metadata(fun(m) -> !live_meta, live)
live = map_metadata(fun(m) -> begin
l = ref []
if m["song"] != "" then
info = string.extract(pattern="(.*?) - (.*)$", m["song"])
artist = info["1"]
title = info["2"]
l := list.append([
("title", title),
("artist", artist)
], !l)
else
if m["live_name"] != "" then
l := list.add(("title", "LIVE: "^m['live_name']), !l)
else
l := list.add(("title", "LIVE: "^!live_token^"'s stream"), !l)
end
end
!l
end, live)
live = on_track(live_begin, live)
live = on_end(delay=0., live_end, live)
radio = fallback(track_sensitive=false, transitions=[to_live, to_songs], [live, radio])
# Send it off to icecast!
output.icecast(%vorbis.cbr(samplerate=44100, channels=2, bitrate=320),
port = 8008,
password="hackme",
mount = "mystream.ogg",
name = "Iced Potato Radio",
description = "Diamond's personal listening hub.",
url="http://radio.lunasqu.ee/",
radio)
'use strict'
// Some parts of this code were taken from djazz's Parasprite Radio project.
// Check it out here: https://github.com/daniel-j/parasprite-radio
const spawn = require('child_process').spawn
const os = require('os')
// URLs that youtube-dl should hande for you
const ytdlsupports = ["youtube.com/", "youtu.be/", "soundcloud.com/"]
let arg = (process.argv[2] || '').trim()
function protocol (handleCb) {
let yt = spawn('youtube-dl', ['--no-playlist', '--playlist-end', 1, '-j', '-f', 'bestaudio/best', arg])
let output = ''
yt.stdout.on('data', function (chunk) {
output += chunk.toString('utf8')
})
yt.on('close', function () {
let data = JSON.parse(output)
delete data.formats
fetchVideo(data, handleCb)
})
}
function fetchVideo (data, cb) {
if (data.acodec !== 'mp3' || data.vcodec !== 'none') {
let tempName = os.tmpdir() + '/tmp.yt.' + data.id + '.mp3'
// joint stereo VBR2 mp3
spawn('ffmpeg', ['-i', data.url, '-codec:a', 'libmp3lame', '-q:a', 2, '-joint_stereo', 1, '-y', tempName])
data.filename = tempName
console.error('Downloading ' + data.title + '...')
} else {
data.filename = data.url + '&liquidtype=.mp3'
}
outputVideo(data, cb)
}
function outputVideo (video, cb) {
cb({
title: video.title,
artist: video.uploader,
url: video.webpage_url,
art: video.thumbnail,
source: video.filename,
temporary: true
})
}
function formatter (o) {
if (Array.isArray(o)) {
o.forEach(utils.formatter)
return
}
if (o.error) {
o = utils.sayErr(o.what, o.error)
}
let list = []
for (let key in o) {
if (o.hasOwnProperty(key) && key !== 'source' && o[key] !== null && o[key] !== undefined) {
list.push(key + '=' + JSON.stringify(o[key]))
}
}
let out = ''
if (list.length > 0) {
out += 'annotate:' + list.join(',') + ':'
}
out += o.source
console.log(out)
}
function isUrl(s) {
var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
return regexp.test(s);
}
function inArray (a, b) {
for (let i in b) {
if (a.indexOf(b[i]) !== -1) {
return true
}
}
return false
}
function retrieve() {
if (!arg && arg == '') return console.log('')
if (isUrl(arg)) {
if (inArray(arg, ytdlsupports)) {
protocol((dat) => {
if (!dat) return
formatter(dat)
})
} else {
console.log(arg)
}
} else {
if (arg.indexOf('/my/music/dir/') === -1)
arg = '/my/music/dir/' + arg
console.log(arg)
}
}
retrieve()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment