diff --git a/software/src/app.py b/software/src/app.py index c2f79f3..72d10c2 100644 --- a/software/src/app.py +++ b/software/src/app.py @@ -193,3 +193,6 @@ class PlayerApp: def get_nfc(self): return self.nfc + + def get_playlist_db(self): + return self.playlist_db diff --git a/software/src/utils/playlistdb.py b/software/src/utils/playlistdb.py index 296461b..546311b 100644 --- a/software/src/utils/playlistdb.py +++ b/software/src/utils/playlistdb.py @@ -248,6 +248,16 @@ class BTreeDB(IPlaylistDB): if 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): """ 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) return self.getPlaylistForTag(tag) + def deletePlaylistForTag(self, tag: bytes): + self._deletePlaylist(tag) + def validate(self, dump=False): """ Validate the structure of the playlist database. diff --git a/software/src/webserver.py b/software/src/webserver.py index 5d6aec4..5af66fa 100644 --- a/software/src/webserver.py +++ b/software/src/webserver.py @@ -4,6 +4,8 @@ Copyright (c) 2024-2025 Stefan Kratochwil ''' import asyncio +import json +import os from microdot import Microdot, redirect, send_file @@ -12,14 +14,16 @@ server = None config = None app = None nfc = None +playlist_db = None 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)) config = config_ app = app_ nfc = app.get_nfc() + playlist_db = app.get_playlist_db() @webapp.before_request @@ -90,3 +94,88 @@ async def static(request, path): # directory traversal is not allowed return 'Not found', 404 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/', 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/', 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/', 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'}