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 ## Requirements
Python 3.6+ Python 3.6+
FFmpeg compiled with `--enable-libopus` FFmpeg compiled with `--enable-libopus`
Python packages: `flask gunicorn mutagen` Python packages: `flask gunicorn mutagen Flask-RESTful`
## Install ## Install
1. Get on the floor 1. Get on the floor

View File

@ -3,13 +3,13 @@
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 flask import Flask, Response, render_template, send_file from flask import Flask, Response, render_template, send_file
from flask_restful import reqparse, abort, Api, Resource
import mutagen import mutagen
MUSIC_DIR = "/mnt/music/Music" MUSIC_DIR = "/mnt/music/Music"
@ -27,7 +27,8 @@ FFMPEG_CMD = [
class Track: class Track:
def __init__(self, filepath=None, d=None): def __init__(self, filepath=None, d=None):
if d: if d:
self._from_dict(d) for attr, value in d.items():
setattr(self, attr, value)
return return
if filepath.endswith("mp3"): if filepath.endswith("mp3"):
@ -36,20 +37,12 @@ class Track:
m = mutagen.File(filepath) m = mutagen.File(filepath)
self.title = m.get('title', [''])[0] self.title = m.get('title', [''])[0]
self.artist = m.get('artist', [''])[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.date = m.get('date', [''])[0]
self.length = str(int(m.info.length) // 60) + ":" self.length = str(int(m.info.length) // 60) + ":"
self.length += str(int(m.info.length) % 60) self.length += str(int(m.info.length) % 60)
self.filepath = filepath 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): def build_library(root_dir):
"""Walks the music directory and builds a library of tracks.""" """Walks the music directory and builds a library of tracks."""
@ -64,6 +57,7 @@ def build_library(root_dir):
tracks.append(track) tracks.append(track)
return tracks return tracks
def init_library(): def init_library():
"""Loads the library from file, or builds a new one if one isn't found.""" """Loads the library from file, or builds a new one if one isn't found."""
if os.path.isfile("library.json"): if os.path.isfile("library.json"):
@ -73,23 +67,78 @@ def init_library():
else: else:
tracks = build_library(MUSIC_DIR) tracks = build_library(MUSIC_DIR)
with open("library.json", "w") as file: 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 return tracks
app = Flask(__name__) app = Flask(__name__)
api = Api(app)
tracks = init_library() tracks = init_library()
@app.route('/') @app.route('/')
def index(): def index():
"""Main index page.""" """Main index page."""
nav_items = list(set(t.artist for t in tracks)) artist = list(set(t.artist for t in tracks))
nav_items.sort() artist.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()
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>') @app.route('/stream/<path:track>')
def stream(track): def stream(track):
"""View for the raw audio file.""" """View for the raw audio file."""

View File

@ -21,16 +21,41 @@ 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;
} }
#currentDirectory { #selectArtistContainer {
padding-left: 0.5em; height: 100%;
width: 100%;
} }
#navItems { #selectArtist {
list-style-type: none; 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 { #playerContainer {

View File

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