Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
7643a13b54 | |||
f171721915 | |||
89f6f3a1d0 | |||
b60cc386dc | |||
265617ee6f | |||
09823d4ebe | |||
646f660227 | |||
77e8fd2c8b | |||
a7ae72c82a | |||
38808dba67 | |||
ec89df7a88 | |||
46ced5572d | |||
9152ed1c83 | |||
0233cfc09e |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@ __pycache__/
|
|||
*.swp
|
||||
*.swo
|
||||
*.json
|
||||
*.db
|
||||
config.py
|
||||
|
|
15
LICENSE
Normal file
15
LICENSE
Normal file
|
@ -0,0 +1,15 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2019, iou1name <iou1name@steelbea.me>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -4,7 +4,7 @@ Stream some music.
|
|||
## Requirements
|
||||
Python 3.6+
|
||||
FFmpeg compiled with `--enable-libopus`
|
||||
Python packages: `flask gunicorn mutagen Flask-RESTful`
|
||||
Python packages: `flask gunicorn mutagen`
|
||||
|
||||
## Install
|
||||
1. Get on the floor
|
||||
|
|
107
buckler_flask.py
Normal file
107
buckler_flask.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Session interface middlewares to integrate the flask app with Buckler.
|
||||
"""
|
||||
import json
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
from flask.sessions import SessionInterface, SessionMixin
|
||||
from flask import session, redirect, request
|
||||
|
||||
import config
|
||||
|
||||
class BucklerSessionInterface(SessionInterface):
|
||||
"""
|
||||
Queries the Buckler server for session data to the current user and
|
||||
application.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.url = config.buckler['url']
|
||||
self.app_id = config.buckler['app_id']
|
||||
self.app_key = config.buckler['app_key']
|
||||
|
||||
def open_session(self, app, request):
|
||||
"""Called when a request is initiated."""
|
||||
user_id = request.cookies.get('userid')
|
||||
user_sid = request.cookies.get('session')
|
||||
|
||||
params = {
|
||||
'app_id': self.app_id,
|
||||
'app_key': self.app_key,
|
||||
'userid': user_id,
|
||||
'session': user_sid
|
||||
}
|
||||
params = urllib.parse.urlencode(params)
|
||||
req = urllib.request.Request(self.url + f"/get_session?{params}")
|
||||
res = urllib.request.urlopen(req)
|
||||
data = json.loads(res.read())
|
||||
if data.get('error'):
|
||||
return None
|
||||
session = BucklerSession()
|
||||
session.update(data['session_data'])
|
||||
session.meta = data['meta']
|
||||
session.cookies = request.cookies
|
||||
return session
|
||||
|
||||
def save_session(self, app, session, response):
|
||||
"""Called at the end of a request."""
|
||||
if not session.modified:
|
||||
return
|
||||
user_id = session.meta.get('user_id')
|
||||
user_sid = session.meta.get('user_sid')
|
||||
|
||||
params = {
|
||||
'app_id': self.app_id,
|
||||
'app_key': self.app_key,
|
||||
'userid': user_id,
|
||||
'session': user_sid
|
||||
}
|
||||
params = urllib.parse.urlencode(params)
|
||||
data = json.dumps(session)
|
||||
req = urllib.request.Request(
|
||||
self.url + f"/set_session?{params}",
|
||||
data=data.encode('utf8'),
|
||||
method='POST')
|
||||
res = urllib.request.urlopen(req)
|
||||
|
||||
last_used = datetime.fromisoformat(session.meta['last_used'])
|
||||
now = datetime.now(last_used.tzinfo)
|
||||
delta = now - last_used
|
||||
if delta.seconds > 600:
|
||||
response.set_cookie(
|
||||
'userid',
|
||||
session.cookies['userid'],
|
||||
max_age=30*24*60*60,
|
||||
secure=True,
|
||||
httponly=True)
|
||||
response.set_cookie(
|
||||
'session',
|
||||
session.cookies['session'],
|
||||
max_age=30*24*60*60,
|
||||
secure=True,
|
||||
httponly=True)
|
||||
|
||||
|
||||
class BucklerSession(dict, SessionMixin):
|
||||
"""A server side session class based on the Buckler security shield."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.modified = False
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super().__setitem__(key, value)
|
||||
self.modified = True
|
||||
|
||||
|
||||
def require_auth():
|
||||
"""
|
||||
Requires the user to be properly authenticated with Buckler before
|
||||
accessing any views on the application.
|
||||
"""
|
||||
if not hasattr(session, 'meta'):
|
||||
resp = redirect(config.buckler['login_url'])
|
||||
resp.set_cookie('redirect', request.url)
|
||||
return resp
|
11
config.py.template
Executable file
11
config.py.template
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Configuration settings for Musik.
|
||||
`buckler` specifies settings pertaining to the Buckler server.
|
||||
"""
|
||||
buckler = {
|
||||
'url': "http://127.0.0.1:5400/buckler",
|
||||
'app_id': 1,
|
||||
'app_key': """password""",
|
||||
'login_url': "/buckler/login",
|
||||
}
|
194
database.py
Normal file
194
database.py
Normal file
|
@ -0,0 +1,194 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
API for interacting with the Musik database.
|
||||
"""
|
||||
import os
|
||||
import sqlite3
|
||||
import multiprocessing
|
||||
|
||||
import mutagen
|
||||
import mutagen.mp3
|
||||
|
||||
MUSIC_DIR = "/home/iou1name/music/Music"
|
||||
MUSIC_EXT = ['flac', 'mp3', 'wav', 'm4a']
|
||||
DB_LOCK = multiprocessing.Lock()
|
||||
|
||||
def db_execute(*args, **kwargs):
|
||||
"""
|
||||
Opens a connection to the app's database and executes the SQL statements
|
||||
passed to this function.
|
||||
"""
|
||||
with sqlite3.connect('musik.db') as con:
|
||||
DB_LOCK.acquire()
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
res = cur.execute(*args, **kwargs)
|
||||
DB_LOCK.release()
|
||||
return res
|
||||
|
||||
def db_execute_many(*args, **kwargs):
|
||||
"""
|
||||
Same as `db_executei()` except with `cur.executemany()`.
|
||||
"""
|
||||
with sqlite3.connect('musik.db') as con:
|
||||
DB_LOCK.acquire()
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
res = cur.executemany(*args, **kwargs)
|
||||
DB_LOCK.release()
|
||||
return res
|
||||
|
||||
def init_database():
|
||||
"""
|
||||
Initializes the database.
|
||||
"""
|
||||
try:
|
||||
db_execute("SELECT * FROM tracks LIMIT 1").fetchone()
|
||||
except sqlite3.OperationalError:
|
||||
DB_LOCK.release()
|
||||
db_execute("CREATE TABLE tracks("
|
||||
"filepath TEXT PRIMARY KEY,"
|
||||
"artist TEXT,"
|
||||
"albumartist TEXT,"
|
||||
"date TEXT,"
|
||||
"album TEXT,"
|
||||
"discnumber TEXT,"
|
||||
"tracknumber TEXT,"
|
||||
"title TEXT,"
|
||||
"genre TEXT,"
|
||||
"length TEXT,"
|
||||
"last_modified INTEGER"
|
||||
")"
|
||||
)
|
||||
build_library(MUSIC_DIR)
|
||||
|
||||
|
||||
def build_library(root_dir):
|
||||
"""Walks the music directory and builds a library of tracks."""
|
||||
print("Building library")
|
||||
filepaths = []
|
||||
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
|
||||
filepath = os.path.join(root_dir, dir_name, file)
|
||||
last_modified = os.path.getmtime(filepath)
|
||||
filepaths.append((filepath, last_modified))
|
||||
tracks_prev = db_execute("SELECT * FROM tracks")
|
||||
tracks_prev = {track['filepath']: track for track in tracks_prev}
|
||||
|
||||
global worker
|
||||
def worker(tup):
|
||||
"""Worker for multi-processing tracks."""
|
||||
filepath, last_modified = tup
|
||||
track_prev = tracks_prev.get(filepath)
|
||||
if track_prev:
|
||||
if track_prev['last_modified'] >= last_modified:
|
||||
return tuple(track_prev)
|
||||
data = read_track(filepath)
|
||||
return data
|
||||
|
||||
with multiprocessing.Pool() as pool:
|
||||
mapping = pool.imap(worker, filepaths)
|
||||
tracks = []
|
||||
prev_percent = 0
|
||||
while True:
|
||||
try:
|
||||
tracks.append(mapping.next())
|
||||
except StopIteration:
|
||||
break
|
||||
percent = round(len(tracks) / len(filepaths) * 100, 2)
|
||||
if percent >= prev_percent + 2.5:
|
||||
print(f"{percent}%")
|
||||
prev_percent = percent
|
||||
db_execute("DELETE FROM tracks")
|
||||
db_execute_many(
|
||||
"INSERT INTO tracks VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
tracks
|
||||
)
|
||||
print("Done")
|
||||
|
||||
|
||||
def read_track(filepath):
|
||||
"""
|
||||
Reads the specified file and extracts relevant information from it.
|
||||
Returns a tuple.
|
||||
"""
|
||||
if filepath.endswith("mp3"):
|
||||
m = mutagen.mp3.EasyMP3(filepath)
|
||||
else:
|
||||
m = mutagen.File(filepath)
|
||||
artist = m.get('artist', [''])[0]
|
||||
if m.get('albumartist'):
|
||||
albumartist = m.get('albumartist', [''])[0]
|
||||
else:
|
||||
albumartist = m.get('artist', [''])[0]
|
||||
date = m.get('date', [''])[0]
|
||||
album = m.get('album', [''])[0]
|
||||
discnumber = m.get('discnumber', [''])[0]
|
||||
tracknumber = m.get('tracknumber', [''])[0]
|
||||
title = m.get('title', [''])[0]
|
||||
genre = m.get('genre', [''])[0]
|
||||
length = str(int(m.info.length) // 60) + ":"
|
||||
length += str(int(m.info.length) % 60)
|
||||
last_modified = os.path.getmtime(filepath)
|
||||
|
||||
d = locals()
|
||||
d.pop('m')
|
||||
return tuple(d.values())
|
||||
|
||||
|
||||
def get_artist_albums(artist):
|
||||
"""
|
||||
Returns a list of all albums produced by the specified `artist`.
|
||||
"""
|
||||
albums = db_execute(
|
||||
"SELECT DISTINCT album FROM tracks WHERE albumartist = ? ORDER BY date ASC",
|
||||
(artist,)
|
||||
).fetchall()
|
||||
albums = [row[0] for row in albums]
|
||||
return albums
|
||||
|
||||
def get_album_tracks(artist, album):
|
||||
"""
|
||||
Returns a list of all tracks on the `album` produced by `artist`.
|
||||
"""
|
||||
tracks = db_execute(
|
||||
"SELECT discnumber, tracknumber, title FROM tracks "
|
||||
"WHERE albumartist = ? AND album = ?"
|
||||
"ORDER BY discnumber ASC, tracknumber ASC",
|
||||
(artist, album)
|
||||
).fetchall()
|
||||
tracks = [tuple(row) for row in tracks]
|
||||
return tracks
|
||||
|
||||
def get_track(artist, album, discnumber, tracknumber):
|
||||
"""
|
||||
Returns a dictionary containing all information about a specific track.
|
||||
"""
|
||||
track = db_execute(
|
||||
"SELECT * FROM tracks "
|
||||
"WHERE albumartist = ? AND album = ? "
|
||||
"AND discnumber = ? AND tracknumber = ?",
|
||||
(artist, album, discnumber, tracknumber)
|
||||
).fetchone()
|
||||
return dict(track)
|
||||
|
||||
def get_random_track():
|
||||
"""
|
||||
Returns a random track.
|
||||
"""
|
||||
track = db_execute(
|
||||
"SELECT * FROM tracks ORDER BY random() LIMIT 1"
|
||||
).fetchone()
|
||||
return dict(track)
|
||||
|
||||
def get_all_artists():
|
||||
"""
|
||||
Returns a list of all artist names.
|
||||
"""
|
||||
artists = db_execute(
|
||||
"SELECT DISTINCT albumartist FROM tracks ORDER BY albumartist ASC"
|
||||
)
|
||||
artists = [row[0] for row in artists]
|
||||
return artists
|
254
musik.py
254
musik.py
|
@ -8,194 +8,84 @@ import random
|
|||
import subprocess
|
||||
from urllib import parse
|
||||
|
||||
from flask import Flask, Response, render_template, send_file, url_for
|
||||
from flask_restful import reqparse, abort, Api, Resource
|
||||
import mutagen
|
||||
from flask import Flask, Response, render_template, send_file, url_for, request, abort
|
||||
|
||||
import database
|
||||
import buckler_flask
|
||||
|
||||
MUSIC_DIR = "/home/iou1name/music/Music"
|
||||
MUSIC_EXT = ['flac', 'mp3', 'wav', 'm4a']
|
||||
FFMPEG_CMD = [
|
||||
'ffmpeg', '-y',
|
||||
'-loglevel', 'panic',
|
||||
'-i', '',
|
||||
'-codec:a', 'libopus',
|
||||
'-b:a', '64k',
|
||||
'-f', 'opus',
|
||||
'-'
|
||||
]
|
||||
|
||||
class Track:
|
||||
def __init__(self, filepath=None, d=None, coverart=""):
|
||||
if d:
|
||||
for attr, value in d.items():
|
||||
setattr(self, attr, value)
|
||||
return
|
||||
|
||||
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
|
||||
filepath = os.path.join(root_dir, dir_name, file)
|
||||
if "folder.jpg" in files:
|
||||
coverart = os.path.join(root_dir, dir_name, "folder.jpg")
|
||||
else:
|
||||
coverart = ""
|
||||
track = Track(filepath, coverart=coverart)
|
||||
tracks.append(track)
|
||||
print("Done")
|
||||
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__)
|
||||
api = Api(app)
|
||||
tracks = init_library()
|
||||
app.session_interface = buckler_flask.BucklerSessionInterface()
|
||||
app.before_request(buckler_flask.require_auth)
|
||||
database.init_database()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Main index page."""
|
||||
artists = list(set(t.artist for t in tracks))
|
||||
artists.sort()
|
||||
artists = database.get_all_artists()
|
||||
return render_template('index.html', **locals())
|
||||
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('artist')
|
||||
parser.add_argument('album')
|
||||
parser.add_argument('track')
|
||||
@app.route('/select')
|
||||
def select():
|
||||
"""Retrieve information about an artist, album or track."""
|
||||
artist = request.args.get('artist')
|
||||
album = request.args.get('album')
|
||||
discnumber = request.args.get('discnumber')
|
||||
tracknumber = request.args.get('tracknumber')
|
||||
|
||||
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."""
|
||||
artist, album, track = map(parse.unquote, (artist, album, track))
|
||||
for t in tracks:
|
||||
if (t.artist == artist and
|
||||
t.album == album and
|
||||
t.title == track):
|
||||
break
|
||||
if tracknumber:
|
||||
result = database.get_track(artist, album, discnumber, tracknumber)
|
||||
if not result:
|
||||
return ("Valid artist, album, discnumber and tracknumber "
|
||||
"parameters must be supplied.")
|
||||
else:
|
||||
result.pop('filepath')
|
||||
elif album:
|
||||
result = database.get_album_tracks(artist, album)
|
||||
if not result:
|
||||
return "Valid artist and album parameters must be supplied."
|
||||
elif artist:
|
||||
result = database.get_artist_albums(artist)
|
||||
if not result:
|
||||
return "Valid artist parameter must be supplied."
|
||||
else:
|
||||
abort(404, message="Track does not exist.")
|
||||
return "Invalid query."
|
||||
return json.dumps(result)
|
||||
|
||||
FFMPEG_CMD[5] = t.filepath
|
||||
|
||||
@app.route('/select/random')
|
||||
def select_random():
|
||||
"""
|
||||
Selects a random track.
|
||||
"""
|
||||
track = database.get_random_track()
|
||||
track.pop('filepath')
|
||||
return json.dumps(track)
|
||||
|
||||
|
||||
@app.route('/stream')
|
||||
def stream():
|
||||
"""View for the raw audio file."""
|
||||
artist = parse.unquote(request.args.get('artist'))
|
||||
album = parse.unquote(request.args.get('album'))
|
||||
discnumber = parse.unquote(request.args.get('discnumber'))
|
||||
tracknumber = parse.unquote(request.args.get('tracknumber'))
|
||||
|
||||
track = database.get_track(artist, album, discnumber, tracknumber)
|
||||
if not track:
|
||||
return "Track does not exist."
|
||||
|
||||
FFMPEG_CMD[5] = track['filepath']
|
||||
|
||||
def generate():
|
||||
with subprocess.Popen(FFMPEG_CMD, stdout=subprocess.PIPE) as proc:
|
||||
|
@ -206,23 +96,27 @@ def stream(artist, album, track):
|
|||
return Response(generate(), mimetype="audio/ogg")
|
||||
|
||||
|
||||
@app.route('/coverart/<artist>/<album>/<track>')
|
||||
def coverart(artist, album, track):
|
||||
@app.route('/coverart')
|
||||
def coverart():
|
||||
"""View for the raw audio file."""
|
||||
artist, album, track = map(parse.unquote, (artist, album, track))
|
||||
for t in tracks:
|
||||
if (t.artist == artist and
|
||||
t.album == album and
|
||||
t.title == track):
|
||||
break
|
||||
artist = parse.unquote(request.args.get('artist'))
|
||||
album = parse.unquote(request.args.get('album'))
|
||||
discnumber = parse.unquote(request.args.get('discnumber'))
|
||||
tracknumber = parse.unquote(request.args.get('tracknumber'))
|
||||
|
||||
track = database.get_track(artist, album, discnumber, tracknumber)
|
||||
if not track:
|
||||
return "Track does not exist."
|
||||
|
||||
cover_path = os.path.join(os.path.dirname(track['filepath']), 'folder.jpg')
|
||||
if os.path.isfile(cover_path):
|
||||
return send_file(cover_path)
|
||||
else:
|
||||
abort(404, message="Track does not exist.")
|
||||
|
||||
if t.coverart:
|
||||
return send_file(t.coverart)
|
||||
else:
|
||||
return "False"
|
||||
return "No cover art for this track found."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) > 1:
|
||||
database.build_library(database.MUSIC_DIR)
|
||||
app.run(host='0.0.0.0', port=5150)
|
||||
|
|
|
@ -4,14 +4,13 @@ body {
|
|||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
#globalContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
#titleContainer {
|
||||
|
@ -19,11 +18,7 @@ img {
|
|||
}
|
||||
|
||||
#navigationContainer {
|
||||
flex: auto;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
border-top: 2px solid #ccc;
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
@ -34,22 +29,15 @@ img {
|
|||
}
|
||||
|
||||
#playerContainer {
|
||||
height: 30vh;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#albumCover {
|
||||
padding-right: 1em;
|
||||
padding-left: 1em;
|
||||
padding-bottom: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#playerControls {
|
||||
text-align: center;
|
||||
padding-right: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
|
101
static/musik.js
101
static/musik.js
|
@ -3,73 +3,83 @@ function load() {
|
|||
document.getElementById('player').addEventListener('ended', function() {
|
||||
// shuffle
|
||||
if (document.getElementById('shuffle').checked) {
|
||||
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);
|
||||
};
|
||||
httpRequest.open('GET', api_uri + '/random', true);
|
||||
httpRequest.send();
|
||||
fetch(api_uri + '/random').then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(json) {
|
||||
change_track(json);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function select_artist(select) {
|
||||
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);
|
||||
let params = {
|
||||
artist: select.value,
|
||||
};
|
||||
let query = Object.keys(params)
|
||||
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&');
|
||||
fetch(api_uri + '?' + query).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(json) {
|
||||
let html_str = '';
|
||||
for (let i = 0; i < nav_items.length; i++) {
|
||||
html_str += '<option value="' + nav_items[i] + '">' + nav_items[i] + '</option>';
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
html_str += '<option value="' + json[i] + '">' + json[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) {
|
||||
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);
|
||||
let params = {
|
||||
artist: document.getElementById('artistList').value,
|
||||
album: select.value,
|
||||
};
|
||||
let query = Object.keys(params)
|
||||
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&');
|
||||
fetch(api_uri + '?' + query).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(json) {
|
||||
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>';
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
html_str += '<option data-discnumber="' + json[i][0] + '" data-tracknumber="' + json[i][1]+ '" value="' + json[i][2] + '">' + json[i][0] + '.' + json[i][1] + ' - ' + json[i][2] + '</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);
|
||||
let params = {
|
||||
artist: document.getElementById('artistList').value,
|
||||
album: document.getElementById('albumList').value,
|
||||
discnumber: select.selectedOptions[0].dataset.discnumber,
|
||||
tracknumber: select.selectedOptions[0].dataset.tracknumber,
|
||||
};
|
||||
httpRequest.open('GET', api_uri + '?artist=' + document.getElementById('artistList').value + '&album=' + document.getElementById('albumList').value + '&track=' + select.value, true);
|
||||
httpRequest.send();
|
||||
let query = Object.keys(params)
|
||||
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&');
|
||||
fetch(api_uri + '?' + query).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(json) {
|
||||
change_track(json);
|
||||
});
|
||||
}
|
||||
|
||||
function change_track(track) {
|
||||
let params = {
|
||||
artist: track.albumartist,
|
||||
album: track.album,
|
||||
discnumber: track.discnumber,
|
||||
tracknumber: track.tracknumber,
|
||||
};
|
||||
let query = Object.keys(params)
|
||||
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&');
|
||||
|
||||
let source = document.getElementById('stream');
|
||||
source.src = track.streampath;
|
||||
source.src = document.location.href + '/stream?' + query;
|
||||
let player = document.getElementById('player');
|
||||
player.load();
|
||||
player.play();
|
||||
|
@ -77,5 +87,6 @@ function change_track(track) {
|
|||
document.getElementById('nowPlayingAlbum').innerHTML = track.album;
|
||||
document.getElementById('nowPlayingTitle').innerHTML = track.title;
|
||||
|
||||
document.getElementById('albumCover').firstChild.src = track.coverart;
|
||||
document.getElementById('albumCover').firstChild.src = document.location.href + 'coverart?' + query;
|
||||
document.title = 'Musik | ' + track.artist + ' - ' + track.title;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<link rel="stylesheet" type="text/css" href="/static/musik.css">
|
||||
<script type="text/javascript" src="/static/musik.js"></script>
|
||||
<script>
|
||||
const api_uri = "{{ url_for('selection') }}";
|
||||
const api_uri = "{{ url_for('select') }}";
|
||||
</script>
|
||||
<script>window.onload = load;</script>
|
||||
</head>
|
||||
|
@ -30,7 +30,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="playerContainer">
|
||||
<span id="albumCover"><img src=""/></span>
|
||||
<span id="albumCover"><img src=""></span>
|
||||
<span id="playerControls">
|
||||
<h4><span id="nowPlayingArtist"></span> - <span id="nowPlayingAlbum"></span></h4>
|
||||
<h3 id="nowPlayingTitle"></h3>
|
||||
|
|
Loading…
Reference in New Issue
Block a user