feat: Add names to playlists
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m38s
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 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s

Fixes #63.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
This commit is contained in:
2025-12-23 13:25:39 +01:00
parent 94a8c3d720
commit bdde8cb4c2
3 changed files with 47 additions and 18 deletions

View File

@@ -213,6 +213,10 @@
<option value="audioplay">Audioplay (no shuffle, start at previous track)</option> <option value="audioplay">Audioplay (no shuffle, start at previous track)</option>
</select> </select>
</div> </div>
<div>
<label>Playlist name</label>
<input type="text" placeholder="Playlist name" id="playlist-edit-name" />
</div>
<div class="flex-horizontal"> <div class="flex-horizontal">
<div style="flex-grow: 1"> <div style="flex-grow: 1">
<label>Tracks</label> <label>Tracks</label>
@@ -493,7 +497,7 @@
document.getElementById('playlist-exist-button-delete') document.getElementById('playlist-exist-button-delete')
.addEventListener('click', (e) => { .addEventListener('click', (e) => {
if (lastSelected === null) return; if (lastSelected === null) return;
const tagid = lastSelected.innerText; const tagid = lastSelected.getAttribute('data-tag');
if(confirm(`Really delete playlist ${tagid}?`)) { if(confirm(`Really delete playlist ${tagid}?`)) {
fetch(`/api/v1/playlist/${tagid}`, { fetch(`/api/v1/playlist/${tagid}`, {
method: 'DELETE'}) method: 'DELETE'})
@@ -504,7 +508,7 @@
.addEventListener('click', selectLastTag); .addEventListener('click', selectLastTag);
document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => { document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => {
if (lastSelected !== null) if (lastSelected !== null)
showScreen("playlist_edit", {load: lastSelected.innerText}); showScreen("playlist_edit", {load: lastSelected.getAttribute('data-tag')});
}); });
document.getElementById('playlist-exist-list').addEventListener("click", (e) => { document.getElementById('playlist-exist-list').addEventListener("click", (e) => {
const node = e.target.closest("li"); const node = e.target.closest("li");
@@ -559,8 +563,9 @@
container.innerHTML = ""; container.innerHTML = "";
for (const playlist of playlists) { for (const playlist of playlists) {
const li = document.createElement("li"); const li = document.createElement("li");
li.innerHTML = playlist; li.innerHTML = `${playlist.name} (${playlist.tag})`;
li.className = "node" li.className = "node"
li.setAttribute('data-tag', playlist.tag);
container.appendChild(li) container.appendChild(li)
} }
} }
@@ -585,7 +590,7 @@
document.getElementById('playlist-exist-list') document.getElementById('playlist-exist-list')
.querySelectorAll("li") .querySelectorAll("li")
.forEach(n => { .forEach(n => {
if (n.innerText == tagtext) { if (n.getAttribute('data-tag') == tagtext) {
n.classList.add("selected"); n.classList.add("selected");
lastSelected = n; lastSelected = n;
} else { } else {
@@ -687,6 +692,8 @@
} else if (playlist.persist === "track" && playlist.shuffle === "no") { } else if (playlist.persist === "track" && playlist.shuffle === "no") {
playlisttype.value = "audioplay"; playlisttype.value = "audioplay";
} }
const playlistname = document.getElementById('playlist-edit-name');
playlistname.value = playlist.name;
} }
async function save() { async function save() {
@@ -709,6 +716,8 @@
playlistData.shuffle = "no"; playlistData.shuffle = "no";
break; break;
} }
const playlistname = document.getElementById('playlist-edit-name');
playlistData.name = playlistname.value;
const container = document.getElementById('playlist-edit-list'); const container = document.getElementById('playlist-edit-list');
playlistData.paths = [...container.querySelectorAll("li")].map((node) => node.innerText); playlistData.paths = [...container.querySelectorAll("li")].map((node) => node.innerText);
const saveRes = await fetch(`/api/v1/playlist/${playlistId}`, const saveRes = await fetch(`/api/v1/playlist/${playlistId}`,

View File

@@ -33,12 +33,13 @@ class BTreeDB(IPlaylistDB):
PERSIST_OFFSET = b'offset' PERSIST_OFFSET = b'offset'
class Playlist(IPlaylist): class Playlist(IPlaylist):
def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle): def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle, name):
self.parent = parent self.parent = parent
self.tag = tag self.tag = tag
self.pos = pos self.pos = pos
self.persist = persist self.persist = persist
self.shuffle = shuffle self.shuffle = shuffle
self.name = name
self.length = self.parent._getPlaylistLength(self.tag) self.length = self.parent._getPlaylistLength(self.tag)
self._shuffle() self._shuffle()
@@ -168,6 +169,10 @@ class BTreeDB(IPlaylistDB):
return (b''.join([tag, b'/playlist/']), return (b''.join([tag, b'/playlist/']),
b''.join([tag, b'/playlist0'])) b''.join([tag, b'/playlist0']))
@staticmethod
def _keyPlaylistName(tag):
return b''.join([tag, b'/playlistname'])
def _flush(self): def _flush(self):
""" """
Flush the database and call the flush_func if it was provided. Flush the database and call the flush_func if it was provided.
@@ -222,12 +227,13 @@ class BTreeDB(IPlaylistDB):
raise RuntimeError("Malformed playlist key") raise RuntimeError("Malformed playlist key")
return int(elements[2])+1 return int(elements[2])+1
def _savePlaylist(self, tag, entries, persist, shuffle, flush=True): def _savePlaylist(self, tag, entries, persist, shuffle, name, flush=True):
self._deletePlaylist(tag, False) self._deletePlaylist(tag, False)
for idx, entry in enumerate(entries): for idx, entry in enumerate(entries):
self.db[self._keyPlaylistEntry(tag, idx)] = entry self.db[self._keyPlaylistEntry(tag, idx)] = entry
self.db[self._keyPlaylistPersist(tag)] = persist self.db[self._keyPlaylistPersist(tag)] = persist
self.db[self._keyPlaylistShuffle(tag)] = shuffle self.db[self._keyPlaylistShuffle(tag)] = shuffle
self.db[self._keyPlaylistName(tag)] = name.encode()
if flush: if flush:
self._flush() self._flush()
@@ -240,7 +246,7 @@ class BTreeDB(IPlaylistDB):
pass pass
for k in (self._keyPlaylistPos(tag), self._keyPlaylistPosOffset(tag), for k in (self._keyPlaylistPos(tag), self._keyPlaylistPosOffset(tag),
self._keyPlaylistPersist(tag), self._keyPlaylistShuffle(tag), self._keyPlaylistPersist(tag), self._keyPlaylistShuffle(tag),
self._keyPlaylistShuffleSeed(tag)): self._keyPlaylistShuffleSeed(tag), self._keyPlaylistName(tag)):
try: try:
del self.db[k] del self.db[k]
except KeyError: except KeyError:
@@ -248,15 +254,18 @@ class BTreeDB(IPlaylistDB):
if flush: if flush:
self._flush() self._flush()
def getPlaylistTags(self): def getPlaylists(self):
""" """
Get a keys-only dict of all defined playlists. Playlists currently do not have names, but are identified by Get a list of all defined playlists with their tag and names.
their tag.
""" """
playlist_tags = set() playlist_tags = set()
for item in self.db: for item in self.db:
playlist_tags.add(item.split(b'/')[0]) playlist_tags.add(item.split(b'/')[0])
return playlist_tags playlists = []
for tag in playlist_tags:
name = self.db.get(self._keyPlaylistName(tag), b'').decode()
playlists.append({'tag': tag, 'name': name})
return playlists
def getPlaylistForTag(self, tag: bytes): def getPlaylistForTag(self, tag: bytes):
""" """
@@ -275,18 +284,19 @@ class BTreeDB(IPlaylistDB):
return None return None
if self._keyPlaylistEntry(tag, pos) not in self.db: if self._keyPlaylistEntry(tag, pos) not in self.db:
pos = 0 pos = 0
name = self.db.get(self._keyPlaylistName(tag), b'').decode()
shuffle = self.db.get(self._keyPlaylistShuffle(tag), self.SHUFFLE_NO) shuffle = self.db.get(self._keyPlaylistShuffle(tag), self.SHUFFLE_NO)
return self.Playlist(self, tag, pos, persist, shuffle) return self.Playlist(self, tag, pos, persist, shuffle, name)
def createPlaylistForTag(self, tag: bytes, entries: typing.Iterable[bytes], persist=PERSIST_TRACK, def createPlaylistForTag(self, tag: bytes, entries: typing.Iterable[bytes], persist=PERSIST_TRACK,
shuffle=SHUFFLE_NO): shuffle=SHUFFLE_NO, name: str = ''):
""" """
Create and save a playlist for 'tag' and return the Playlist object. If a playlist already existed for 'tag' it Create and save a playlist for 'tag' and return the Playlist object. If a playlist already existed for 'tag' it
is overwritten. is overwritten.
""" """
assert persist in (self.PERSIST_NO, self.PERSIST_TRACK, self.PERSIST_OFFSET) assert persist in (self.PERSIST_NO, self.PERSIST_TRACK, self.PERSIST_OFFSET)
assert shuffle in (self.SHUFFLE_NO, self.SHUFFLE_YES) assert shuffle in (self.SHUFFLE_NO, self.SHUFFLE_YES)
self._savePlaylist(tag, entries, persist, shuffle) self._savePlaylist(tag, entries, persist, shuffle, name)
return self.getPlaylistForTag(tag) return self.getPlaylistForTag(tag)
def deletePlaylistForTag(self, tag: bytes): def deletePlaylistForTag(self, tag: bytes):
@@ -370,6 +380,14 @@ class BTreeDB(IPlaylistDB):
_ = int(val) _ = int(val)
except ValueError: except ValueError:
fail(f' Bad playlistposoffset value for {last_tag}: {val!r}') fail(f' Bad playlistposoffset value for {last_tag}: {val!r}')
elif fields[1] == b'playlistname':
val = self.db[k]
try:
name = val.decode()
if dump:
print(f'\tName: {name}')
except UnicodeError:
fail(f' Bad playlistname for {last_tag}: Not valid unicode')
else: else:
fail(f'Unknown key {k!r}') fail(f'Unknown key {k!r}')
return result return result

View File

@@ -112,7 +112,7 @@ async def static(request, path):
@webapp.route('/api/v1/playlists', methods=['GET']) @webapp.route('/api/v1/playlists', methods=['GET'])
async def playlists_get(request): async def playlists_get(request):
return sorted(playlist_db.getPlaylistTags()) return playlist_db.getPlaylists()
def is_hex(s): def is_hex(s):
@@ -133,10 +133,11 @@ async def playlist_get(request, tag):
return None, 404 return None, 404
return { return {
'shuffle': playlist.__dict__.get('shuffle'), 'shuffle': playlist.shuffle,
'persist': playlist.__dict__.get('persist'), 'persist': playlist.persist,
'paths': [(p[len(fsroot):] if p.startswith(fsroot) else p).decode() 'paths': [(p[len(fsroot):] if p.startswith(fsroot) else p).decode()
for p in playlist.getPaths()], for p in playlist.getPaths()],
'name': playlist.name
} }
@@ -156,7 +157,8 @@ async def playlist_put(request, tag):
playlist_db.createPlaylistForTag(tag.encode(), playlist_db.createPlaylistForTag(tag.encode(),
(fsroot + path.encode() for path in playlist.get('paths', [])), (fsroot + path.encode() for path in playlist.get('paths', [])),
playlist.get('persist', 'track').encode(), playlist.get('persist', 'track').encode(),
playlist.get('shuffle', 'no').encode()) playlist.get('shuffle', 'no').encode(),
playlist.get('name', ''))
return '', 204 return '', 204