From 1e55715d98fe51c2813a697dce6a18147c6b8b0d Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Wed, 21 Feb 2024 04:51:06 +0100 Subject: [PATCH] feat: rewrite playlist urls Also allows navigating media within the browser or rather provides directory listings html --- README.md | 8 ++- beetsplug/webm3u/playlist.py | 38 ++++++++++++ beetsplug/webm3u/routes.py | 62 +++++++++++++------ .../templates/{playlists.html => list.html} | 12 ++-- 4 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 beetsplug/webm3u/playlist.py rename beetsplug/webm3u/templates/{playlists.html => list.html} (69%) diff --git a/README.md b/README.md index 251b45b..57ba6f6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Once the `webm3u` plugin is enabled within your beets configuration, you can run beet webm3u ``` -Once the server started, you can browse [`http://localhost:8339`](http://localhost:8339). +You can browse the server at [`http://localhost:8339`](http://localhost:8339). ### CLI @@ -68,3 +68,9 @@ make beets-sh A temporary beets library is written to `./data`. It can be removed by calling `make clean-data`. + +To just start the server, run: +```sh +make beets-webm3u +``` + diff --git a/beetsplug/webm3u/playlist.py b/beetsplug/webm3u/playlist.py new file mode 100644 index 0000000..14a1a1d --- /dev/null +++ b/beetsplug/webm3u/playlist.py @@ -0,0 +1,38 @@ +import re + +def parse_playlist(filepath): + # CAUTION: attribute values that contain ',' or ' ' are not supported + extinf_regex = re.compile(r'^#EXTINF:([0-9]+)( [^,]+)?,[\s]*(.*)') + with open(filepath, 'r', encoding='UTF-8') as file: + linenum = 0 + item = PlaylistItem() + while line := file.readline(): + line = line.rstrip() + linenum += 1 + if linenum == 1: + assert line == '#EXTM3U', 'File is not an EXTM3U playlist!' + continue + if len(line.strip()) == 0: + continue + m = extinf_regex.match(line) + if m: + item = PlaylistItem() + duration = m.group(1) + item.duration = int(duration) + attrs = m.group(2) + if attrs: + item.attrs = {k: v.strip('"') for k,v in [kv.split('=') for kv in attrs.strip().split(' ')]} + item.title = m.group(3) + continue + if line.startswith('#'): + continue + item.uri = line + yield item + item = PlaylistItem() + +class PlaylistItem(): + def __init__(self): + self.title = None + self.duration = None + self.uri = None + self.attrs = None diff --git a/beetsplug/webm3u/routes.py b/beetsplug/webm3u/routes.py index 7e1c58c..04f5f9f 100644 --- a/beetsplug/webm3u/routes.py +++ b/beetsplug/webm3u/routes.py @@ -1,7 +1,8 @@ import os -from flask import Flask, Blueprint, send_from_directory, send_file, abort, render_template, request, jsonify +from flask import Flask, Blueprint, send_from_directory, send_file, abort, render_template, request, url_for, jsonify from beets import config from pathlib import Path +from beetsplug.webm3u.playlist import parse_playlist MIMETYPE_HTML = 'text/html' MIMETYPE_JSON = 'application/json' @@ -14,43 +15,66 @@ def playlists(path): root_dir = config['webm3u']['playlist_dir'].get() if not root_dir: root_dir = config['smartplaylist']['playlist_dir'].get() + return _serve_files('Playlists', root_dir, path, _filter_m3u_files, _send_playlist) + +@bp.route('/music/', defaults={'path': ''}) +@bp.route('/music/') +def music(path): + root_dir = config['directory'].get() + return _serve_files('Files', root_dir, path, _filter_none, _send_file) + +def _send_file(filepath): + return send_file(filepath) + +def _send_playlist(filepath): + items = [_rewrite(item) for item in parse_playlist(filepath)] + lines = ['#EXTINF:{},{}\n{}'.format(i.duration, i.title, i.uri) for i in items] + return '#EXTM3U\n'+('\n'.join(lines)) + +def _rewrite(item): + path = url_for('webm3u_bp.music', path=item.uri) + path = os.path.normpath(path) + item.uri = '{}{}'.format(request.host_url.rstrip('/'), path) + return item + +def _filter_m3u_files(filename): + return filename.endswith('.m3u') or filename.endswith('.m3u8') + +def _filter_none(filename): + return True + +def _serve_files(title, root_dir, path, filter, handler): abs_path = os.path.join(root_dir, path) _check_path(root_dir, abs_path) if not os.path.exists(abs_path): return abort(404) if os.path.isfile(abs_path): # TODO: transform item URIs within playlist - return send_file(abs_path) + return handler(abs_path) else: - pl = _playlists(abs_path) + f = _files(abs_path, filter) dirs = _directories(abs_path) mimetypes = (MIMETYPE_JSON, MIMETYPE_HTML) mimetype = request.accept_mimetypes.best_match(mimetypes, MIMETYPE_JSON) if mimetype == MIMETYPE_HTML: - return render_template('playlists.html', - path=path, - playlists=pl, + return render_template('list.html', + title=title, + files=f, directories=dirs, humanize=_humanize_size, ) else: return jsonify({ 'directories': [{'name': d} for d in dirs], - 'playlists': pl, + 'files': f, }) -@bp.route('/music/', defaults={'path': ''}) -@bp.route('/music/') -def music(path): - root_dir = config['directory'].get() - return send_from_directory(root_dir, path) - -def _playlists(dir): - l = [f for f in os.listdir(dir) if _is_playlist(dir, f)] +def _files(dir, filter): + l = [f for f in os.listdir(dir) if _is_file(dir, f) and filter(f)] l.sort() - return [_playlist_dto(dir, f) for f in l] + return [_file_dto(dir, f) for f in l] -def _playlist_dto(dir, filename): +def _file_dto(dir, filename): st = os.stat(os.path.join(dir, filename)) return { 'name': Path(filename).stem, @@ -58,9 +82,9 @@ def _playlist_dto(dir, filename): 'size': st.st_size, } -def _is_playlist(dir, filename): +def _is_file(dir, filename): f = os.path.join(dir, filename) - return os.path.isfile(f) and (f.endswith('.m3u') or f.endswith('.m3u8')) + return os.path.isfile(f) def _directories(dir): l = [d for d in os.listdir(dir) if os.path.isdir(_join(dir, d))] diff --git a/beetsplug/webm3u/templates/playlists.html b/beetsplug/webm3u/templates/list.html similarity index 69% rename from beetsplug/webm3u/templates/playlists.html rename to beetsplug/webm3u/templates/list.html index c2da6ac..51145c9 100644 --- a/beetsplug/webm3u/templates/playlists.html +++ b/beetsplug/webm3u/templates/list.html @@ -3,32 +3,30 @@ - Playlists + {{title}} -

Playlists

+

{{title}}

🢘 back

- \ No newline at end of file +