Skip to content

Instantly share code, notes, and snippets.

@sk22
Last active March 16, 2022 00:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sk22/ad87ffebb6df2f2d5f181fb6830d9050 to your computer and use it in GitHub Desktop.
Save sk22/ad87ffebb6df2f2d5f181fb6830d9050 to your computer and use it in GitHub Desktop.
gerbera as an upnp-av/dlna server for web radio stations with nginx proxying https streams over http :)
<?xml version="1.0" encoding="UTF-8"?>
<config version="2" xmlns="http://mediatomb.cc/config/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://mediatomb.cc/config/2 http://mediatomb.cc/config/2.xsd">
<!--
See http://gerbera.io or read the docs for more
information on creating and using config.xml configuration files.
-->
<!-- ... -->
<import hidden-files="no">
<scripting script-charset="UTF-8">
<common-script>/usr/share/gerbera/js/common.js</common-script>
<playlist-script>/etc/gerbera/radio-playlists.js</playlist-script>
</scripting>
<!-- ... -->
</import>
<!-- ... -->
// 📻 modified by sk22 to use an nginx proxy for web radios
// (radio emoji indicates modified code)
var PROXY_PORT = 49180; // 📻
// automatically generated output of `hostname -I` because nginx is running on localhost
var IP_ADDRESSES = '192.168.0.182 ';
// Default MediaTomb playlist parsing script.
//
// Note: This script currently only handles .m3u and .pls playlists, but it can
// easily be extended. It doesn't use the "correct" way to parse the playlist,
// but a more fault-tolerant way, so it will try to do it's best and won't
// complain even if the playlist is completely messed up.
/*MT_F*
MediaTomb - http://www.mediatomb.cc/
playlists.js - this file is part of MediaTomb.
Copyright (C) 2006-2010 Gena Batyan <bgeradz@mediatomb.cc>,
Sergey 'Jin' Bostandzhyan <jin@mediatomb.cc>,
Leonhard Wimmer <leo@mediatomb.cc>
This file is free software; the copyright owners give unlimited permission
to copy and/or redistribute it; with or without modifications, as long as
this notice is preserved.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$Id$
*/
/**
* 📻
* @param {string} location stream url, e.g. https://stream.radio.com/live.mp3
* @param {string} ip ip of proxy (on localhost), e.g. 192.168.0.182
* @returns e.g. http://192.168.0.182/stream.radio.com/live.mp3
*/
function getProxiedLocation(location, ip) {
if (location.indexOf(https) == 0) {
return 'http://' + ip + ':' + PROXY_PORT
+ '/' + location.slice(https.length);
} else {
return location;
}
}
/**
* 📻 only using first ipv4 address as gerbera will only bind to one interface anyway
*/
var ipAddress = IP_ADDRESSES.split(' ').filter(function (val) {
return val.indexOf('.') >= 0;
})[0];
var playlistOrder = 1;
function addRadioPlaylistItem(location, title, playlistChain, order, proxied) { // 📻
if (location.match(/^.*:\/\//)) {
var exturl = new Object();
exturl.mimetype = 'audio/mpeg';
exturl.objectType = OBJECT_TYPE_ITEM_EXTERNAL_URL;
exturl.location = proxied ? getProxiedLocation(location, ipAddress) : location; // 📻
exturl.title = (title ? title : location);
exturl.protocol = 'http-get';
exturl.upnpclass = UPNP_CLASS_ITEM_MUSIC_TRACK;
exturl.description = "Song from " + playlist.title;
exturl.playlistOrder = (order ? order : playlistOrder++);
exturl.meta = exturl.meta || new Array(); // 📻
// 📻 setting artist to item name because live streams overwrite title
// (so we can have the song title in the title and the station name in
// the artist field)
exturl.meta[M_ARTIST] = exturl.meta[M_ARTIST] || exturl.title; // 📻
print('add object ' + exturl.title + ' to chain ' + playlistChain)
addCdsObject(exturl, playlistChain, UPNP_CLASS_PLAYLIST_CONTAINER);
}
else {
if (location.substr(0, 1) != '/')
location = playlistLocation + location;
var item = new Object();
item.location = location;
if (title)
item.title = title;
else {
var locationParts = location.split('/');
item.title = locationParts[locationParts.length - 1];
if (!item.title)
item.title = location;
}
item.objectType = OBJECT_TYPE_ITEM;
item.playlistOrder = (order ? order : playlistOrder++);
addCdsObject(item, playlistChain, UPNP_CLASS_PLAYLIST_CONTAINER);
}
}
print("Processing playlist: " + playlist.location);
var playlistLocation = playlist.location.substring(0, playlist.location.lastIndexOf('/') + 1);
// the function getPlaylistType is defined in common.js
var type = getPlaylistType(playlist.mimetype);
// the function createContainerChain is defined in common.js
var playlist_title = playlist.title
var dot_index = playlist_title.lastIndexOf('.');
if (dot_index > 1)
playlist_title = playlist_title.substring(0, dot_index);
var playlistChainProxied = createContainerChain(new Array(playlist_title)); // 📻
var playlistChainDirect = createContainerChain(new Array('direct', playlist_title)); // 📻
var playlistDirChain = null; // 📻 not using the directory structure chain
// 📻 var last_path = getLastPath(playlist.location);
// if (last_path)
// playlistDirChain = createContainerChain(new Array('Playlists', 'Directories', last_path, playlist_title));
if (type == '') {
print("Unknown playlist mimetype: '" + playlist.mimetype + "' of playlist '" + playlist.location + "'");
}
else if (type == 'm3u') {
var title = null;
var line = readln();
do {
if (line.match(/^#EXTINF:(-?\d+),(\S.+)$/i)) {
// duration = RegExp.$1; // currently unused
title = line.split(',')[1]
}
else if (!line.match(/^(#|\s*$)/)) {
addRadioPlaylistItem(line, title, playlistChainProxied, null, true); // 📻
addRadioPlaylistItem(line, title, playlistChainDirect, null, false); // 📻
if (playlistDirChain)
addRadioPlaylistItem(line, title, playlistDirChain);
title = null;
}
line = readln();
}
while (line);
}
else if (type == 'pls') {
/* 📻 only using m3u for radio stations at the moment
var title = null;
var file = null;
var lastId = -1;
var line = readln();
do {
if (line.match(/^\[playlist\]$/i)) {
// It seems to be a correct playlist, but we will try to parse it
// anyway even if this header is missing, so do nothing.
}
else if (line.match(/^NumberOfEntries=(\d+)$/i)) {
// var numEntries = RegExp.$1;
}
else if (line.match(/^Version=(\d+)$/i)) {
// var plsVersion = RegExp.$1;
}
else if (line.match(/^File\s*(\d+)\s*=\s*(\S.+)$/i)) {
var thisFile = RegExp.$2;
var id = parseInt(RegExp.$1, 10);
if (lastId == -1)
lastId = id;
if (lastId != id) {
if (file) {
addRadioPlaylistItem(file, title, playlistChain, lastId);
if (playlistDirChain)
addRadioPlaylistItem(file, title, playlistDirChain, lastId);
}
title = null;
lastId = id;
}
file = thisFile
}
else if (line.match(/^Title\s*(\d+)\s*=\s*(\S.+)$/i)) {
var thisTitle = RegExp.$2;
var id = parseInt(RegExp.$1, 10);
if (lastId == -1)
lastId = id;
if (lastId != id) {
if (file) {
addRadioPlaylistItem(file, title, playlistChain, lastId);
if (playlistDirChain)
addRadioPlaylistItem(file, title, playlistDirChain, lastId);
}
file = null;
lastId = id;
}
title = thisTitle;
}
else if (line.match(/^Length\s*(\d+)\s*=\s*(\S.+)$/i)) {
// currently unused
}
line = readln();
}
while (line);
if (file) {
addRadioPlaylistItem(file, title, playlistChain, lastId);
if (playlistDirChain)
addRadioPlaylistItem(file, title, playlistDirChain, lastId);
}
*/
}
#!/bin/bash
# only needed if using dynamic ip
# to be run before gerbera starts; hard codes the current ip addresses into the radio-playlists.js script
sed "s/IP_ADDRESSES =.*$/IP_ADDRESSES = '`hostname -I`';/" -i /etc/gerbera/radio-playlists.js
echo updated ip addresses to `hostname -I`
# proxies https://stream.radio.com:8001/live.mp3
# via http://0.0.0.0:8081/stream.radio.com:8001/live.mp3
# ...so your (my) old web radio can play the stream via http :)
map $upstream_status $new_location {
301 http://$host:49180/$upstream_http_location;
default '';
}
server {
listen 49180;
resolver 1.1.1.1;
merge_slashes off;
location ~ ^/https://(.*)$ {
return 302 /$1;
}
location ~ ^/(.*)$ {
proxy_pass https://$1;
proxy_set_header "User-Agent" "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0";
proxy_hide_header Location;
add_header Location $new_location;
}
}
# only needed if using dynamic ip address!
# for fixed ip addresses, you can keep using the original systemd service
# first stop & disable original gerbera systemd service:
# $ sudo systemctl stop gerbera.service
# $ sudo systemctl disable gerbera.service
# then enable & start this custom gerbera systemd service:
# $ sudo systemctl enable gerbera-custom.service
# $ sudo systemctl start gerbera-custom.service
[Unit]
Description=Gerbera Media Server
Documentation=man:gerbera(1) https://gerbera.io/
After=mysql.target network.target
[Service]
Type=simple
User=gerbera
Group=gerbera
ExecStartPre=/etc/gerbera/update-gerbera-ip.sh
ExecStart=/usr/bin/gerbera -c /etc/gerbera/config.xml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment