Skip to content

Instantly share code, notes, and snippets.

@daniel-j
Last active January 2, 2018 19:04
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daniel-j/2c8db53158f6000d6c7d to your computer and use it in GitHub Desktop.
Save daniel-j/2c8db53158f6000d6c7d to your computer and use it in GitHub Desktop.
Parasprite Radio files and utilities (for LiquidSoap and MPD)
#!/usr/bin/env node
var net = require('net');
var client = net.connect(1234, "localhost");
client.pipe(process.stdout);
client.on('connect', function () {
console.log("Connected!");
process.stdin.on('data', function (data) {
client.write(data.toString()+"\r\n");
});
});
#!/usr/bin/env node
var net = require('net');
var client = net.connect(1234, "localhost");
client.pipe(process.stdout);
client.on('connect', function () {
if (process.argv[2]) {
client.write("request.push /home/djazz/music/"+process.argv[2]+"\r\n");
}
client.write("request.queue\r\n");
client.write("quit\r\n");
client.end();
});
#!/bin/bash
while true
do
mpc -q idle playlist > /dev/null
filename=`mpc -q playlist -f %file% | head -n1`
echo "Queueing $filename"
node queue "$filename"
sleep 1
mpc -q clear
done
#!/usr/bin/liquidsoap
# Put the log file in some directory
set("log.file.path","./radio.log")
set("log.file", false)
# Print log messages to the console,
# can also be done by passing the -v option to liquidsoap.
set("log.stdout", true)
# 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")
%include "credentials.liq"
%include "tunein.liq"
radio_name = "Parasprite Radio"
description = "Brony music 24/7!"
radio_url = "http://radio.djazz.se/"
radio_genre = "Pony"
icecast_host = "icecast.djazz.se"
icecast_port = 5000
encoding = "UTF-8"
tunein_station_id = "s225092"
harbor_port = 7000
meta = ref []
# Mixes two streams, with faded transitions between the state when only the
# normal stream is available and when the special stream gets added on top of
# it.
# @category Source / Track Processing
# @param ~delay Delay before starting the special source.
# @param ~p Portion of amplitude of the normal source in the mix.
# @param ~normal The normal source, which could be called the carrier too.
# @param ~special The special source.
def smooth_add(~delay=0.5,~p=0.2,~normal,~special)
d = delay
fade.final = fade.final(duration=d*2.)
fade.initial = fade.initial(duration=d*2.)
q = 1. - p
c = amplify
fallback(track_sensitive=false,
[special,normal],
transitions=[
fun(normal,special)->
add(normalize=false,
[c(p,normal),
c(q,fade.final(type="sin",normal)),
sequence([blank(duration=d),c(q,special)])]),
fun(special,normal)->
add(normalize=false,
[c(p,normal),
c(q,fade.initial(type="sin",normal))])
])
end
def scrobble(~user,~password,m)
audioscrobbler.nowplaying(user=user, password=password, m)
audioscrobbler.submit(user=user, password=password, m)
tunein.submit(partner_id=tunein_partner_id, partner_key=tunein_partner_key, station_id=tunein_station_id, m)
end
def update_nowplaying(m)
# is this needed? oh well..
recode = string.recode(out_enc="UTF-8")
def f(x) =
(recode(fst(x)),recode(snd(x)))
end
meta := list.map(f,m)
data = json_of(compact=true, m)
# send metadata to script
system("node ../app/util/track-change.js "^quote(base64.encode(data)))
# update the info on server
ignore(http.post(
data = data,
headers = [("Content-Type", "application/json; charset=utf-8")],
timeout = 5.0,
"http://127.0.0.1:8000/liquidsoap-meta"
))
end
# Return the json content
# of meta
def get_meta(~protocol,~data,~headers,uri) =
m = !meta
http_response(
protocol = protocol,
code = 200,
headers = [("Content-Type","application/json; charset=utf-8")],
data = json_of(compact=true, m)
)
end
# playlists
songs = mksafe(audio_to_stereo(playlist(reload_mode="watch", reload=600, prefix="/mnt/", "/home/djazz/.mpd/playlists/radio.m3u")))
friendship = audio_to_stereo(playlist(reload_mode="watch", reload=600, "friendship.m3u"))
# queue: requests; announce: plays over music
queue = audio_to_stereo(request.queue(id="request"))
announce = audio_to_stereo(request.queue(id="announce"))
source = fallback([queue, songs])
# scrobble music (last.fm & tunein) but not jingles etc..
source = on_metadata(scrobble(user=lastfm_username, password=lastfm_password), source)
# TODO: replace with jingles
source = rotate(weights=[1,10], [friendship, source])
# update now playing (and generate cover art..)
source = on_metadata(update_nowplaying, source)
# http endpoint to get metadata
harbor.http.register(port=harbor_port, method="GET", "/getmeta", get_meta)
# submit to Liquidsoap Flow
source = register_flow(
radio = radio_name,
description = description,
website = radio_url,
genre = radio_genre,
user = flow_username,
password = flow_password,
streams = [
("mp3/320k", "http://radio.djazz.se/stream/"),
("aac/64k", "http://radio.djazz.se/stream/?mobile")
],
source
)
# play announcements over the music
source = smooth_add(delay=0.8, p=0.15, normal=source, special=announce)
# audio tweaking
#source = normalize(gain_max=3., gain_min=-3., source)
source = smart_crossfade(start_next=2., fade_in=2., fade_out=3., width=3., source)
output.icecast(
%mp3.cbr(
bitrate = 320,
stereo_mode = "joint_stereo",
internal_quality = 2,
id3v2 = true
),
host = icecast_host,
port = icecast_port,
mount = "radio",
password = icecast_password,
name = radio_name,
description = description,
url = radio_url,
genre = radio_genre,
encoding = encoding,
source
)
output.icecast(
%fdkaac(bitrate=64),
host = icecast_host,
port = icecast_port,
mount = "radio_mobile",
password = icecast_password,
name = radio_name,
description = description,
url = radio_url,
genre = radio_genre,
encoding = encoding,
source
)
#!/usr/bin/env node
'use strict';
var mpd = require('mpd');
var fs = require('fs');
var cmd = mpd.cmd;
var client = mpd.connect({
port: 6600,
host: '127.0.0.1'
});
function getPlaylist() {
console.log("Reading playlist...");
client.sendCommand(cmd("listplaylistinfo", ['radio']), function(err, msg) {
if (err) {
console.error(err);
return;
}
var playlist = msg.split("\nfile: ");
for (var i = 0; i < playlist.length; i++) {
if (i > 0) {
playlist[i] = "file: "+playlist[i];
}
playlist[i] = playlist[i].split("\n");
if (i === playlist.length-1) {
playlist[i].pop();
}
var o = {};
for (var j = 0; j < playlist[i].length; j++) {
var line = playlist[i][j];
var pos = line.indexOf(": ");
if (pos !== -1) {
var key = line.substr(0, pos).toLowerCase();
var value = line.substr(pos+2);
o[key] = value;
}
}
o['time'] = +o['time'];
delete o['file'];
delete o['last-modified'];
delete o['date'];
playlist[i] = o;
}
fs.writeFile(__dirname+'/playlist.json', JSON.stringify(playlist), function (err) {
if (err) {
console.error("Unable to write file. "+err);
return;
}
console.log("Playlist updated!");
});
});
}
client.on('ready', function() {
console.log("ready");
getPlaylist();
});
client.on('system', function(name) {
console.log("update", name);
if (name === "stored_playlist" || name === "database") {
getPlaylist();
}
});
#!/usr/bin/env node
'use strict';
var fs = require('fs')
var path = require('path')
var mm = require('musicmetadata')
var lwip = require('lwip')
function typeToMime(type) {
switch (type) {
case 'jpg': type = 'image/jpeg'; break
case 'jpeg': type = 'image/jpeg'; break
case 'png': type = 'image/png'; break
default: type = null; break
}
return type
}
function imageFromFile(filename, cb) {
var stream = fs.createReadStream(filename)
var gotimg = false
//allowed = ['.mp3', '.ogg', '.flac', '.wma']
//if allowed.indexOf(path.extname(filename).toLowerCase()) == -1
// cb 'non-allowed file type'
// return
var parser = mm(stream, {}, function (err, meta) {
var pictures = meta.picture
if (pictures && pictures[0]) {
var type = typeToMime(pictures[0].format)
if (type !== null) {
cb(null, type, meta.picture[0].data)
gotimg = true
}
}
if (!gotimg) {
var dir = path.dirname(filename)
fs.readdir(dir, function (err, result) {
if (err) {
cb(err)
return
}
var valid = ['.png', '.jpg', '.jpeg']
var commonFiles = ['cover', 'folder', 'album', 'artist', 'art']
result = result.filter(function (f) {
var ext = path.extname(f).toLowerCase()
return valid.indexOf(ext) !== -1
})
var img = null
for (var i = 0; i < result.length; i++) {
var file = result[i]
if (img !== null) break
var f = file.toLowerCase()
if (commonFiles.indexOf(path.basename(f, path.extname(f))) !== -1) {
img = file
}
else {
for (var j = 0; j < commonFiles.length; j++) {
var common = commonFiles[j]
if (f.indexOf(common) !== -1) {
img = file
break
}
}
}
}
if (img == null) {
cb('no image found\n'+JSON.stringify(meta))
}
else {
fs.readFile(path.join(dir, img), function (err, data) {
if (err) {
cb(err)
}
else {
cb(null, typeToMime(path.extname(img).substr(1)), data)
}
})
}
})
//res.sendFile path.join(filename+'/../cover.jpg'),
// root: config.media_dir
}
})
parser.on('done', function (err) {
stream.destroy()
if (err && !gotimg)
cb(err)
})
parser.on('error', function (err) {
stream.destroy()
if (err && !gotimg)
cb(err)
})
stream.on('error', function (err) {
if (!gotimg)
cb(err)
})
}
var liq = JSON.parse(new Buffer(process.argv[2], "base64").toString('utf8'))
console.log(liq);
var json = {
title: liq.title || path.basename(liq.filename, path.extname(liq.filename)),
artist: liq.artist || null,
album: liq.album || null,
albumartist: liq.albumartist || null,
url: liq.url || null,
year: +liq.year || null
}
fs.writeFile(__dirname+'/now/json', JSON.stringify(json), 'utf8')
console.log("Generating art...")
imageFromFile(liq.filename, function (err, type, data) {
if (err) {
console.log("Failed to generate art! "+err)
fs.unlink(__dirname+'/now/image-full', function () {})
fs.unlink(__dirname+'/now/image-small.png', function () {})
fs.unlink(__dirname+'/now/image-tiny.png', function () {})
fs.unlink(__dirname+'/now/type.txt', function () {})
}
else {
fs.writeFile(__dirname+'/now/image-full', data, function (err) {
if (err) throw err
console.log("Saved full art")
})
fs.writeFile(__dirname+'/now/type.txt', type)
var t = type.split('/')[1] === 'png' ? 'png':'jpg'
lwip.open(data, t, function (err, image) {
if (err) console.log(err)
else {
image.batch()
.cover(80, 80)
.writeFile(__dirname+'/now/image-tiny.png', function (err) {
if (err) throw err
console.log("Saved tiny art")
})
}
})
lwip.open(data, t, function (err, image) {
if (err) console.log(err)
else {
image.batch()
.cover(350, 350)
.writeFile(__dirname+'/now/image-small.png', function (err) {
if (err) throw err
console.log("Saved small art")
})
}
})
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment