mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
feat(playlists): manual drag order + Sort A-Z for the playlist list (#1004)
Playlists could only ever be listed alphabetically (system playlists first). Users who group playlists by purpose had no way to put the ones they reach for daily at the front. Adds a nullable `position` column and orders by `(system_key IS NULL), (position IS NULL), position, name COLLATE NOCASE`, so manually-ordered playlists lead, unpositioned ones keep sorting alphabetically behind them, and system playlists stay pinned first. Drag-reorder mirrors the existing within-playlist song reorder, adapted for grid tiles (insert side decided on the horizontal midpoint since tiles flow left-to-right then wrap). System playlists are neither drag sources nor drop targets. `POST /api/playlists/reorder` requires an exact permutation of the current non-system ids, so a duplicate, omission, extra, unknown id, or a system id is rejected rather than silently producing duplicate positions; booleans are rejected explicitly because `sorted([True, 2]) == sorted([1, 2])` would otherwise slip through the permutation check. `POST /api/playlists/sort-alpha` clears the manual order again. Claude-Session: https://claude.ai/code/session_01SFDokqh2H6mEjk1Kgbi6JW Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2413991c5a
commit
f0d9c3abc0
+39
-1
@@ -667,6 +667,16 @@ class MetadataDB:
|
||||
self.conn.execute(_ddl)
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
# Manual playlist ordering (tester ask): `position` orders the
|
||||
# PLAYLISTS themselves (playlist_songs.position orders songs within
|
||||
# one). NULL = unpositioned — those sort alphabetically AFTER the
|
||||
# manually positioned ones, and system playlists stay pinned first
|
||||
# regardless (see list_playlists). Additive, idempotent — same
|
||||
# pattern as `rules`/`kind` above.
|
||||
try:
|
||||
self.conn.execute("ALTER TABLE playlists ADD COLUMN position INTEGER")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
# Wishlist / "wanted" (feedBack#636 item 4): a persisted, actionable
|
||||
# list of songs the user does NOT own yet — the *arr "Wanted/Monitored"
|
||||
# analogue. Unlike playlists (which reference owned local songs by
|
||||
@@ -2405,10 +2415,14 @@ class MetadataDB:
|
||||
|
||||
def list_playlists(self) -> list[dict]:
|
||||
from urllib.parse import quote
|
||||
# Order: system playlists pinned first, then manually positioned user
|
||||
# playlists (position = drag order), then unpositioned ones
|
||||
# alphabetically — so a manual order wins and a playlist created after
|
||||
# a reorder still lands somewhere predictable (see reorder_playlists).
|
||||
rows = self.conn.execute(
|
||||
"SELECT id, name, system_key, created_at, updated_at, kind FROM playlists "
|
||||
"WHERE rules IS NULL " # smart collections live in the source picker, not here
|
||||
"ORDER BY (system_key IS NULL), name COLLATE NOCASE"
|
||||
"ORDER BY (system_key IS NULL), (position IS NULL), position, name COLLATE NOCASE"
|
||||
).fetchall()
|
||||
out = []
|
||||
for r in rows:
|
||||
@@ -2710,6 +2724,30 @@ class MetadataDB:
|
||||
self.conn.commit()
|
||||
return True
|
||||
|
||||
def reorder_playlists(self, ordered_ids: list[int]) -> bool:
|
||||
"""Persist a manual ordering of the playlists THEMSELVES: position =
|
||||
index in `ordered_ids` (the songs-within sibling is reorder_playlist).
|
||||
Caller (the route) validates the list is an exact permutation of the
|
||||
current non-system playlist ids."""
|
||||
with self._lock:
|
||||
for pos, pid in enumerate(ordered_ids):
|
||||
self.conn.execute(
|
||||
"UPDATE playlists SET position = ?, updated_at = datetime('now') WHERE id = ?",
|
||||
(pos, pid),
|
||||
)
|
||||
self.conn.commit()
|
||||
return True
|
||||
|
||||
def clear_playlist_positions(self) -> bool:
|
||||
"""Drop every manual playlist position → back to alphabetical
|
||||
(the "Sort A–Z" affordance)."""
|
||||
with self._lock:
|
||||
self.conn.execute(
|
||||
"UPDATE playlists SET position = NULL, updated_at = datetime('now') "
|
||||
"WHERE position IS NOT NULL")
|
||||
self.conn.commit()
|
||||
return True
|
||||
|
||||
def toggle_saved(self, filename: str) -> bool:
|
||||
"""Add/remove a song on the Saved-for-Later playlist. Returns new state.
|
||||
The presence check and the add/remove run under one lock so two
|
||||
|
||||
@@ -70,6 +70,37 @@ def api_create_playlist(data: dict):
|
||||
return appstate.meta_db.create_playlist(name, kind=kind)
|
||||
|
||||
|
||||
@router.post("/api/playlists/reorder")
|
||||
def api_reorder_playlists(data: dict):
|
||||
"""Manual ordering of the playlists themselves (position = index in
|
||||
`order`); the songs-within sibling is /api/playlists/{pid}/reorder.
|
||||
System playlists stay pinned first and are not part of the order."""
|
||||
order = data.get("order")
|
||||
if not isinstance(order, list) or not all(
|
||||
isinstance(i, int) and not isinstance(i, bool) for i in order):
|
||||
return JSONResponse({"error": "order must be a list of playlist ids"}, status_code=400)
|
||||
# Require an exact permutation of the current non-system playlist ids: a
|
||||
# list with duplicates, omissions, extras, unknown ids, or a system id
|
||||
# would otherwise produce duplicate positions / a partial reorder while
|
||||
# still returning 200 (mirrors the songs-within validation).
|
||||
current = [p["id"] for p in appstate.meta_db.list_playlists() if not p["system_key"]]
|
||||
if len(order) != len(current) or sorted(order) != sorted(current):
|
||||
return JSONResponse(
|
||||
{"error": "order must be a permutation of your playlists' ids"},
|
||||
status_code=400,
|
||||
)
|
||||
appstate.meta_db.reorder_playlists(order)
|
||||
return api_list_playlists()
|
||||
|
||||
|
||||
@router.post("/api/playlists/sort-alpha")
|
||||
def api_sort_playlists_alpha():
|
||||
"""Clear every manual playlist position → back to the alphabetical
|
||||
default (system playlists were pinned first either way)."""
|
||||
appstate.meta_db.clear_playlist_positions()
|
||||
return api_list_playlists()
|
||||
|
||||
|
||||
@router.get("/api/playlists/{pid}")
|
||||
def api_get_playlist(pid: int):
|
||||
pl = appstate.meta_db.get_playlist(pid)
|
||||
|
||||
Reference in New Issue
Block a user