Skip to content

Commit

Permalink
feat: rewrite playlist urls
Browse files Browse the repository at this point in the history
Also allows navigating media within the browser or rather provides directory listings html
  • Loading branch information
mgoltzsche committed Feb 21, 2024
1 parent 34d6bd7 commit 1e55715
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 27 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
```

38 changes: 38 additions & 0 deletions beetsplug/webm3u/playlist.py
Original file line number Diff line number Diff line change
@@ -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
62 changes: 43 additions & 19 deletions beetsplug/webm3u/routes.py
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -14,53 +15,76 @@ 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/<path:path>')
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/<path:path>')
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,
'path': 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))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,30 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playlists</title>
<title>{{title}}</title>
<link rel="stylesheet" type="text/css" href= "{{ url_for('static',filename='style.css') }}">
</head>
<body>
<h1>Playlists</h1>
<h1>{{title}}</h1>

<p>
<a href="javascript:history.back()">&#129176; back</a>
</p>

<ul>
{% if path %}
<li>
<a href="../">..</a>
</li>
{% endif %}
{% for dir in directories %}
<li>
<a href="{{ dir }}/">&#128193; {{ dir }}</a>
</li>
{% endfor %}
{% for playlist in playlists %}
{% for file in files %}
<li>
<a href="{{ playlist.path }}">&#127925; {{ playlist.name }} <span>({{ humanize(playlist.size) }})</span></a>
<a href="{{ file.path }}">&#127925; {{ file.name }} <span>({{ humanize(file.size) }})</span></a>
</li>
{% endfor %}
</ul>
</body>
</html>
</html>

0 comments on commit 1e55715

Please sign in to comment.