Merge branch 'mbl/backend-for-frontend' into mbl-next

This commit is contained in:
2025-12-20 20:27:58 +01:00
3 changed files with 106 additions and 1 deletions

View File

@@ -193,3 +193,6 @@ class PlayerApp:
def get_nfc(self): def get_nfc(self):
return self.nfc return self.nfc
def get_playlist_db(self):
return self.playlist_db

View File

@@ -248,6 +248,16 @@ class BTreeDB(IPlaylistDB):
if flush: if flush:
self._flush() self._flush()
def getPlaylistTags(self):
"""
Get a keys-only dict of all defined playlists. Playlists currently do not have names, but are identified by
their tag.
"""
playlist_tags = set()
for item in self.db:
playlist_tags.add(item.split(b'/')[0])
return playlist_tags
def getPlaylistForTag(self, tag: bytes): def getPlaylistForTag(self, tag: bytes):
""" """
Lookup the playlist for 'tag' and return the Playlist object. Return None if no playlist exists for the given Lookup the playlist for 'tag' and return the Playlist object. Return None if no playlist exists for the given
@@ -279,6 +289,9 @@ class BTreeDB(IPlaylistDB):
self._savePlaylist(tag, entries, persist, shuffle) self._savePlaylist(tag, entries, persist, shuffle)
return self.getPlaylistForTag(tag) return self.getPlaylistForTag(tag)
def deletePlaylistForTag(self, tag: bytes):
self._deletePlaylist(tag)
def validate(self, dump=False): def validate(self, dump=False):
""" """
Validate the structure of the playlist database. Validate the structure of the playlist database.

View File

@@ -4,6 +4,8 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
''' '''
import asyncio import asyncio
import json
import os
from microdot import Microdot, redirect, send_file from microdot import Microdot, redirect, send_file
@@ -12,14 +14,16 @@ server = None
config = None config = None
app = None app = None
nfc = None nfc = None
playlist_db = None
def start_webserver(config_, app_): def start_webserver(config_, app_):
global server, config, app, nfc global server, config, app, nfc, playlist_db
server = asyncio.create_task(webapp.start_server(port=80)) server = asyncio.create_task(webapp.start_server(port=80))
config = config_ config = config_
app = app_ app = app_
nfc = app.get_nfc() nfc = app.get_nfc()
playlist_db = app.get_playlist_db()
@webapp.before_request @webapp.before_request
@@ -90,3 +94,88 @@ async def static(request, path):
# directory traversal is not allowed # directory traversal is not allowed
return 'Not found', 404 return 'Not found', 404
return send_file('/frontend/static/' + path, max_age=86400) return send_file('/frontend/static/' + path, max_age=86400)
@webapp.route('/api/v1/playlists', methods=['GET'])
async def playlists_get(request):
return sorted(playlist_db.getPlaylistTags())
def is_hex(s):
hex_chars = '0123456789abcdef'
return all(c in hex_chars for c in s)
fsroot = b'/sd'
@webapp.route('/api/v1/playlist/<tag>', methods=['GET'])
async def playlist_get(request, tag):
if not is_hex(tag):
return 'invalid tag', 400
playlist = playlist_db.getPlaylistForTag(tag.encode())
if playlist is None:
return None, 404
return {
'shuffle': playlist.__dict__.get('shuffle'),
'persist': playlist.__dict__.get('persist'),
'paths': [(p[len(fsroot):] if p.startswith(fsroot) else p).decode()
for p in playlist.getPaths()],
}
@webapp.route('/api/v1/playlist/<tag>', methods=['PUT'])
async def playlist_put(request, tag):
if not is_hex(tag):
return 'invalid tag', 400
playlist = request.json
if 'persist' in playlist and \
playlist['persist'] not in ['no', 'track', 'offset']:
return "Invalid 'persist' setting", 400
if 'shuffle' in playlist and \
playlist['shuffle'] not in ['no', 'yes']:
return "Invalid 'shuffle' setting", 400
playlist_db.createPlaylistForTag(tag.encode(),
(fsroot + path.encode() for path in playlist.get('paths', [])),
playlist.get('persist', 'track').encode(),
playlist.get('shuffle', 'no').encode())
return '', 204
@webapp.route('/api/v1/playlist/<tag>', methods=['DELETE'])
async def playlist_delete(request, tag):
if not is_hex(tag):
return 'invalid tag', 400
playlist_db.deletePlaylistForTag(tag.encode())
return '', 204
@webapp.route('/api/v1/audiofiles', methods=['GET'])
async def audiofiles_get(request):
def directory_iterator():
yield '['
first = True
dirstack = [fsroot]
while dirstack:
current_dir = dirstack.pop()
for entry in os.ilistdir(current_dir):
name = entry[0]
type_ = entry[1]
current_path = current_dir + b'/' + name
if type_ == 0x4000:
dirstack.append(current_path)
elif type_ == 0x8000:
if name.lower().endswith('.mp3'):
jsonpath = json.dumps(current_path[len(fsroot):])
if not first:
yield ','+jsonpath
else:
yield jsonpath
first = False
yield ']'
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}