Initial commit

main
Jerry Aldrich 6 years ago
commit c0ebbfb925
  1. 33
      Dockerfile
  2. 43
      config.json
  3. 70
      entrypoint.sh
  4. 65
      icecast.xml
  5. 282
      player.html

@ -0,0 +1,33 @@
FROM alpine:latest
LABEL maintainer "jerryaldrichiii@gmail.com"
RUN apk add --update \
icecast \
mailcap \
jq \
&& rm -rf /var/cache/apk/*
RUN mkdir -p /etc/icecast
RUN mv /etc/icecast.xml /etc/icecast/icecast-original.xml
COPY ./icecast.xml /etc/icecast/icecast.xml
RUN chown -R icecast:icecast /etc/icecast
RUN chown -R icecast:icecast /var/log/icecast
RUN chown -R icecast:icecast /usr/share/icecast
EXPOSE 8000
RUN mkdir /bootstrap
COPY ./config.json /bootstrap
RUN chown -R icecast:icecast /bootstrap
COPY ./entrypoint.sh /etc/icecast/entrypoint.sh
RUN chmod +x /etc/icecast/entrypoint.sh
COPY player.html /usr/share/icecast/web/player.html
USER icecast:icecast
ENTRYPOINT ["/etc/icecast/entrypoint.sh"]

@ -0,0 +1,43 @@
{
"title": "radio.jerryaldrichiii.com",
"hostname": "localhost",
"port": "8000",
"https_streams": false,
"streams": [
{
"name": "DEF CON Radio",
"server":"ice2.somafm.com",
"port": 80,
"mount": "/defcon-256-mp3",
"local_mount":"/defcon"
},
{
"name": "SF 10-33",
"server":"ice2.somafm.com",
"port": 80,
"mount": "/sf1033-128-mp3",
"local_mount":"/sf1033"
},
{
"name": "Mission Control",
"server":"ice2.somafm.com",
"port": 80,
"mount": "/missioncontrol-128-mp3",
"local_mount":"/missioncontrol"
},
{
"name": "Cyberia",
"server":"lainon.life",
"port": 8000,
"mount": "/cyberia.mp3",
"local_mount": "/cyberia"
},
{
"name": "Synthwave Radio",
"server":"stream.syntheticfm.com",
"port": 8040,
"mount": "/live",
"local_mount": "/synthwave"
}
]
}

@ -0,0 +1,70 @@
#!/bin/sh
set -e
set_val() {
# Combine args in case there are spaces
# https://linux.die.net/man/1/ash
val="${@#$1 }" # Remove smallest prefix pattern (space after $1 intentional)
sed -i "s/<$1>[^<]*<\/$1>/<$1>$val<\/$1>/g" /etc/icecast/icecast.xml
}
# The below will replace values in the config with the ENV variable specified.
# If one is not specified, what follows ':-' will be used.
# NOTE: The order below matches the order seen in the config.
set_val location ${ICECAST_LOCATION:-San Francisco}
set_val admin ${ICECAST_ADMIN:-Someone Failed to Change Me}
set_val clients ${ICECAST_LIMITS_CLIENTS:-100}
set_val sources ${ICECAST_LIMITS_SOURCES:-2}
set_val queue-size ${ICECAST_LIMITS_QUEUE_SIZE:-524288}
set_val client-timeout ${ICECAST_LIMITS_CLIENT_TIMEOUT:-30}
set_val header-timeout ${ICECAST_LIMITS_HEADER_TIMEOUT:-14}
set_val source-timeout ${ICECAST_LIMITS_SOURCE_TIMEOUT:-10}
set_val burst-on-connect ${ICECAST_LIMITS_BURST_ON_CONNECT:-1}
set_val burst-size ${ICECAST_LIMITS_BURST_SIZE:-65535}
set_val fileserve ${ICECAST_FILESERVE:-1}
set_val source-password ${AUTHENTICATION_SOURCE_PASSWORD:-changeorbehacked}
set_val relay-password ${AUTHENTICATION_RELAY_PASSWORD:-changeorbehacked}
set_val admin-user ${AUTHENTICATION_ADMIN_USER:-changeorbehacked}
set_val admin-password ${AUTHENTICATION_ADMIN_PASSWORD:-changeorbehacked}
set_val relays-on-demand ${RELAYS_ON_DEMAND:-1}
set_val loglevel ${LOG_LEVEL:-3}
set_val logsize ${LOG_SIZE:-10000}
set_val logarchive ${LOG_ARCHIVE:-0}
# Build relay XML sections
CONFIG="$(cat /bootstrap/config.json)"
for stream in $(echo "$CONFIG" | jq -r '.streams[] | @base64'); do
_parse() {
echo ${stream} | base64 -d | jq -r ${1}
}
RELAY_XML=$(cat <<-EOX
$RELAY_XML
<relay>
<server>$(_parse .server)</server>
<port>$(_parse .port)</port>
<mount>$(_parse .mount)</mount>
<local-mount>$(_parse .local_mount)</local-mount>
<relay-shoutcast-metadata>1</relay-shoutcast-metadata>
</relay>
EOX
)
done
# Set hostname (needs to match for listen urls and such)
host=$(echo "$CONFIG" | jq -r '.hostname')
set_val hostname ${ICECAST_HOSTNAME:-$host}
# Using | as the delimiter because of URLs containing /
sed -i "s|ICECAST_RELAYS|$(echo "$RELAY_XML" | tr "\n" "#")|g;s/#/\n/g" /etc/icecast/icecast.xml
# Publish config.json for consumers (e.g. web player)
cp /bootstrap/config.json /usr/share/icecast/web/
icecast -c "/etc/icecast/icecast.xml"

@ -0,0 +1,65 @@
<icecast>
<hostname>SEE_ENTRYPOINT</hostname>
<location>SEE_ENTRYPOINT</location>
<admin>SEE_ENTRYPOINT</admin>
<!--
The mime-types config is moved to <paths> in 2.5.0. As of the time of this
writing 2.5.0 is only in beta. You'll likely need to move once it is
released for production use.
-->
<mime-types>/etc/mime.types</mime-types>
<limits>
<clients>SEE_ENTRYPOINT</clients>
<sources>SEE_ENTRYPOINT</sources>
<queue-size>SEE_ENTRYPOINT</queue-size>
<client-timeout>SEE_ENTRYPOINT</client-timeout>
<header-timeout>SEE_ENTRYPOINT</header-timeout>
<source-timeout>SEE_ENTRYPOINT</source-timeout>
<burst-on-connect>SEE_ENTRYPOINT</burst-on-connect>
<burst-size>SEE_ENTRYPOINT</burst-size>
</limits>
<authentication>
<source-password>SEE_ENTRYPOINT</source-password>
<relay-password>SEE_ENTRYPOINT</relay-password>
<admin-user>SEE_ENTRYPOINT</admin-user>
<admin-password>SEE_ENTRYPOINT</admin-password>
</authentication>
<listen-socket>
<port>8000</port>
</listen-socket>
<http-headers>
<header name="Access-Control-Allow-Origin" value="*" />
</http-headers>
<relays-on-demand>SEE_ENTRYPOINT</relays-on-demand>
<fileserve>SEE_ENTRYPOINT</fileserve>
<paths>
<logdir>/var/log/icecast</logdir>
<webroot>/usr/share/icecast/web</webroot>
<adminroot>/usr/share/icecast/admin</adminroot>
<alias source="/" destination="/player.html"/>
</paths>
<logging>
<!-- Changed to '-' so logging is sent to STDERR, before was access.log and error.log-->
<accesslog>-</accesslog>
<errorlog>-</errorlog>
<loglevel>SEE_ENTRYPOINT</loglevel>
<logsize>SEE_ENTRYPOINT</logsize>
<logarchive>SEE_ENTRYPOINT</logarchive>
</logging>
<security>
<chroot>0</chroot>
</security>
<!-- TODO: Document mounting stream_data.json -->
ICECAST_RELAYS
</icecast>

@ -0,0 +1,282 @@
<!-- If you're reading this...this was a weekend project...please don't judge -->
<!DOCTYPE HTML>
<html>
<head>
<title>radio.jerryaldrichiii.com</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="Description" content="radio.jerryaldrichiii.com"><Paste>
<style>
* {
background: black;
background-color: black;
}
a, a:hover, a:active, a:visited {
color: #00FF00;
}
body {
background: black;
background-color: black;
color: #00FF00;
text-align: center;
overflow-y: scroll;
display: inline;
font: calc(0.40em + 1vmin) monospace;
}
#stream-info {
width: 60ch;
margin: auto;
margin-bottom: 1ch;
}
#controls {
display: inline-flex;
justify-content: space-between;
width: 60ch;
}
button {
background-color: black;
color: #00FF00;
border: none;
cursor: pointer;
outline: none;
padding: 0px 0px 0px 0px;
font-size: inherit;
}
#volume-container > * {
vertical-align: middle;
}
#volume {
-webkit-appearance: none;
-webkit-transition: .2s;
background: black;
outline: auto;
outline-color: #00FF00;
opacity: 0.7;
transition: opacity .2s;
vertical-align: middle;
}
#volume:hover {
opacity: 1;
}
#volume::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: .5em;
width: .5em;
background: #00FF00;
cursor: pointer;
}
#volume::-moz-range-thumb, .volume::-webkit-slider-thumb {
background: black;
width: .5em;
cursor: pointer;
}
select {
background: black;
border-color: #00FF00;
color: #00FF00;
cursor: pointer;
font-size: inherit;
}
select:focus{
outline:none;
}
</style>
</head>
<audio id="player" src=""></audio>
<body>
<div>
<pre id="stream-info"></pre>
<div id="controls">
<select id="streamSelection">
<option value="" disabled selected style="display:none;">Select a Stream</option>
</select>
<button id="playButton">Play</button>
<div id="volume-container">
<label for="volume" style="vertical-align: middle">Volume: </label>
<input id="volume" type="range" min="0" max="100" value="100">
</div>
<button id="muteButton">Mute</button>
</div>
</div>
<body>
<script>
function loadStreamChoices(data) {
var selectItem = document.getElementById('streamSelection')
streams = data["streams"]
for(let i in streams) {
var opt = document.createElement('option')
if(data.https_streams == true) {
opt.value = "https://"
} else {
opt.value = "http://"
}
opt.value = opt.value + data.hostname + ":" + data.port + streams[i]["local_mount"]
opt.innerHTML = streams[i]["name"]
selectItem.appendChild(opt)
}
}
var configXMLHTTP = new XMLHttpRequest();
configXMLHTTP.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var data = JSON.parse(this.responseText);
loadStreamChoices(data)
}
};
configXMLHTTP.open("GET", "/config.json", true);
configXMLHTTP.send();
</script>
<script>
var UPDATE_INTERVAL = window.setInterval(renderStreamInfo, 10000)
function titleLine(data, width) {
title = data["icestats"]["host"]
width = width - 2 // Subtracting 2 here to account for "|"
spacing = Math.floor((width - title.length)/2)
line = "|" + " ".repeat(spacing) + title + " ".repeat(spacing)
if(spacing + title.length + spacing != width) {
line = line + " |" // Handle title not being even
} else {
line = line + "|"
}
return line
}
function getSourceInfo(data) {
sources = data["icestats"]["source"]
for(let i in sources) {
selectedStream = document.getElementById('streamSelection').value.replace(/^.*[\\\/]/, '')
icecastStream = sources[i]["listenurl"].replace(/^.*[\\\/]/, '')
if(selectedStream == icecastStream) {
sources[i]["streamurl"] = document.getElementById('streamSelection').value
return sources[i]
}
}
}
function fetchText(statusData, key) {
text = ""
streamInfo = getSourceInfo(statusData)
if(streamInfo && streamInfo[key]) {
text = String(streamInfo[key])
}
return text
}
// This is so bad...but special chars mess up '.length'
function badThing(text) {
text = text.replace(/[^\x00-\x7F]/g, '')
return text
}
function buildLine(width, text, prefix) {
text = badThing(text)
prefix = "|" + prefix
spacing = (width - prefix.length - text.length - 2) // Subtract 2 for " |"
if(spacing < 0) {
spacing = 0
}
line = prefix + text + " ".repeat(spacing) + " |"
// Trim line if too long
if(line.length > width) {
end = (width - line.length)
start = width - 2 // Subtract 2 for " |"
line = line.substring(end, start) + " |"
}
return line
}
function buildDownloadLine(width, url) {
line = "| Download: "
line = line + "<a href='" + url + ".m3u" + "'>M3U</a>"
line = line + "/"
line = line + "<a href='" + url + ".xspf" + "'>XSPF</a>"
line = line + " ".repeat(width - 24) + " |"
return line
}
function renderStreamInfo() {
var statusXMLHTTP = new XMLHttpRequest();
statusXMLHTTP.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var streamData = JSON.parse(this.responseText);
width = 60
songNameLine = buildLine(width, fetchText(streamData, "title"), " Song Title: ")
listenersLine = buildLine(width, fetchText(streamData, "listeners"), " Listeners: ")
downloadLine = buildDownloadLine(width, fetchText(streamData, "streamurl"))
var streamInfo = []
streamInfo.push("/" + "-".repeat(width - 2) + "\\")
streamInfo.push(titleLine(streamData, width))
if(songNameLine.match(/Song Title: \S/)) {
streamInfo.push("|" + " ".repeat(width - 2) + "|")
streamInfo.push(songNameLine)
streamInfo.push(listenersLine)
streamInfo.push(downloadLine)
streamInfo.push("|" + " ".repeat(width - 2) + "|")
}
streamInfo.push("\\" + "-".repeat(width - 2) + "/")
document.getElementById("stream-info").innerHTML = streamInfo.join("\n")
}
};
statusXMLHTTP.open("GET", "/status-json.xsl", true);
statusXMLHTTP.send();
}
function setUpdateInterval(interval) {
window.clearInterval(UPDATE_INTERVAL)
UPDATE_INTERVAL = window.setInterval(renderStreamInfo, interval)
renderStreamInfo()
}
window.onload = function(){
renderStreamInfo()
}
streamSelection.onchange = function() {
player.src = this.value
document.title = this.options[this.selectedIndex].text
player.load()
player.play()
}
player.onwaiting = function() {
setUpdateInterval(100)
playButton.innerHTML = "Loading..."
return;
}
player.onplaying = function() {
setUpdateInterval(10000)
playButton.innerHTML = "Pause"
return;
}
player.onpause = function() {
setUpdateInterval(60000)
playButton.innerHTML = "Play"
return;
}
playButton.onclick = function() {
if(player.paused) {
player.play();
} else {
player.pause();
}
}
muteButton.onclick = function() {
if(player.muted) {
player.muted = false;
this.innerHTML = "Mute"
} else {
player.muted = true;
this.innerHTML = "Muted"
}
}
volume.oninput = function() {
player.volume = this.value/100;
}
</script>
</html>
Loading…
Cancel
Save