Compare commits
No commits in common. "d1e065d9f6d606cd7b1131f88a2b7c0307ecbd99" and "b0e4b956a9ddd3172dfd18bdc8aafe50cbef45b1" have entirely different histories.
d1e065d9f6
...
b0e4b956a9
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*.json
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ Stream some music.
|
||||||
## Requirements
|
## Requirements
|
||||||
Python 3.6+
|
Python 3.6+
|
||||||
FFmpeg compiled with `--enable-libopus`
|
FFmpeg compiled with `--enable-libopus`
|
||||||
Python packages: `flask gunicorn mutagen Flask-RESTful`
|
Python packages: `flask gunicorn mutagen`
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
1. Get on the floor
|
1. Get on the floor
|
||||||
|
|
268
musik.py
268
musik.py
|
@ -3,16 +3,17 @@
|
||||||
Music streaming.
|
Music streaming.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import subprocess
|
import subprocess
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from flask import Flask, Response, render_template, send_file, url_for
|
from flask import Flask, Response, render_template, send_file
|
||||||
from flask_restful import reqparse, abort, Api, Resource
|
from werkzeug.utils import secure_filename
|
||||||
import mutagen
|
|
||||||
|
|
||||||
MUSIC_DIR = "/home/iou1name/music/Music"
|
MUSIC_DIR = "/mnt/music/Music"
|
||||||
MUSIC_EXT = ['flac', 'mp3', 'wav', 'm4a']
|
MUSIC_EXT = ['flac', 'mp3', 'wav', 'm4a']
|
||||||
FFMPEG_CMD = [
|
FFMPEG_CMD = [
|
||||||
'ffmpeg', '-y',
|
'ffmpeg', '-y',
|
||||||
|
@ -24,178 +25,46 @@ FFMPEG_CMD = [
|
||||||
'-'
|
'-'
|
||||||
]
|
]
|
||||||
|
|
||||||
class Track:
|
def build_tree(root_dir):
|
||||||
def __init__(self, filepath=None, d=None, coverart=""):
|
"""Walks the music directory and builds a tree."""
|
||||||
if d:
|
print("Building tree.")
|
||||||
for attr, value in d.items():
|
tree = defaultdict(dict)
|
||||||
setattr(self, attr, value)
|
for dirName, subDirs, files in os.walk(root_dir):
|
||||||
return
|
if dirName == root_dir:
|
||||||
|
|
||||||
if filepath.endswith("mp3"):
|
|
||||||
m = mutagen.mp3.EasyMP3(filepath)
|
|
||||||
else:
|
|
||||||
m = mutagen.File(filepath)
|
|
||||||
self.tracknumber = m.get('tracknumber', [''])[0]
|
|
||||||
self.title = m.get('title', [''])[0]
|
|
||||||
self.artist = m.get('artist', [''])[0]
|
|
||||||
self.album = m.get('album', [''])[0]
|
|
||||||
self.date = m.get('date', [''])[0]
|
|
||||||
self.length = str(int(m.info.length) // 60) + ":"
|
|
||||||
self.length += str(int(m.info.length) % 60)
|
|
||||||
self.filepath = filepath
|
|
||||||
self.coverart = coverart
|
|
||||||
|
|
||||||
|
|
||||||
def build_library(root_dir):
|
|
||||||
"""Walks the music directory and builds a library of tracks."""
|
|
||||||
print("Building library")
|
|
||||||
tracks = []
|
|
||||||
for dir_name, sub_dirs, files in os.walk(root_dir):
|
|
||||||
for file in files:
|
|
||||||
if not os.path.splitext(file)[1][1:] in MUSIC_EXT:
|
|
||||||
continue
|
continue
|
||||||
filepath = os.path.join(root_dir, dir_name, file)
|
reg = re.search(".*/(.+?) - (.+) \(\d{4}\)", dirName)
|
||||||
if "folder.jpg" in files:
|
if not reg:
|
||||||
coverart = os.path.join(root_dir, dir_name, "folder.jpg")
|
print(dirName)
|
||||||
else:
|
continue
|
||||||
coverart = ""
|
artist, album = reg.groups()
|
||||||
track = Track(filepath, coverart=coverart)
|
tracks = [f for f in files if f.rpartition('.')[2] in MUSIC_EXT]
|
||||||
tracks.append(track)
|
tree[artist][album] = tracks
|
||||||
print("Done")
|
return tree
|
||||||
return tracks
|
|
||||||
|
|
||||||
|
|
||||||
def init_library():
|
|
||||||
"""Loads the library from file, or builds a new one if one isn't found."""
|
|
||||||
if os.path.isfile("library.json"):
|
|
||||||
with open("library.json", "r") as file:
|
|
||||||
tracks = json.loads(file.read())
|
|
||||||
tracks = [Track(d=d) for d in tracks]
|
|
||||||
else:
|
|
||||||
tracks = build_library(MUSIC_DIR)
|
|
||||||
with open("library.json", "w") as file:
|
|
||||||
file.write(json.dumps([vars(t) for t in tracks]))
|
|
||||||
return tracks
|
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
api = Api(app)
|
|
||||||
tracks = init_library()
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""Main index page."""
|
"""Main index page."""
|
||||||
artists = list(set(t.artist for t in tracks))
|
nav_items = os.listdir(MUSIC_DIR)
|
||||||
artists.sort()
|
nav_items.sort()
|
||||||
|
nav_items = [item + '/' for item in nav_items]
|
||||||
|
cd = "/"
|
||||||
return render_template('index.html', **locals())
|
return render_template('index.html', **locals())
|
||||||
|
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
@app.route('/stream/<path:track>')
|
||||||
parser.add_argument('artist')
|
def stream(track):
|
||||||
parser.add_argument('album')
|
|
||||||
parser.add_argument('track')
|
|
||||||
|
|
||||||
def validate_select_args(args):
|
|
||||||
"""
|
|
||||||
If a track is specified, both artist and album must also be specified.
|
|
||||||
If an album is specified, the artist must also be specified.
|
|
||||||
"""
|
|
||||||
if args.get('track'):
|
|
||||||
if not args.get('artist') or not args.get('album'):
|
|
||||||
abort(400, message="Artist and album must also be specified.")
|
|
||||||
elif args.get('album'):
|
|
||||||
if not args.get('artist'):
|
|
||||||
abort(400, message="Artist must also be specified.")
|
|
||||||
elif not args.get('artist'):
|
|
||||||
abort(400, message="You must specify at least an artist.")
|
|
||||||
|
|
||||||
class Selection(Resource):
|
|
||||||
def get(self):
|
|
||||||
global tracks
|
|
||||||
args = parser.parse_args()
|
|
||||||
validate_select_args(args)
|
|
||||||
|
|
||||||
if args.get('track'):
|
|
||||||
for track in tracks:
|
|
||||||
if (track.artist == args.get('artist') and
|
|
||||||
track.album == args.get('album') and
|
|
||||||
track.title == args.get('track')):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
abort(404, message="Track does not exist.")
|
|
||||||
found = dict(vars(track))
|
|
||||||
found.pop('filepath')
|
|
||||||
found['streampath'] = url_for(
|
|
||||||
'stream',
|
|
||||||
artist=parse.quote(track.artist, safe=''),
|
|
||||||
album=parse.quote(track.album, safe=''),
|
|
||||||
track=parse.quote(track.title, safe=''))
|
|
||||||
found['coverart'] = url_for(
|
|
||||||
'coverart',
|
|
||||||
artist=parse.quote(track.artist, safe=''),
|
|
||||||
album=parse.quote(track.album, safe=''),
|
|
||||||
track=parse.quote(track.title, safe=''))
|
|
||||||
return found
|
|
||||||
|
|
||||||
elif args.get('album'):
|
|
||||||
found = []
|
|
||||||
for track in tracks:
|
|
||||||
if (track.artist == args.get('artist') and
|
|
||||||
track.album == args.get('album')):
|
|
||||||
found.append(track)
|
|
||||||
if not found:
|
|
||||||
abort(404, message="Album does not exist.")
|
|
||||||
found = [t.tracknumber + " - " + t.title for t in found]
|
|
||||||
found.sort()
|
|
||||||
return found
|
|
||||||
|
|
||||||
elif args.get('artist'):
|
|
||||||
found = []
|
|
||||||
for track in tracks:
|
|
||||||
if track.artist == args.get('artist'):
|
|
||||||
found.append(track)
|
|
||||||
if not found:
|
|
||||||
abort(404, message="Artist does not exist.")
|
|
||||||
found = list(set(t.album for t in found))
|
|
||||||
return sorted(found)
|
|
||||||
|
|
||||||
|
|
||||||
class RandomSelection(Resource):
|
|
||||||
def get(self):
|
|
||||||
global tracks
|
|
||||||
track = random.choice(tracks)
|
|
||||||
found = dict(vars(track))
|
|
||||||
found.pop('filepath')
|
|
||||||
found['streampath'] = url_for(
|
|
||||||
'stream',
|
|
||||||
artist=track.artist,
|
|
||||||
album=track.album,
|
|
||||||
track=track.title)
|
|
||||||
found['coverart'] = url_for(
|
|
||||||
'coverart',
|
|
||||||
artist=track.artist,
|
|
||||||
album=track.album,
|
|
||||||
track=track.title)
|
|
||||||
return found
|
|
||||||
|
|
||||||
api.add_resource(Selection, '/select')
|
|
||||||
api.add_resource(RandomSelection, '/select/random')
|
|
||||||
api.init_app(app)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/stream/<artist>/<album>/<track>')
|
|
||||||
def stream(artist, album, track):
|
|
||||||
"""View for the raw audio file."""
|
"""View for the raw audio file."""
|
||||||
artist, album, track = map(parse.unquote, (artist, album, track))
|
track = parse.unquote(track)
|
||||||
for t in tracks:
|
path = os.path.join(MUSIC_DIR, track)
|
||||||
if (t.artist == artist and
|
path = os.path.abspath(path)
|
||||||
t.album == album and
|
if not path.startswith(MUSIC_DIR):
|
||||||
t.title == track):
|
return "False"
|
||||||
break
|
if not os.path.isfile(path):
|
||||||
else:
|
return "False"
|
||||||
abort(404, message="Track does not exist.")
|
FFMPEG_CMD[5] = path
|
||||||
|
|
||||||
FFMPEG_CMD[5] = t.filepath
|
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
with subprocess.Popen(FFMPEG_CMD, stdout=subprocess.PIPE) as proc:
|
with subprocess.Popen(FFMPEG_CMD, stdout=subprocess.PIPE) as proc:
|
||||||
|
@ -206,22 +75,63 @@ def stream(artist, album, track):
|
||||||
return Response(generate(), mimetype="audio/ogg")
|
return Response(generate(), mimetype="audio/ogg")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/coverart/<artist>/<album>/<track>')
|
@app.route('/get_dir/<path:directory>/')
|
||||||
def coverart(artist, album, track):
|
@app.route('/get_dir/')
|
||||||
"""View for the raw audio file."""
|
def get_dir(directory=""):
|
||||||
artist, album, track = map(parse.unquote, (artist, album, track))
|
"""Returns the contents of the requested directory."""
|
||||||
for t in tracks:
|
directory = directory.replace("DOTDOT", "..")
|
||||||
if (t.artist == artist and
|
directory = os.path.join(MUSIC_DIR, directory)
|
||||||
t.album == album and
|
directory = os.path.abspath(directory)
|
||||||
t.title == track):
|
if not directory.startswith(MUSIC_DIR):
|
||||||
break
|
|
||||||
else:
|
|
||||||
abort(404, message="Track does not exist.")
|
|
||||||
|
|
||||||
if t.coverart:
|
|
||||||
return send_file(t.coverart)
|
|
||||||
else:
|
|
||||||
return "False"
|
return "False"
|
||||||
|
if not os.path.isdir(directory):
|
||||||
|
return "False"
|
||||||
|
|
||||||
|
nav_items = os.listdir(directory)
|
||||||
|
nav_items.sort()
|
||||||
|
if directory != MUSIC_DIR:
|
||||||
|
nav_items = [".."] + nav_items
|
||||||
|
|
||||||
|
nav_items_new = []
|
||||||
|
for item in nav_items:
|
||||||
|
if os.path.isdir(os.path.join(directory, item)):
|
||||||
|
item += '/'
|
||||||
|
nav_items_new.append(item)
|
||||||
|
nav_items_new = [directory.replace(MUSIC_DIR, '') + '/'] + nav_items_new
|
||||||
|
|
||||||
|
return json.dumps(nav_items_new)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/get_shuffle')
|
||||||
|
def shuffle():
|
||||||
|
"""Returns a randomly selected track from the library."""
|
||||||
|
item = random.choice(os.listdir(MUSIC_DIR))
|
||||||
|
path = os.path.join(MUSIC_DIR, item)
|
||||||
|
n = 0
|
||||||
|
while not item.rpartition('.')[2] in MUSIC_EXT:
|
||||||
|
n += 1
|
||||||
|
item = random.choice(os.listdir(path))
|
||||||
|
if os.path.isdir(os.path.join(path, item)):
|
||||||
|
path = os.path.join(path, item)
|
||||||
|
if n == 5:
|
||||||
|
item = random.choice(os.listdir(MUSIC_DIR))
|
||||||
|
path = os.path.join(MUSIC_DIR, item)
|
||||||
|
n = 0
|
||||||
|
path = os.path.join(path, item)
|
||||||
|
return path.replace(MUSIC_DIR, '')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/album_cover/<path:cover>')
|
||||||
|
def album_cover(cover):
|
||||||
|
"""View for the raw audio file."""
|
||||||
|
path = os.path.join(MUSIC_DIR, cover)
|
||||||
|
path = os.path.abspath(path)
|
||||||
|
if not path.startswith(MUSIC_DIR):
|
||||||
|
return "False"
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return "False"
|
||||||
|
|
||||||
|
return send_file(path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -21,16 +21,16 @@ img {
|
||||||
#navigationContainer {
|
#navigationContainer {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 100%;
|
|
||||||
border-top: 2px solid #ccc;
|
border-top: 2px solid #ccc;
|
||||||
border-bottom: 2px solid #ccc;
|
border-bottom: 2px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
#currentDirectory {
|
||||||
height: 100%;
|
padding-left: 0.5em;
|
||||||
width: 100%;
|
}
|
||||||
|
|
||||||
|
#navItems {
|
||||||
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playerContainer {
|
#playerContainer {
|
||||||
|
|
|
@ -8,74 +8,71 @@ function load() {
|
||||||
httpRequest.onreadystatechange = function() {
|
httpRequest.onreadystatechange = function() {
|
||||||
if (httpRequest.readyState !== XMLHttpRequest.DONE) { return; }
|
if (httpRequest.readyState !== XMLHttpRequest.DONE) { return; }
|
||||||
if (httpRequest.status !== 200) { return; }
|
if (httpRequest.status !== 200) { return; }
|
||||||
|
let track = httpRequest.responseText;
|
||||||
let track = JSON.parse(httpRequest.responseText);
|
|
||||||
change_track(track);
|
change_track(track);
|
||||||
};
|
};
|
||||||
httpRequest.open('GET', api_uri + '/random', true);
|
httpRequest.open('GET', '/musik/get_shuffle', true);
|
||||||
httpRequest.send();
|
httpRequest.send();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function select_artist(select) {
|
|
||||||
var httpRequest;
|
function navigate(item) {
|
||||||
httpRequest = new XMLHttpRequest();
|
if (/\..{3,5}$/.test(item)) {
|
||||||
httpRequest.onreadystatechange = function () {
|
select_track(item);
|
||||||
if (httpRequest.readyState !== XMLHttpRequest.DONE) { return; }
|
}
|
||||||
if (httpRequest.status !== 200) { return; }
|
else {
|
||||||
nav_items = JSON.parse(httpRequest.responseText);
|
get_dir(item);
|
||||||
let html_str = '';
|
|
||||||
for (let i = 0; i < nav_items.length; i++) {
|
|
||||||
html_str += '<option value="' + nav_items[i] + '">' + nav_items[i] + '</option>';
|
|
||||||
}
|
}
|
||||||
document.getElementById('albumList').innerHTML = html_str;
|
|
||||||
document.getElementById('trackList').innerHTML = '';
|
|
||||||
};
|
|
||||||
httpRequest.open('GET', api_uri + '?artist=' + select.value, true);
|
|
||||||
httpRequest.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function select_album(select) {
|
function select_track(item) {
|
||||||
var httpRequest;
|
let cd = document.getElementById('currentDirectory').innerText;
|
||||||
httpRequest = new XMLHttpRequest();
|
let track = cd + item;
|
||||||
httpRequest.onreadystatechange = function () {
|
|
||||||
if (httpRequest.readyState !== XMLHttpRequest.DONE) { return; }
|
|
||||||
if (httpRequest.status !== 200) { return; }
|
|
||||||
nav_items = JSON.parse(httpRequest.responseText);
|
|
||||||
let html_str = '';
|
|
||||||
for (let i = 0; i < nav_items.length; i++) {
|
|
||||||
html_str += '<option value="' + nav_items[i].replace(/\d* - /, '') + '">' + nav_items[i] + '</option>';
|
|
||||||
}
|
|
||||||
document.getElementById('trackList').innerHTML = html_str;
|
|
||||||
};
|
|
||||||
httpRequest.open('GET', api_uri + '?artist=' + document.getElementById('artistList').value + '&album=' + select.value, true);
|
|
||||||
httpRequest.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_track(select) {
|
|
||||||
var httpRequest;
|
|
||||||
httpRequest = new XMLHttpRequest();
|
|
||||||
httpRequest.onreadystatechange = function () {
|
|
||||||
if (httpRequest.readyState !== XMLHttpRequest.DONE) { return; }
|
|
||||||
if (httpRequest.status !== 200) { return; }
|
|
||||||
|
|
||||||
let track = JSON.parse(httpRequest.responseText);
|
|
||||||
change_track(track);
|
change_track(track);
|
||||||
};
|
|
||||||
httpRequest.open('GET', api_uri + '?artist=' + document.getElementById('artistList').value + '&album=' + document.getElementById('albumList').value + '&track=' + select.value, true);
|
|
||||||
httpRequest.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function change_track(track) {
|
function change_track(track) {
|
||||||
let source = document.getElementById('stream');
|
let source = document.getElementById('stream');
|
||||||
source.src = track.streampath;
|
source.src = '/musik/stream' + encodeURIComponent(track);
|
||||||
let player = document.getElementById('player');
|
let player = document.getElementById('player');
|
||||||
player.load();
|
player.load();
|
||||||
player.play();
|
player.play();
|
||||||
document.getElementById('nowPlayingArtist').innerHTML = track.artist;
|
document.getElementById('nowPlaying').innerHTML = track;
|
||||||
document.getElementById('nowPlayingAlbum').innerHTML = track.album;
|
|
||||||
document.getElementById('nowPlayingTitle').innerHTML = track.title;
|
|
||||||
|
|
||||||
document.getElementById('albumCover').firstChild.src = track.coverart;
|
let arr = track.split('/');
|
||||||
|
let art = document.getElementById('albumCover');
|
||||||
|
art.firstChild.src = '/musik/album_cover' + arr.slice(0, arr.length-1).join("/") + '/folder.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_dir(item) {
|
||||||
|
let cd = document.getElementById('currentDirectory').innerText;
|
||||||
|
if (item === "../") {
|
||||||
|
if (cd !== "/") {
|
||||||
|
item = cd.slice(0, cd.slice(0, -1).lastIndexOf("/")+1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
item = cd + item;
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpRequest;
|
||||||
|
httpRequest = new XMLHttpRequest();
|
||||||
|
httpRequest.onreadystatechange = function () {
|
||||||
|
if (httpRequest.readyState !== XMLHttpRequest.DONE) { return; }
|
||||||
|
if (httpRequest.status !== 200) { return; }
|
||||||
|
nav_items = JSON.parse(httpRequest.responseText);
|
||||||
|
document.getElementById('currentDirectory').innerText = nav_items.shift();
|
||||||
|
let html_str = '';
|
||||||
|
for (let i = 0; i < nav_items.length; i++) {
|
||||||
|
html_str += `<li><a href="javascript:void(0);" onclick="navigate(\`${nav_items[i]}\`)">${nav_items[i]}</a></li>`;
|
||||||
|
}
|
||||||
|
document.getElementById('navItems').innerHTML = html_str;
|
||||||
|
};
|
||||||
|
httpRequest.open('GET', '/musik/get_dir/' + item, true);
|
||||||
|
httpRequest.send();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,36 +4,23 @@
|
||||||
<title>Musik</title>
|
<title>Musik</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/static/musik.css">
|
<link rel="stylesheet" type="text/css" href="/static/musik.css">
|
||||||
<script type="text/javascript" src="/static/musik.js"></script>
|
<script type="text/javascript" src="/static/musik.js"></script>
|
||||||
<script>
|
|
||||||
const api_uri = "{{ url_for('selection') }}";
|
|
||||||
</script>
|
|
||||||
<script>window.onload = load;</script>
|
<script>window.onload = load;</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="globalContainer">
|
<div id="globalContainer">
|
||||||
<div id="titleContainer"><h1>Musik</h1></div>
|
<div id="titleContainer"><h1>Musik</h1></div>
|
||||||
<div id="navigationContainer">
|
<div id="navigationContainer">
|
||||||
<div id="artistListContainer" class="list">
|
<div id="currentDirectory">{{ cd }}</div>
|
||||||
<select id="artistList" size="2" class="list" onchange="select_artist(this)">
|
<ul id="navItems">
|
||||||
{% for artist in artists %}
|
{% for item in nav_items %}
|
||||||
<option value="{{ artist }}">{{ artist }}</option>
|
<li><a href="javascript:void(0);" onclick="navigate('{{ item }}')">{{ item }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</ul>
|
||||||
</div>
|
|
||||||
<div id="albumListContainer" class="list">
|
|
||||||
<select id="albumList" size="2" class="list" onchange="select_album(this)">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="trackListContainer" class="list">
|
|
||||||
<select id="trackList" size="2" class="list" onchange="select_track(this)">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="playerContainer">
|
<div id="playerContainer">
|
||||||
<span id="albumCover"><img src=""/></span>
|
<span id="albumCover"><img src=""/></span>
|
||||||
<span id="playerControls">
|
<span id="playerControls">
|
||||||
<h4><span id="nowPlayingArtist"></span> - <span id="nowPlayingAlbum"></span></h4>
|
<h3 id="nowPlaying"></h3>
|
||||||
<h3 id="nowPlayingTitle"></h3>
|
|
||||||
<audio id="player" controls>
|
<audio id="player" controls>
|
||||||
<source id="stream" src="" type="audio/ogg">
|
<source id="stream" src="" type="audio/ogg">
|
||||||
</audio>
|
</audio>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user