7 Commits

Author SHA1 Message Date
8aea47e1ce fix: webserver: Use streaming response for filesystem listing
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m42s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-20 19:24:14 +01:00
67a3ebdfdf feat: webserver: set and delete playlists
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-20 19:23:39 +01:00
8bfc409473 Merge remote-tracking branch 'origin/more-backend-for's-frontend' into more-frontend 2025-12-20 18:21:50 +01:00
e2ca9e5139 feat: api endpoint to get playlist properties
Some checks failed
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m36s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Failing after 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s
2025-12-20 16:52:37 +01:00
070cf887ab feat(wip): api endpoint to get playlist properties
Some checks failed
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m47s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Failing after 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s
2025-12-20 15:40:52 +01:00
28846c9274 wip: api endpoint to list all available audio files
Some checks failed
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m37s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Failing after 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Iterative approach. Currently only lists mp3 files.
2025-12-20 14:40:07 +01:00
51cb2c3a68 feat: api endpoint for reading all available playlists
Some checks failed
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m36s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Failing after 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
2025-12-19 18:33:42 +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'}