Compare commits

...

2 Commits

Author SHA1 Message Date
a4af2a1fd5 added rest api 2019-02-04 10:06:58 -05:00
1d9037310e refactor Track initialization 2019-02-01 12:59:19 -05:00
4 changed files with 111 additions and 27 deletions

View File

@ -4,7 +4,7 @@ Stream some music.
## Requirements
Python 3.6+
FFmpeg compiled with `--enable-libopus`
Python packages: `flask gunicorn mutagen`
Python packages: `flask gunicorn mutagen Flask-RESTful`
## Install
1. Get on the floor

View File

@ -3,13 +3,13 @@
Music streaming.
"""
import os
import re
import json
import random
import subprocess
from urllib import parse
from flask import Flask, Response, render_template, send_file
from flask_restful import reqparse, abort, Api, Resource
import mutagen
MUSIC_DIR = "/mnt/music/Music"
@ -27,7 +27,8 @@ FFMPEG_CMD = [
class Track:
def __init__(self, filepath=None, d=None):
if d:
self._from_dict(d)
for attr, value in d.items():
setattr(self, attr, value)
return
if filepath.endswith("mp3"):
@ -36,20 +37,12 @@ class Track:
m = mutagen.File(filepath)
self.title = m.get('title', [''])[0]
self.artist = m.get('artist', [''])[0]
self.ablum = m.get('album', [''])[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
def _from_dict(self, d):
self.title = d.get('title')
self.artist = d.get('artist')
self.ablum = d.get('album')
self.date = d.get('date')
self.length = d.get('length')
self.filepath = d.get('filepath')
def build_library(root_dir):
"""Walks the music directory and builds a library of tracks."""
@ -64,6 +57,7 @@ def build_library(root_dir):
tracks.append(track)
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"):
@ -73,23 +67,78 @@ def init_library():
else:
tracks = build_library(MUSIC_DIR)
with open("library.json", "w") as file:
file.write(json.dumps([t.__dict__ for t in tracks]))
file.write(json.dumps([vars(t) for t in tracks]))
return tracks
app = Flask(__name__)
api = Api(app)
tracks = init_library()
@app.route('/')
def index():
"""Main index page."""
nav_items = list(set(t.artist for t in tracks))
nav_items.sort()
nav_items = [item + '/' for item in nav_items]
cd = "/"
artist = list(set(t.artist for t in tracks))
artist.sort()
return render_template('index.html', **locals())
parser = reqparse.RequestParser()
parser.add_argument('artist')
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()
print(args)
validate_select_args(args)
if args.get('track'):
track = [t for t in tracks
if (t.title == args.get('track') and
t.album == args.get('album') and
t.artist == args.get('artist'))]
if not track:
abort(404, message="Track does not exist.")
else:
return vars(track[0])
elif args.get('album'):
tracks = [t for t in tracks
if (t.album == args.get('album') and
t.artist == args.get('artist'))]
if not tracks:
abort(404, message="Album does not exist.")
else:
return [t.title for t in tracks]
elif args.get('artist'):
albums = list(set(t.album for t in tracks
if t.artist == args.get('artist')))
if not albums:
abort(404, message="Artist does not exist.")
else:
return sorted(albums)
api.add_resource(Selection, '/select')
@app.route('/stream/<path:track>')
def stream(track):
"""View for the raw audio file."""

View File

@ -21,16 +21,41 @@ img {
#navigationContainer {
flex: auto;
overflow: auto;
display: flex;
flex-direction: row;
height: 100%;
border-top: 2px solid #ccc;
border-bottom: 2px solid #ccc;
}
#currentDirectory {
padding-left: 0.5em;
#selectArtistContainer {
height: 100%;
width: 100%;
}
#navItems {
list-style-type: none;
#selectArtist {
height: 100%;
width: 100%;
}
#selectAlbumContainer {
height: 100%;
width: 100%;
}
#selectAlbum {
height: 100%;
width: 100%;
}
#selectTrackContainer {
height: 100%;
width: 100%;
}
#selectTrack {
height: 100%;
width: 100%;
}
#playerContainer {

View File

@ -10,12 +10,22 @@
<div id="globalContainer">
<div id="titleContainer"><h1>Musik</h1></div>
<div id="navigationContainer">
<div id="currentDirectory">{{ cd }}</div>
<ul id="navItems">
{% for item in nav_items %}
<li><a href="javascript:void(0);" onclick="navigate('{{ item }}')">{{ item }}</a></li>
<div id="selectArtistContainer">
<select id="selectArtist" size="2">
{% for artist in artists %}
<!--<li><a href="javascript:void(0);" onclick="navigate('{{ item }}')">{{ item }}</a></li>-->
<option>{{ artist }}</option>
{% endfor %}
</ul>
</select>
</div>
<div id="selectAlbumContainer">
<select id="selectAlbum" size="2">
</select>
</div>
<div id="selectTrackContainer">
<select id="selectTrack" size="2">
</select>
</div>
</div>
<div id="playerContainer">
<span id="albumCover"><img src=""/></span>