v0.4.0
This commit is contained in:
parent
32a8905899
commit
caf59c6c70
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "icecast-metadata-js"]
|
||||
path = icecast-metadata-js
|
||||
url = https://github.com/eshaz/icecast-metadata-js.git
|
188
embed.html
Normal file
188
embed.html
Normal file
|
@ -0,0 +1,188 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>wormTuner Embed</title>
|
||||
<link rel="icon" href="/favicon.png" type="image/png" sizes="128x128">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<script src="icecast-metadata-player-1.13.1.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var drawRequest = 0;
|
||||
const settings = {
|
||||
site: "https://example.com/",
|
||||
icestats: "https://example.com/status-json.xsl"
|
||||
};
|
||||
const player = new IcecastMetadataPlayer(settings.site+'stream/'+window.location.search.substr(1)+"?"+Date.now(), {
|
||||
onMetadata: (metadata) => {updateMetadata(metadata.StreamTitle);},
|
||||
metadataTypes: ["icy"]
|
||||
});;
|
||||
var station = {};
|
||||
var metacanvas = null;
|
||||
|
||||
function fixURL(url) {
|
||||
return url.replace('http://example.com:8000/', '/stream/');
|
||||
}
|
||||
|
||||
function play() {
|
||||
player.play();
|
||||
document.querySelector('.play').classList.add('active');
|
||||
}
|
||||
|
||||
function stop() {
|
||||
player.stop();
|
||||
document.querySelector('.play').classList.remove('active');
|
||||
}
|
||||
|
||||
function updateMetadata(track) {
|
||||
var line_index = [0,0,0];
|
||||
var startTimes = [new Date(),new Date(),new Date()];
|
||||
var max_chars = 30;
|
||||
function marqueeify(line, x, y, e) {
|
||||
if (line.length >= max_chars) {
|
||||
var txt = line.concat(' ').concat(line);
|
||||
if (line_index[e] - 4 > line.length) {line_index[e] = 0;}
|
||||
ctx.fillText(txt.substr(line_index[e],max_chars), x,y);
|
||||
var now = new Date();
|
||||
if (now >= new Date(startTimes[e].getTime() + 200)) {
|
||||
startTimes[e] = now;
|
||||
line_index[e]++;
|
||||
}
|
||||
} else ctx.fillText(line,x,y);
|
||||
}
|
||||
|
||||
station.title = track;
|
||||
if (metacanvas) {
|
||||
var ctx = metacanvas.getContext("2d",{antialias: false,alpha: false});
|
||||
|
||||
var WIDTH = metacanvas.width;
|
||||
var HEIGHT = metacanvas.height;
|
||||
|
||||
window.cancelAnimationFrame(drawRequest);
|
||||
function drawMeta() {
|
||||
drawRequest = window.requestAnimationFrame(drawMeta);
|
||||
|
||||
ctx.clearRect(0,0,WIDTH,HEIGHT);
|
||||
if (station != null) {
|
||||
ctx.font = "16px hack,monospace";
|
||||
ctx.fillStyle = "#FFF";
|
||||
ctx.textAlign = "justify";
|
||||
|
||||
ctx.font = "bold 16px hack,monospace";
|
||||
marqueeify(station.server_name,8,20,0);
|
||||
ctx.font = "16px hack,monospace";
|
||||
marqueeify(track,8,38,1);
|
||||
|
||||
/*
|
||||
let meta = [];
|
||||
meta.push(station.server_type.substr(station.server_type.indexOf('/')+1).toUpperCase());
|
||||
if (station.bitrate) meta.push(station.bitrate+"kbps");
|
||||
else if (station.quality) meta.push(station.quality);
|
||||
else meta.push('?kbps');
|
||||
meta.push(station.channels+'ch');
|
||||
meta.push((station.samplerate / 1000)+'khz');
|
||||
meta.push(station.genre);
|
||||
marqueeify(meta.join(' - '),8,56,2);
|
||||
*/
|
||||
marqueeify(station.genre,8,56,2);
|
||||
}
|
||||
}
|
||||
drawMeta();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
metacanvas = document.querySelector('canvas.metadata');
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = function() {
|
||||
if (xhr.response) {
|
||||
var stations = xhr.response.icestats.source;
|
||||
for(let s = 0; s < stations.length; s++) {
|
||||
if (fixURL(stations[s].listenurl) == '/stream/'+window.location.search.substr(1)) {
|
||||
station = stations[s];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open('GET', settings.icestats, true);
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
function volChange(e) {
|
||||
player.audioElement.volume=e.value/100;
|
||||
e.parentNode.title = e.value+'%';
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f3dca9;
|
||||
border: 2px outset #f2e5c8;
|
||||
margin: 0;
|
||||
width: 308px;
|
||||
height: 118px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
*:link, *:visited {color:black;}
|
||||
|
||||
button, slider {
|
||||
border: 2px outset #DFDFDF;
|
||||
background: #DFDFDF;
|
||||
}
|
||||
slider input {
|
||||
background: lightgray;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 4px;
|
||||
margin: 6 2;
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
slider input::-moz-range-thumb,
|
||||
slider input::-webkit-slider-thumb {
|
||||
background: gray;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:active,
|
||||
button.active {
|
||||
border: 2px inset gray;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
canvas {
|
||||
color: #F0F0F0;
|
||||
background: black;
|
||||
border: 2px inset #f2e5c8;
|
||||
}
|
||||
|
||||
center { margin: 4px; }
|
||||
</style>
|
||||
<meta property="og:site_name" content="wormTuner Embed">
|
||||
<meta property="og:image" content="/favicon.png">
|
||||
<meta property="og:title" content="wormTuner">
|
||||
<meta property="og:description" content="Worm-crafted radio waves, straight to your computer!">
|
||||
</head>
|
||||
<body>
|
||||
<center><a href='https://example.com/' target='_blank'>wormTuner</a></center>
|
||||
<canvas width=304 height=66 class=metadata></canvas>
|
||||
<controls>
|
||||
<button title='Play' class='play' onclick='play()'><img src='play.png'></button>
|
||||
<button title='Stop' onclick='stop()'><img src='stop.png'></button>
|
||||
<div style='flex-grow:1;'></div>
|
||||
<slider title='100%'><img src='vol.png'><input type=range min=0 max=100 value=100 step=5 oninput="volChange(this)"></slider>
|
||||
</controls>
|
||||
</body>
|
||||
</html>
|
BIN
favicon.png
Normal file
BIN
favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
1
icecast-metadata-js
Submodule
1
icecast-metadata-js
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 818cf3fe5cafeb46c4ec91dc15ea9105c5838bc8
|
113
index.html
113
index.html
|
@ -1,40 +1,75 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>wormTuner</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<link rel="icon" href="/favicon.png" type="image/png" sizes="128x128">
|
||||
<script type="text/javascript" src="audiovis.js"></script>
|
||||
<script type="text/javascript" src="radio.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<meta property="og:site_name" content="wormTuner">
|
||||
<meta property="og:title" content="wormTuner">
|
||||
<meta property="og:description" content="Worm-crafted radio waves, straight to your computer!">
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<img src="banner.png" class='banner' width=384>
|
||||
<div class='player'>
|
||||
<div class='textainer'>
|
||||
<div class='station'></div>
|
||||
<div class='track'></div>
|
||||
<div class='genre'></div>
|
||||
</div>
|
||||
<div class="vismode hidden">0/4</div>
|
||||
<canvas class='vis' height=96 width=370><a>Test</a></canvas>
|
||||
<div class='vol hidden'>VOL [====================] 100%</div>
|
||||
<div class="about">
|
||||
<a class='name' href="https://github.com/Worlio/wormTuner"></a>
|
||||
<div style="flex-grow:1;"></div>
|
||||
<a class='vers'></a>
|
||||
</div>
|
||||
</div>
|
||||
</center>
|
||||
<center class='section list'>
|
||||
<div><p>Click on a station to tune in! If you are having trouble, try the <a href='alt.html'>Alternate player</a>.</p></div>
|
||||
<div class='broadcasts'>
|
||||
|
||||
</div>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<title>wormTuner</title>
|
||||
<link rel="icon" href="/favicon.png" type="image/png" sizes="128x128">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<script src="icecast-metadata-js/src/icecast-metadata-player/build/icecast-metadata-player-1.13.2.min.js"></script>
|
||||
<script type="text/javascript" src="radio.js"></script>
|
||||
<link rel="stylesheet" href="radio.css">
|
||||
<meta property="og:site_name" content="wormTuner">
|
||||
<meta property="og:image" content="/favicon.png">
|
||||
<meta property="og:title" content="wormTuner">
|
||||
<meta property="og:description" content="Worm-crafted radio waves, straight to your computer!">
|
||||
</head>
|
||||
<body>
|
||||
<div class='banner'>
|
||||
<div class='player-container'>
|
||||
<player>
|
||||
<canvas width=304 height=44 class=metadata><metadata>
|
||||
<station></station>
|
||||
<song></song>
|
||||
</metadata></canvas>
|
||||
<controls>
|
||||
<button title='Play' class='play' onclick='play()'><img src='play.png'></button>
|
||||
<button title='Stop' onclick='stop()'><img src='stop.png'></button>
|
||||
<button style='flex-grow:1;pointer-events:none;'></button>
|
||||
<slider title='100%'><img src='vol.png'><input type=range min=0 max=100 value=100 step=5 oninput="volChange(this)"></slider>
|
||||
<button class='fav-button' title='Favorite' onclick='favoriteCurrentTrack()'><img src='heart.png'></button>
|
||||
</controls>
|
||||
</player>
|
||||
</div>
|
||||
<img src="banner.png" style="display: block;" width="320" height="132">
|
||||
</div>
|
||||
<div class='container'>
|
||||
<panel class='vis-container'>
|
||||
<visualizer>
|
||||
<canvas width=304 height=96 class=vis></canvas>
|
||||
<controls>
|
||||
<button style='flex-grow:1;pointer-events:none;'>wormTuner</button>
|
||||
<fakebutton title='Change Visualizer'>
|
||||
<img src='vis.png'>
|
||||
<select onchange='setVisMode(this.value)' name='visModes'>
|
||||
</select>
|
||||
</fakebutton>
|
||||
</controls>
|
||||
</visualizer>
|
||||
<div class='button-row'>
|
||||
<input type=search placeholder="Search stations..." style='flex-grow:1;' oninput="searchStations(this.value)">
|
||||
<button onclick="updateXHR()">Refresh</button>
|
||||
</div>
|
||||
<stations></stations>
|
||||
</panel>
|
||||
<panel>
|
||||
<div class='button-row'>
|
||||
<button style='flex-grow:1;pointer-events:none;'>History</button>
|
||||
</div>
|
||||
<history></history>
|
||||
</panel>
|
||||
<panel>
|
||||
<div class='button-row'>
|
||||
<button style='flex-grow:1;pointer-events:none;'>Favorites</button>
|
||||
</div>
|
||||
<div class='button-row'>
|
||||
<input type=search placeholder="Search favorites..." style='flex-grow:1;' oninput="searchFavorites(this.value)">
|
||||
<button onclick="exportFavorites()">Export JSON</button>
|
||||
</div>
|
||||
<favorites></favorites>
|
||||
</panel>
|
||||
</div>
|
||||
<div class='footer'>
|
||||
<a target="_blank" href='https://github.com/Worlio/wormTuner'>wormTuner v0.4.0</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
304
radio.css
Normal file
304
radio.css
Normal file
|
@ -0,0 +1,304 @@
|
|||
:root {
|
||||
--background: #b3a99e;
|
||||
|
||||
--primary: #0079b5;
|
||||
--primary-border: #009ae6;
|
||||
|
||||
--secondary: #f3dca9;
|
||||
--secondary-border: #f2e5c8;
|
||||
|
||||
--button: #DFDFDF;
|
||||
--button-active: gray;
|
||||
|
||||
--monitor: #0F0F0F;
|
||||
--monitor-text: #F0F0F0;
|
||||
--monitor-border: gray;
|
||||
|
||||
--link-color: unset;
|
||||
}
|
||||
|
||||
*:link, *:visited {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
background-repeat: repeat;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container, .player-container {
|
||||
background: var(--primary);
|
||||
border: 2px outset var(--primary-border);
|
||||
padding: 4px;
|
||||
}
|
||||
.container {
|
||||
min-width: 320px;
|
||||
min-height: 280px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin: auto;
|
||||
max-width: 976px;
|
||||
}
|
||||
.player-container {
|
||||
position: relative;
|
||||
border-bottom: none;
|
||||
width: 320px;
|
||||
height: 78px;
|
||||
margin-bottom: -2px;
|
||||
z-index: 1;
|
||||
}
|
||||
.footer {
|
||||
min-width: 320px;
|
||||
max-width: 976px;
|
||||
text-align: right;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
player, visualizer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--secondary);
|
||||
border: 2px outset var(--secondary-border);
|
||||
flex-shrink: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
player {
|
||||
width: 308px; height: 68px;
|
||||
}
|
||||
|
||||
visualizer {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vis-container {
|
||||
width: 320px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
button, fakebutton, slider {
|
||||
border: 2px outset var(--button);
|
||||
background: var(--button);
|
||||
}
|
||||
|
||||
slider input {
|
||||
background: lightgray;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 4px;
|
||||
margin: 6 2;
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
slider input::-moz-range-thumb,
|
||||
slider input::-webkit-slider-thumb {
|
||||
background: gray;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
button:active,
|
||||
button.active,
|
||||
fakebutton:active,
|
||||
fakebutton.active {
|
||||
border: 2px inset var(--button-active);
|
||||
background: var(--button-active);
|
||||
}
|
||||
|
||||
fakebutton { display: flex; }
|
||||
fakebutton img { margin: 2px; }
|
||||
|
||||
controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
controls input { flex-grow: 1; }
|
||||
|
||||
tablist {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
metadata, canvas {
|
||||
color: var(--monitor-text);
|
||||
background: black;
|
||||
border: 2px inset var(--secondary-border);
|
||||
}
|
||||
|
||||
metadata {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
metadata station { font-weight: bold; }
|
||||
|
||||
panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
stations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
border: 2px inset var(--primary-border);
|
||||
margin: 2px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
favorites, history, stats {
|
||||
background: var(--monitor);
|
||||
color: var(--monitor-text);
|
||||
border: 2px inset var(--monitor-border);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
overflow-y: scroll;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
tabbox {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
tabbox.active { display: flex; }
|
||||
|
||||
station {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 4px;
|
||||
margin: 2px;
|
||||
border: 2px inset var(--button-active);
|
||||
background: var(--button-active);
|
||||
}
|
||||
|
||||
station.active {
|
||||
background: var(--button);
|
||||
border: 2px outset var(--button);
|
||||
color: black;
|
||||
}
|
||||
|
||||
station station-name { font-weight: bold; }
|
||||
station track-meta {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
history track-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
history track-container track-station { font-weight: bold; }
|
||||
|
||||
history track-container:nth-of-type(1) {
|
||||
background-color: var(--monitor-text);
|
||||
color: var(--monitor);
|
||||
}
|
||||
|
||||
canvas.vis, canvas.metadata { border-bottom: none; }
|
||||
|
||||
.button-row { display: flex; }
|
||||
|
||||
.fav-button img {
|
||||
filter: contrast(0%);
|
||||
}
|
||||
|
||||
.fav-button.fav img {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
favorites .station-header,
|
||||
history .station-header {
|
||||
text-align: center;
|
||||
margin: 4px 0;
|
||||
text-decoration: underline;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
favorites .track,
|
||||
history .track {
|
||||
display: flex;
|
||||
border-bottom: 1px dashed var(--monitor-text);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
favorites .track button,
|
||||
history .track button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
favorites .track a,
|
||||
history .track a {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media only screen and (orientation: portrait) {
|
||||
.container, .footer {
|
||||
flex-direction: column;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.banner { flex-direction: column-reverse; }
|
||||
.banner img, .player-container {
|
||||
margin: auto;
|
||||
}
|
||||
.banner > img { flex-shrink: 0; }
|
||||
.player-container { margin-bottom: -2px; }
|
||||
|
||||
panel {
|
||||
max-height: 480px;
|
||||
}
|
||||
|
||||
favorites, history {
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (orientation: landscape) {
|
||||
.container, .footer {
|
||||
width: calc(100% - 64px);
|
||||
height: calc(100% - 212px);
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin: auto;
|
||||
min-width: 320px;
|
||||
width: calc(100% - 52px);
|
||||
max-width: 988px;
|
||||
}
|
||||
.banner > img { margin-top: 2px; }
|
||||
|
||||
.player-container {
|
||||
margin-top: 48px;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user