Musik/musik.py

258 lines
6.4 KiB
Python
Raw Normal View History

2018-09-12 13:04:00 -04:00
#!/usr/bin/env python3
"""
Music streaming.
"""
2018-09-13 14:22:27 -04:00
import os
import json
2018-09-14 08:02:24 -04:00
import random
2018-09-12 13:04:00 -04:00
import subprocess
import multiprocessing
2018-09-13 14:22:27 -04:00
from urllib import parse
2018-09-12 13:04:00 -04:00
2019-02-05 11:23:41 -05:00
from flask import Flask, Response, render_template, send_file, url_for
2019-02-04 10:06:58 -05:00
from flask_restful import reqparse, abort, Api, Resource
import mutagen
import mutagen.mp3
2018-09-12 13:04:00 -04:00
2019-02-21 12:57:58 -05:00
MUSIC_DIR = "/home/iou1name/music/Music"
2018-09-14 08:02:24 -04:00
MUSIC_EXT = ['flac', 'mp3', 'wav', 'm4a']
2018-09-12 13:04:00 -04:00
FFMPEG_CMD = [
'ffmpeg', '-y',
'-loglevel', 'panic',
'-i', '',
'-codec:a', 'libopus',
'-b:a', '64k',
'-f', 'opus',
'-'
]
class Track:
def __init__(self, filepath=None, d=None):
if d:
2019-02-01 12:59:19 -05:00
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)
2019-02-05 11:33:58 -05:00
self.tracknumber = m.get('tracknumber', [''])[0]
self.title = m.get('title', [''])[0]
if m.get('albumartist'):
self.artist = m.get('albumartist', [''])[0]
else:
self.artist = m.get('artist', [''])[0]
2019-02-01 12:59:19 -05:00
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 = os.path.join(
os.path.dirname(self.filepath), 'folder.jpg')
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)
filepaths.append(filepath)
global worker
def worker(filepath):
"""Worker for multi-processing tracks."""
track = Track(filepath)
return track
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
2019-02-05 11:33:58 -05:00
print("Done")
return tracks
2019-02-01 12:59:19 -05:00
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:
2019-02-01 12:59:19 -05:00
file.write(json.dumps([vars(t) for t in tracks]))
return tracks
2019-01-11 11:27:25 -05:00
2019-02-25 13:40:36 -05:00
def escape(string):
"""Escape things."""
string = parse.quote(string, safe='')
string = string.replace('&', '%26')
return string
2018-09-12 13:04:00 -04:00
app = Flask(__name__)
2019-02-04 10:06:58 -05:00
api = Api(app)
tracks = init_library()
2018-09-12 13:04:00 -04:00
@app.route('/')
def index():
"""Main index page."""
2019-02-05 11:23:41 -05:00
artists = list(set(t.artist for t in tracks))
artists.sort()
2018-09-13 14:22:27 -04:00
return render_template('index.html', **locals())
2018-09-12 13:04:00 -04:00
2019-02-04 10:06:58 -05:00
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()
validate_select_args(args)
if args.get('track'):
2019-02-05 11:23:41 -05:00
for track in tracks:
if (track.artist == args.get('artist') and
track.album == args.get('album') and
track.title == args.get('track')):
break
2019-02-04 10:06:58 -05:00
else:
2019-02-05 11:23:41 -05:00
abort(404, message="Track does not exist.")
found = dict(vars(track))
found.pop('filepath')
found['streampath'] = url_for(
'stream',
2019-02-25 13:40:36 -05:00
artist=escape(track.artist),
album=escape(track.album),
track=escape(track.title))
2019-02-05 13:04:10 -05:00
found['coverart'] = url_for(
'coverart',
2019-02-25 13:40:36 -05:00
artist=escape(track.artist),
album=escape(track.album),
track=escape(track.title))
2019-02-05 11:23:41 -05:00
return found
2019-02-04 10:06:58 -05:00
elif args.get('album'):
2019-02-05 11:23:41 -05:00
found = []
for track in tracks:
if (track.artist == args.get('artist') and
track.album == args.get('album')):
found.append(track)
if not found:
2019-02-04 10:06:58 -05:00
abort(404, message="Album does not exist.")
2019-02-05 11:33:58 -05:00
found = [t.tracknumber + " - " + t.title for t in found]
found.sort()
return found
2019-02-04 10:06:58 -05:00
elif args.get('artist'):
2019-02-05 11:23:41 -05:00
found = []
for track in tracks:
if track.artist == args.get('artist'):
found.append(track)
if not found:
2019-02-04 10:06:58 -05:00
abort(404, message="Artist does not exist.")
2019-02-05 11:23:41 -05:00
found = list(set(t.album for t in found))
return sorted(found)
2019-02-04 10:06:58 -05:00
2019-02-05 12:44:31 -05:00
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)
2019-02-07 15:02:54 -05:00
found['coverart'] = url_for(
'coverart',
artist=track.artist,
album=track.album,
track=track.title)
2019-02-05 12:44:31 -05:00
return found
2019-02-04 10:06:58 -05:00
api.add_resource(Selection, '/select')
2019-02-05 12:44:31 -05:00
api.add_resource(RandomSelection, '/select/random')
2019-02-05 11:23:41 -05:00
api.init_app(app)
2019-02-04 10:06:58 -05:00
2019-02-05 11:23:41 -05:00
@app.route('/stream/<artist>/<album>/<track>')
def stream(artist, album, track):
2018-09-12 13:04:00 -04:00
"""View for the raw audio file."""
2019-02-21 12:57:58 -05:00
artist, album, track = map(parse.unquote, (artist, album, track))
2019-02-05 11:23:41 -05:00
for t in tracks:
if (t.artist == artist and
t.album == album and
t.title == track):
break
else:
abort(404, message="Track does not exist.")
FFMPEG_CMD[5] = t.filepath
2018-09-12 13:04:00 -04:00
def generate():
with subprocess.Popen(FFMPEG_CMD, stdout=subprocess.PIPE) as proc:
data = proc.stdout.read(1024)
while data:
yield data
data = proc.stdout.read(1024)
return Response(generate(), mimetype="audio/ogg")
2019-02-05 13:04:10 -05:00
@app.route('/coverart/<artist>/<album>/<track>')
def coverart(artist, album, track):
"""View for the raw audio file."""
2019-02-21 12:57:58 -05:00
artist, album, track = map(parse.unquote, (artist, album, track))
2019-02-05 13:04:10 -05:00
for t in tracks:
if (t.artist == artist and
t.album == album and
t.title == track):
break
else:
abort(404, message="Track does not exist.")
if os.path.isfile(t.coverart):
2019-02-05 13:04:10 -05:00
return send_file(t.coverart)
else:
return "No cover art for this track found."
2018-09-12 13:04:00 -04:00
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5150)