Prepare for themes
This commit is contained in:
parent
7306c3ab15
commit
50d2fe1349
24
env.dist
24
env.dist
|
@ -1,3 +1,25 @@
|
||||||
|
# This is the environment for Groove on Demand. You can (re)populate your own environment
|
||||||
|
# by running the following command from the command-line:
|
||||||
|
#
|
||||||
|
# groove setup
|
||||||
|
#
|
||||||
|
|
||||||
|
DATABASE_PATH=groove_on_demand.db
|
||||||
|
|
||||||
|
# Admin user credentials
|
||||||
|
USERNAME=evilchili
|
||||||
|
PASSWORD=fnord
|
||||||
|
|
||||||
|
# Web interface configuration
|
||||||
HOST=127.0.0.1
|
HOST=127.0.0.1
|
||||||
PORT==2323
|
PORT=2323
|
||||||
|
THEMES_PATH=themes
|
||||||
|
DEFAULT_THEME=blue_train
|
||||||
|
SECRET_KEY=fnord
|
||||||
|
|
||||||
|
# Media scanner configuration
|
||||||
|
MEDIA_ROOT=/mnt/grunt/music/FLAC
|
||||||
|
MEDIA_GLOB=*.mp3,*.flac,*.m4a
|
||||||
|
|
||||||
|
# Set this value to enable debugging
|
||||||
DEBUG=
|
DEBUG=
|
||||||
|
|
241
static/player.js
241
static/player.js
|
@ -1,241 +0,0 @@
|
||||||
/*!
|
|
||||||
* Howler.js Audio Player Demo
|
|
||||||
* howlerjs.com
|
|
||||||
*
|
|
||||||
* (c) 2013-2020, James Simpson of GoldFire Studios
|
|
||||||
* goldfirestudios.com
|
|
||||||
*
|
|
||||||
* MIT License
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Cache references to DOM elements.
|
|
||||||
var elms = ['track', 'timer', 'duration', 'playBtn', 'pauseBtn', 'prevBtn', 'nextBtn', 'playlistBtn', 'progress', 'bar', 'loading', 'playlist', 'list', 'barEmpty', 'barFull', 'sliderBtn'];
|
|
||||||
elms.forEach(function(elm) {
|
|
||||||
window[elm] = document.getElementById(elm);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Player class containing the state of our playlist and where we are in it.
|
|
||||||
* Includes all methods for playing, skipping, updating the display, etc.
|
|
||||||
* @param {Array} playlist Array of objects with playlist song details ({title, url, howl}).
|
|
||||||
*/
|
|
||||||
var Player = function(playlist) {
|
|
||||||
this.playlist = playlist;
|
|
||||||
this.index = 0;
|
|
||||||
|
|
||||||
// Display the title of the first track.
|
|
||||||
track.innerHTML = '1. ' + playlist[0].title;
|
|
||||||
|
|
||||||
// Setup the playlist display.
|
|
||||||
playlist.forEach(function(song) {
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.className = 'list-song';
|
|
||||||
div.innerHTML = song.title;
|
|
||||||
div.onclick = function() {
|
|
||||||
player.skipTo(playlist.indexOf(song));
|
|
||||||
};
|
|
||||||
list.appendChild(div);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Player.prototype = {
|
|
||||||
/**
|
|
||||||
* Play a song in the playlist.
|
|
||||||
* @param {Number} index Index of the song in the playlist (leave empty to play the first or current).
|
|
||||||
*/
|
|
||||||
play: function(index) {
|
|
||||||
var self = this;
|
|
||||||
var sound;
|
|
||||||
|
|
||||||
index = typeof index === 'number' ? index : self.index;
|
|
||||||
var data = self.playlist[index];
|
|
||||||
|
|
||||||
// If we already loaded this track, use the current one.
|
|
||||||
// Otherwise, setup and load a new Howl.
|
|
||||||
if (data.howl) {
|
|
||||||
sound = data.howl;
|
|
||||||
} else {
|
|
||||||
sound = data.howl = new Howl({
|
|
||||||
src: [data.url],
|
|
||||||
html5: true, // Force to HTML5 so that the audio can stream in (best for large files).
|
|
||||||
onplay: function() {
|
|
||||||
// Display the duration.
|
|
||||||
duration.innerHTML = self.formatTime(Math.round(sound.duration()));
|
|
||||||
|
|
||||||
// Start updating the progress of the track.
|
|
||||||
requestAnimationFrame(self.step.bind(self));
|
|
||||||
|
|
||||||
pauseBtn.style.display = 'block';
|
|
||||||
},
|
|
||||||
onload: function() {
|
|
||||||
loading.style.display = 'none';
|
|
||||||
},
|
|
||||||
onend: function() {
|
|
||||||
self.skip('next');
|
|
||||||
},
|
|
||||||
onpause: function() {
|
|
||||||
},
|
|
||||||
onstop: function() {
|
|
||||||
},
|
|
||||||
onseek: function() {
|
|
||||||
// Start updating the progress of the track.
|
|
||||||
requestAnimationFrame(self.step.bind(self));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin playing the sound.
|
|
||||||
sound.play();
|
|
||||||
|
|
||||||
// Update the track display.
|
|
||||||
track.innerHTML = (index + 1) + '. ' + data.title;
|
|
||||||
|
|
||||||
// Show the pause button.
|
|
||||||
if (sound.state() === 'loaded') {
|
|
||||||
playBtn.style.display = 'none';
|
|
||||||
pauseBtn.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
loading.style.display = 'block';
|
|
||||||
playBtn.style.display = 'none';
|
|
||||||
pauseBtn.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep track of the index we are currently playing.
|
|
||||||
self.index = index;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause the currently playing track.
|
|
||||||
*/
|
|
||||||
pause: function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Get the Howl we want to manipulate.
|
|
||||||
var sound = self.playlist[self.index].howl;
|
|
||||||
|
|
||||||
// Puase the sound.
|
|
||||||
sound.pause();
|
|
||||||
|
|
||||||
// Show the play button.
|
|
||||||
playBtn.style.display = 'block';
|
|
||||||
pauseBtn.style.display = 'none';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip to the next or previous track.
|
|
||||||
* @param {String} direction 'next' or 'prev'.
|
|
||||||
*/
|
|
||||||
skip: function(direction) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Get the next track based on the direction of the track.
|
|
||||||
var index = 0;
|
|
||||||
if (direction === 'prev') {
|
|
||||||
index = self.index - 1;
|
|
||||||
if (index < 0) {
|
|
||||||
index = self.playlist.length - 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
index = self.index + 1;
|
|
||||||
if (index >= self.playlist.length) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.skipTo(index);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip to a specific track based on its playlist index.
|
|
||||||
* @param {Number} index Index in the playlist.
|
|
||||||
*/
|
|
||||||
skipTo: function(index) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Stop the current track.
|
|
||||||
if (self.playlist[self.index].howl) {
|
|
||||||
self.playlist[self.index].howl.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset progress.
|
|
||||||
progress.style.width = '0%';
|
|
||||||
|
|
||||||
// Play the new track.
|
|
||||||
self.play(index);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the volume and update the volume slider display.
|
|
||||||
* @param {Number} val Volume between 0 and 1.
|
|
||||||
*/
|
|
||||||
volume: function(val) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Update the global volume (affecting all Howls).
|
|
||||||
Howler.volume(val);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seek to a new position in the currently playing track.
|
|
||||||
* @param {Number} per Percentage through the song to skip.
|
|
||||||
*/
|
|
||||||
seek: function(per) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Get the Howl we want to manipulate.
|
|
||||||
var sound = self.playlist[self.index].howl;
|
|
||||||
|
|
||||||
// Convert the percent into a seek position.
|
|
||||||
if (sound.playing()) {
|
|
||||||
sound.seek(sound.duration() * per);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The step called within requestAnimationFrame to update the playback position.
|
|
||||||
*/
|
|
||||||
step: function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Get the Howl we want to manipulate.
|
|
||||||
var sound = self.playlist[self.index].howl;
|
|
||||||
|
|
||||||
// Determine our current seek position.
|
|
||||||
var seek = sound.seek() || 0;
|
|
||||||
timer.innerHTML = self.formatTime(Math.round(seek));
|
|
||||||
progress.style.width = (((seek / sound.duration()) * 100) || 0) + '%';
|
|
||||||
|
|
||||||
// If the sound is still playing, continue stepping.
|
|
||||||
if (sound.playing()) {
|
|
||||||
requestAnimationFrame(self.step.bind(self));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format the time from seconds to M:SS.
|
|
||||||
* @param {Number} secs Seconds to format.
|
|
||||||
* @return {String} Formatted time.
|
|
||||||
*/
|
|
||||||
formatTime: function(secs) {
|
|
||||||
var minutes = Math.floor(secs / 60) || 0;
|
|
||||||
var seconds = (secs - minutes * 60) || 0;
|
|
||||||
|
|
||||||
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup our new audio player class and pass it the playlist.
|
|
||||||
var player = new Player(playlist_tracks);
|
|
||||||
|
|
||||||
// Bind our player controls.
|
|
||||||
playBtn.addEventListener('click', function() {
|
|
||||||
player.play();
|
|
||||||
});
|
|
||||||
pauseBtn.addEventListener('click', function() {
|
|
||||||
player.pause();
|
|
||||||
});
|
|
||||||
prevBtn.addEventListener('click', function() {
|
|
||||||
player.skip('prev');
|
|
||||||
});
|
|
||||||
nextBtn.addEventListener('click', function() {
|
|
||||||
player.skip('next');
|
|
||||||
});
|
|
|
@ -1,259 +0,0 @@
|
||||||
html {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
outline: 0;
|
|
||||||
font-family: 'Clarendon MT Std', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgb(0,4,16);
|
|
||||||
background: linear-gradient(0deg, rgba(0,4,16,1) 31%, rgba(1,125,147,1) 97%);
|
|
||||||
}
|
|
||||||
|
|
||||||
a, a:visited {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #f1f2f6;
|
|
||||||
}
|
|
||||||
a:active, a:hover {
|
|
||||||
color: #70bc45;
|
|
||||||
}
|
|
||||||
|
|
||||||
#container {
|
|
||||||
overflow-x: wrap;
|
|
||||||
width: 96%;
|
|
||||||
height: 96%;
|
|
||||||
margin: auto;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#details {
|
|
||||||
margin-left: 0.25em;
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid rgb(255,255,255,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#poster {
|
|
||||||
display: none;
|
|
||||||
width: 10em;
|
|
||||||
height: 10em;
|
|
||||||
background: #DDD;
|
|
||||||
}
|
|
||||||
#poster > img {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* informmation */
|
|
||||||
#playlist_title {
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
color: #f1f2f6;
|
|
||||||
}
|
|
||||||
#playlist_desc {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#player {
|
|
||||||
width: 100%;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#controls {
|
|
||||||
width: 5em;
|
|
||||||
height: 5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
#controls > div {
|
|
||||||
width: 5em;
|
|
||||||
height: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#track {
|
|
||||||
color: #70bc45;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#track_controls {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timer {
|
|
||||||
margin-left:0;
|
|
||||||
padding-left:0;
|
|
||||||
}
|
|
||||||
#duration {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Controls */
|
|
||||||
.widget {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
padding: 0 0.5em;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
.btn:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
#big_button {
|
|
||||||
position: relative;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 5em;
|
|
||||||
}
|
|
||||||
#playBtn {
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 5em;
|
|
||||||
padding-left: 0.1em;
|
|
||||||
}
|
|
||||||
#pauseBtn {
|
|
||||||
font-family: sans-serif;
|
|
||||||
display: none;
|
|
||||||
font-size: 4em;
|
|
||||||
}
|
|
||||||
#prevBtn {
|
|
||||||
color: #fff;
|
|
||||||
text-align: right;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 0.75em;
|
|
||||||
font-size: 1.5em;
|
|
||||||
padding:0;
|
|
||||||
}
|
|
||||||
#nextBtn {
|
|
||||||
color: #fff;
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 0.75em;
|
|
||||||
font-size: 1.5em;
|
|
||||||
padding:0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Progress */
|
|
||||||
#bar {
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
#bar > hr {
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid #000;
|
|
||||||
position: absolute;
|
|
||||||
height: 1px;
|
|
||||||
width: calc(100% - 1em);
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress {
|
|
||||||
position: relative;
|
|
||||||
width: 0%;
|
|
||||||
height: 0.33em;
|
|
||||||
margin: 0.33em 0;
|
|
||||||
background-color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading */
|
|
||||||
#loading {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Plylist */
|
|
||||||
#playlist {
|
|
||||||
display: block;
|
|
||||||
margin-left: 0.25em;
|
|
||||||
}
|
|
||||||
#list {
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
.list-song {
|
|
||||||
padding: 0.25em;
|
|
||||||
}
|
|
||||||
.list-song:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background: #f1f2f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer {
|
|
||||||
font-family: helvetica, sans-serif;
|
|
||||||
border-top: 1px solid rgb(128,128,128,0.3);
|
|
||||||
text-align: right;
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.9em;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
margin: 0;
|
|
||||||
margin-top: 1em;
|
|
||||||
padding-top: 1em;
|
|
||||||
}
|
|
||||||
#footer a {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Volume */
|
|
||||||
#volume {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.bar {
|
|
||||||
}
|
|
||||||
#barEmpty {
|
|
||||||
width: 90%;
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#barFull {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
#sliderBtn {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fade-In */
|
|
||||||
.fadeout {
|
|
||||||
webkit-animation: fadeout 0.5s;
|
|
||||||
-ms-animation: fadeout 0.5s;
|
|
||||||
animation: fadeout 0.5s;
|
|
||||||
}
|
|
||||||
.fadein {
|
|
||||||
webkit-animation: fadein 0.5s;
|
|
||||||
-ms-animation: fadein 0.5s;
|
|
||||||
animation: fadein 0.5s;
|
|
||||||
}
|
|
||||||
@keyframes fadein {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
@-webkit-keyframes fadein {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
@-ms-keyframes fadein {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
@keyframes fadeout {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
||||||
@-webkit-keyframes fadeout {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
||||||
@-ms-keyframes fadeout {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="user-scalable=no">
|
|
||||||
<title>Groove On Demand</title>
|
|
||||||
<link rel='stylesheet' href='/static/styles.css' />
|
|
||||||
<link rel='stylesheet' href="https://fonts.cdnfonts.com/css/clarendon-mt-std" />
|
|
||||||
<script defer crossorigin src='https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.core.min.js'></script>
|
|
||||||
<script defer src='/static/player.js'></script>
|
|
||||||
<script>
|
|
||||||
var playlist_tracks = [
|
|
||||||
% for entry in playlist['entries']:
|
|
||||||
{
|
|
||||||
title: "{{entry['artist']}} - {{entry['title']}}",
|
|
||||||
url: "{{entry['url']}}",
|
|
||||||
},
|
|
||||||
% end
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id='container'>
|
|
||||||
|
|
||||||
<div id='details'>
|
|
||||||
<div id='poster'>
|
|
||||||
<img src=''></img>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 id='playlist_title'>{{playlist['name']}}</h1>
|
|
||||||
<span id='playlist_desc'>{{playlist['description']}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table id='player'>
|
|
||||||
<tr>
|
|
||||||
<td id='controls'>
|
|
||||||
<div id='big_button' class='btn'>
|
|
||||||
<div id="loading"></div>
|
|
||||||
<div id="playBtn">⏵</div>
|
|
||||||
<div id="pauseBtn">⏸</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div id="track"></div>
|
|
||||||
<div id='track_controls'>
|
|
||||||
<div class='widget' id="timer">0:00</div>
|
|
||||||
<div class='widget' id="bar">
|
|
||||||
<hr>
|
|
||||||
<div id="progress"></div>
|
|
||||||
</div>
|
|
||||||
<div class='widget' id="duration">0:00</div>
|
|
||||||
<div class="widget btn" id="prevBtn">⏮</div>
|
|
||||||
<div class="widget btn" id="nextBtn">⏭</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div id="playlist">
|
|
||||||
<div id="list"></div>
|
|
||||||
</div>
|
|
||||||
<div id='footer'>groove on demand : an <a alt="evilchili at liner notes dot club" href="https://linernotes.club/@evilchili">@evilchili</a> jam</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue
Block a user