WIP web player UX

This commit is contained in:
evilchili 2022-12-03 23:23:25 -08:00
parent 53e102c8b2
commit 7306c3ab15
4 changed files with 583 additions and 12 deletions

View File

@ -13,7 +13,7 @@ from groove.db.manager import database_manager
from groove.playlist import Playlist from groove.playlist import Playlist
from groove.webserver import requests from groove.webserver import requests
from groove.exceptions import APIHandlingException # from groove.exceptions import APIHandlingException
server = bottle.Bottle() server = bottle.Bottle()
@ -53,6 +53,11 @@ def build():
return "Authenticated. Groovy." return "Authenticated. Groovy."
@server.route('/static/<filepath:path>')
def server_static(filepath):
return static_file(filepath, root='static')
@bottle.auth_basic(is_authenticated) @bottle.auth_basic(is_authenticated)
@server.route('/build/search/playlist') @server.route('/build/search/playlist')
def search_playlist(slug, db): def search_playlist(slug, db):
@ -91,10 +96,16 @@ def serve_playlist(slug, db):
logging.debug(f"Loaded {playlist.record}") logging.debug(f"Loaded {playlist.record}")
logging.debug(playlist.as_dict['entries']) logging.debug(playlist.as_dict['entries'])
args = [ pl = playlist.as_dict
(requests.encode([str(entry['track_id'])], uri='/track'), entry['track_id'])
for entry in playlist.as_dict['entries'] for entry in pl['entries']:
] sig = requests.encode([str(entry['track_id'])], uri='/track')
entry['url'] = f"/track/{sig}/{entry['track_id']}"
template_path = Path(os.environ['TEMPLATE_PATH']) / Path('playlist.tpl') template_path = Path(os.environ['TEMPLATE_PATH']) / Path('playlist.tpl')
return template(str(template_path), url=requests.url(), playlist=playlist.as_dict, args=args) body = template(
str(template_path),
url=requests.url(),
playlist=pl
)
return HTTPResponse(status=200, body=body)

241
static/player.js Normal file
View File

@ -0,0 +1,241 @@
/*!
* 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');
});

259
static/styles.css Normal file
View File

@ -0,0 +1,259 @@
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; }
}

View File

@ -1,6 +1,66 @@
<audio id="audio" preload="none" tabindex="0"> <!doctype html>
% for sig, track_id in args: <html lang="en">
<source src="/track/{{sig}}/{{track_id}}"> <head>
% end <meta charset="utf-8">
Your browser does not support HTML5 audio. <meta name="viewport" content="user-scalable=no">
</audio> <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>