From fe1c1eadf79cd80f18d23eae6ca7a83e79d217b9 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Tue, 23 Dec 2025 13:25:39 +0100 Subject: [PATCH] feat: Add names to playlists Fixes #63. Signed-off-by: Matthias Blankertz --- software/frontend/index.html | 17 ++++++++++---- software/src/utils/playlistdb.py | 38 +++++++++++++++++++++++--------- software/src/webserver.py | 10 +++++---- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/software/frontend/index.html b/software/frontend/index.html index 01867da..2f87fb7 100644 --- a/software/frontend/index.html +++ b/software/frontend/index.html @@ -213,6 +213,10 @@ +
+ + +
@@ -493,7 +497,7 @@ document.getElementById('playlist-exist-button-delete') .addEventListener('click', (e) => { if (lastSelected === null) return; - const tagid = lastSelected.innerText; + const tagid = lastSelected.getAttribute('data-tag'); if(confirm(`Really delete playlist ${tagid}?`)) { fetch(`/api/v1/playlist/${tagid}`, { method: 'DELETE'}) @@ -504,7 +508,7 @@ .addEventListener('click', selectLastTag); document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => { 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) => { const node = e.target.closest("li"); @@ -559,8 +563,9 @@ container.innerHTML = ""; for (const playlist of playlists) { const li = document.createElement("li"); - li.innerHTML = playlist; + li.innerHTML = `${playlist.name} (${playlist.tag})`; li.className = "node" + li.setAttribute('data-tag', playlist.tag); container.appendChild(li) } } @@ -585,7 +590,7 @@ document.getElementById('playlist-exist-list') .querySelectorAll("li") .forEach(n => { - if (n.innerText == tagtext) { + if (n.getAttribute('data-tag') == tagtext) { n.classList.add("selected"); lastSelected = n; } else { @@ -687,6 +692,8 @@ } else if (playlist.persist === "track" && playlist.shuffle === "no") { playlisttype.value = "audioplay"; } + const playlistname = document.getElementById('playlist-edit-name'); + playlistname.value = playlist.name; } async function save() { @@ -709,6 +716,8 @@ playlistData.shuffle = "no"; break; } + const playlistname = document.getElementById('playlist-edit-name'); + playlistData.name = playlistname.value; const container = document.getElementById('playlist-edit-list'); playlistData.paths = [...container.querySelectorAll("li")].map((node) => node.innerText); const saveRes = await fetch(`/api/v1/playlist/${playlistId}`, diff --git a/software/src/utils/playlistdb.py b/software/src/utils/playlistdb.py index 546311b..770010e 100644 --- a/software/src/utils/playlistdb.py +++ b/software/src/utils/playlistdb.py @@ -33,12 +33,13 @@ class BTreeDB(IPlaylistDB): PERSIST_OFFSET = b'offset' 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.tag = tag self.pos = pos self.persist = persist self.shuffle = shuffle + self.name = name self.length = self.parent._getPlaylistLength(self.tag) self._shuffle() @@ -168,6 +169,10 @@ class BTreeDB(IPlaylistDB): return (b''.join([tag, b'/playlist/']), b''.join([tag, b'/playlist0'])) + @staticmethod + def _keyPlaylistName(tag): + return b''.join([tag, b'/playlistname']) + def _flush(self): """ Flush the database and call the flush_func if it was provided. @@ -222,12 +227,13 @@ class BTreeDB(IPlaylistDB): raise RuntimeError("Malformed playlist key") 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) for idx, entry in enumerate(entries): self.db[self._keyPlaylistEntry(tag, idx)] = entry self.db[self._keyPlaylistPersist(tag)] = persist self.db[self._keyPlaylistShuffle(tag)] = shuffle + self.db[self._keyPlaylistName(tag)] = name.encode() if flush: self._flush() @@ -240,7 +246,7 @@ class BTreeDB(IPlaylistDB): pass for k in (self._keyPlaylistPos(tag), self._keyPlaylistPosOffset(tag), self._keyPlaylistPersist(tag), self._keyPlaylistShuffle(tag), - self._keyPlaylistShuffleSeed(tag)): + self._keyPlaylistShuffleSeed(tag), self._keyPlaylistName(tag)): try: del self.db[k] except KeyError: @@ -248,15 +254,18 @@ class BTreeDB(IPlaylistDB): if 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 - their tag. + Get a list of all defined playlists with their tag and names. """ playlist_tags = set() for item in self.db: 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): """ @@ -275,18 +284,19 @@ class BTreeDB(IPlaylistDB): return None if self._keyPlaylistEntry(tag, pos) not in self.db: pos = 0 + name = self.db.get(self._keyPlaylistName(tag), b'').decode() 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, - 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 is overwritten. """ assert persist in (self.PERSIST_NO, self.PERSIST_TRACK, self.PERSIST_OFFSET) 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) def deletePlaylistForTag(self, tag: bytes): @@ -370,6 +380,14 @@ class BTreeDB(IPlaylistDB): _ = int(val) except ValueError: 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: fail(f'Unknown key {k!r}') return result diff --git a/software/src/webserver.py b/software/src/webserver.py index 8808dde..f87f467 100644 --- a/software/src/webserver.py +++ b/software/src/webserver.py @@ -112,7 +112,7 @@ async def static(request, path): @webapp.route('/api/v1/playlists', methods=['GET']) async def playlists_get(request): - return sorted(playlist_db.getPlaylistTags()) + return playlist_db.getPlaylists() def is_hex(s): @@ -133,10 +133,11 @@ async def playlist_get(request, tag): return None, 404 return { - 'shuffle': playlist.__dict__.get('shuffle'), - 'persist': playlist.__dict__.get('persist'), + 'shuffle': playlist.shuffle, + 'persist': playlist.persist, 'paths': [(p[len(fsroot):] if p.startswith(fsroot) else p).decode() for p in playlist.getPaths()], + 'name': playlist.name } @@ -156,7 +157,8 @@ async def playlist_put(request, tag): playlist_db.createPlaylistForTag(tag.encode(), (fsroot + path.encode() for path in playlist.get('paths', [])), playlist.get('persist', 'track').encode(), - playlist.get('shuffle', 'no').encode()) + playlist.get('shuffle', 'no').encode(), + playlist.get('name', '')) return '', 204