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
@@ -175,3 +175,94 @@ def test_deleting_playlist_removes_custom_cover(client, server):
|
||||
assert _playlist_cover_path(pid).exists()
|
||||
client.delete(f"/api/playlists/{pid}")
|
||||
assert not _playlist_cover_path(pid).exists()
|
||||
|
||||
|
||||
# ── Reordering the playlists THEMSELVES (not songs-within) ───────────────────
|
||||
|
||||
def _mk(client, name):
|
||||
return client.post("/api/playlists", json={"name": name}).json()["id"]
|
||||
|
||||
|
||||
def _ids(client):
|
||||
return [p["id"] for p in client.get("/api/playlists").json()]
|
||||
|
||||
|
||||
def test_playlists_default_order_is_alphabetical(client):
|
||||
b = _mk(client, "Bravo")
|
||||
a = _mk(client, "alpha") # NOCASE: lowercase still sorts by letter
|
||||
z = _mk(client, "Zulu")
|
||||
assert _ids(client) == [a, b, z]
|
||||
|
||||
|
||||
def test_playlist_manual_reorder_persists(client):
|
||||
a = _mk(client, "Alpha")
|
||||
b = _mk(client, "Bravo")
|
||||
c = _mk(client, "Charlie")
|
||||
r = client.post("/api/playlists/reorder", json={"order": [c, a, b]})
|
||||
assert r.status_code == 200
|
||||
assert [p["id"] for p in r.json()] == [c, a, b]
|
||||
# persists across independent list calls
|
||||
assert _ids(client) == [c, a, b]
|
||||
assert _ids(client) == [c, a, b]
|
||||
|
||||
|
||||
def test_playlist_reorder_excludes_system_and_keeps_it_pinned(client):
|
||||
# First toggle creates the "Saved for Later" system playlist.
|
||||
client.post("/api/saved/toggle", json={"filename": "x.archive"})
|
||||
a = _mk(client, "Alpha")
|
||||
b = _mk(client, "Bravo")
|
||||
saved = next(p["id"] for p in client.get("/api/playlists").json() if p["system_key"])
|
||||
# A system id in the order is rejected — it isn't reorderable.
|
||||
assert client.post("/api/playlists/reorder", json={"order": [saved, b, a]}).status_code == 400
|
||||
# User playlists reorder; the system playlist stays pinned first.
|
||||
assert client.post("/api/playlists/reorder", json={"order": [b, a]}).status_code == 200
|
||||
listing = client.get("/api/playlists").json()
|
||||
assert listing[0]["system_key"] == "saved_for_later"
|
||||
assert [p["id"] for p in listing[1:]] == [b, a]
|
||||
|
||||
|
||||
def test_playlist_reorder_rejects_bad_orders(client):
|
||||
a = _mk(client, "Alpha")
|
||||
b = _mk(client, "Bravo")
|
||||
for bad in (
|
||||
[a], # missing an id (partial order)
|
||||
[a, b, 999999], # extra unknown id
|
||||
[a, a], # duplicate (drops b)
|
||||
[a, 999999], # unknown id in place of b
|
||||
"nope", # not a list
|
||||
[a, str(b)], # non-int entry
|
||||
[True, False], # bools are ints to Python — must still be rejected
|
||||
None, # {"order": null}
|
||||
):
|
||||
assert client.post("/api/playlists/reorder", json={"order": bad}).status_code == 400, bad
|
||||
assert client.post("/api/playlists/reorder", json={}).status_code == 400
|
||||
# Nothing was persisted by any rejected request.
|
||||
assert _ids(client) == [a, b]
|
||||
|
||||
|
||||
def test_sort_alpha_clears_manual_order(client):
|
||||
a = _mk(client, "Alpha")
|
||||
b = _mk(client, "Bravo")
|
||||
z = _mk(client, "Zulu")
|
||||
client.post("/api/playlists/reorder", json={"order": [z, b, a]})
|
||||
assert _ids(client) == [z, b, a]
|
||||
r = client.post("/api/playlists/sort-alpha")
|
||||
assert r.status_code == 200
|
||||
assert [p["id"] for p in r.json()] == [a, b, z]
|
||||
assert _ids(client) == [a, b, z]
|
||||
|
||||
|
||||
def test_new_playlist_after_manual_reorder_sorts_alphabetically_after_positioned(client):
|
||||
a = _mk(client, "Alpha")
|
||||
b = _mk(client, "Bravo")
|
||||
client.post("/api/playlists/reorder", json={"order": [b, a]})
|
||||
# New playlists are unpositioned → they follow the manually positioned
|
||||
# ones, alphabetically among themselves, and never disturb the manual
|
||||
# order ("Aardvark" would be first alphabetically).
|
||||
z = _mk(client, "Zebra")
|
||||
aa = _mk(client, "Aardvark")
|
||||
assert _ids(client) == [b, a, aa, z]
|
||||
# A subsequent full reorder must include the newcomers (exact permutation).
|
||||
assert client.post("/api/playlists/reorder", json={"order": [b, a]}).status_code == 400
|
||||
assert client.post("/api/playlists/reorder", json={"order": [z, aa, b, a]}).status_code == 200
|
||||
assert _ids(client) == [z, aa, b, a]
|
||||
|
||||
Reference in New Issue
Block a user