refactor(server): extract the playlists routes into routers/playlists.py (R3) - #841
Conversation
….py (R3) The biggest router yet — 12 playlist routes + custom covers — and the first that needs the config-path seam. server.py: 9,302 -> 9,085 (-217). Three causally-linked pieces, all required for playlists: - `config_dir` joins the appstate seam (the plan always put the path constants there; deferred in S3, needed now). It's env-derived, so the ~49 pop-and-reimport fixtures reconfigure it for free — ZERO setattr retargeting. STATIC_DIR/SLOPPAK_CACHE_DIR (patched via setattr) stay in server.py until a router that reads them is extracted, and get retargeted then. - `_clean_str` (pure request-field sanitizer, 14 callers) -> lib/reqfields.py; server.py imports it back. Unblocks wanted/saved/collections/profile/... later. - routers/playlists.py: bodies verbatim, `@app`->`@router`, `meta_db`-> `appstate.meta_db`, `CONFIG_DIR`->`appstate.config_dir`, `_clean_str` from reqfields, `_ART_CACHE_HEADERS` as a local const (art keeps server.py's). The two exclusive cover helpers (_playlist_cover_path/_url) move with it. include_router at the original site; full 143-route table identical to origin/main. One test retarget: test_playlists_api called `server._playlist_cover_path` directly -> now imports it from routers.playlists (reads appstate.config_dir, which the `server` fixture configures). Verified: pyflakes clean; route table identical; pytest 2401 passed (28 in playlists+collections+appstate); packaging guard 51 (auto-picked up reqfields); eslint 0; boot smoke drives create/rename/add-song/cover-upload/serve/delete — the cover writes 1.png under CONFIG_DIR THROUGH appstate.config_dir and serves 200 with an mtime cache-bust token; a wrong-typed name field still 400s via _clean_str; demo untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe playlist endpoints were extracted from ChangesPlaylist router extraction
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/routers/playlists.py`:
- Around line 98-105: Serialize playlist cover upload and deletion through a
shared per-playlist critical section, including the delete flow and the upload
logic around the existence check and file write. Re-check playlist existence
while holding the lock before committing an upload, and ensure cover cleanup
occurs under the same lock. Replace the shared `{pid}.png.tmp` path with a
unique temporary file in the cover’s directory, preserving atomic
same-filesystem replacement and cleanup on failure.
- Around line 214-215: The broad exception handler around cover image processing
incorrectly treats persistence failures as client errors and exposes filesystem
details. In the cover validation and save flow, catch decode/validation
exceptions separately and return the existing-style 400 response only for those;
handle failures from img.save(...) and tmp.replace(...) by logging the exception
server-side and returning a generic 500 response without exposing error details.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6373e736-d162-4018-a059-127313ef296c
📒 Files selected for processing (7)
CHANGELOG.mddocs/size-exemptions.mdlib/appstate.pylib/reqfields.pylib/routers/playlists.pyserver.pytests/test_playlists_api.py
|
Both cover findings are real and both are pre-existing — the flagged lines ( Handling them in a follow-up, same as the loops races on #839 → #840:
Follow-up PR incoming; will link it and resolve these threads. |
|
Follow-up fix opened: #842 — splits cover decode (400) from persist (500, logged, no leak), unique temp file + existence re-check. Both findings addressed; negative-checked 3 ways. |
…emp, existence re-check (#842) Two pre-existing cover-upload issues CodeRabbit flagged on #841 (verbatim move, so correctly not fixed there): 1. One `except Exception as e` wrapped BOTH the PIL decode and the img.save/ tmp.replace, returned 400 for both, and echoed `e` — so a disk/permission failure was mislabeled a client error and could leak a filesystem path. Now: decode/validation -> 400 "Invalid image" (generic); save/replace failure -> logged 500 "could not save cover" (no detail). 2. A shared `{pid}.png.tmp` let two concurrent uploads clobber each other's temp file. Now a unique `tempfile.mkstemp` in the cover dir, atomic replace to publish. Plus an existence re-check just before publishing so an upload that raced a playlist delete can't leave an orphan cover. mkstemp itself is INSIDE the try (Codex catch): an unwritable dir / full disk raises there and is the same persistence failure as save/replace, so it hits the logged generic-500 path instead of escaping as an unhandled 500. Cleanup guards `tmp is not None` for the mkstemp-failed case. Did NOT add a full per-playlist critical section (CodeRabbit's "heavy lift"): FeedBack is single-user (Principle I), so a cover upload racing a delete on the same id can't happen — documented in the code rather than building a lock framework for a precluded race. tests/test_playlist_cover_errors.py pins all of it; negative-checked three ways: the old single-except-400 shape fails the 500 + no-leak-400 tests, and moving mkstemp back outside the try fails the temp-creation-500 test. Fix passes 5/5. Full suite 2405 passed; boot smoke: valid cover 200, bad image -> generic "Invalid image" 400, no .tmp litter. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(R3) Free after _clean_str moved to lib (#841): wanted's deps are JSONResponse, _clean_str (reqfields), app + meta_db (seam). 3 routes (list/add/remove), 0 setattr targets, 0 helpers to relocate. Bodies verbatim; @app -> @router, meta_db -> appstate.meta_db. include_router at the original site; 143-route table identical. No test retargeting. server.py: 8,003 -> 7,974 (this branch is independent of the chart PR). Verified: pyflakes clean; route table identical; pytest 2401 passed (52 in test_wanted_api); eslint 0. Boot smoke: add wishlist entry -> list -> delete, 400 on missing artist+title (_clean_str path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(R3) (#847) Free after _clean_str moved to lib (#841): wanted's deps are JSONResponse, _clean_str (reqfields), app + meta_db (seam). 3 routes (list/add/remove), 0 setattr targets, 0 helpers to relocate. Bodies verbatim; @app -> @router, meta_db -> appstate.meta_db. include_router at the original site; 143-route table identical. No test retargeting. server.py: 8,003 -> 7,974 (this branch is independent of the chart PR). Verified: pyflakes clean; route table identical; pytest 2401 passed (52 in test_wanted_api); eslint 0. Boot smoke: add wishlist entry -> list -> delete, 400 on missing artist+title (_clean_str path). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The biggest router yet — 12 playlist routes + custom covers — and the first that needs the config-path seam.
server.py: 9,302 → 9,085 (−217).Three causally-linked pieces (all required for playlists)
config_dirjoins the appstate seam. The plan always put the path constants in appstate; I deferred them in S3 to dodge theSTATIC_DIRretargets, and now needconfig_dirfor playlist covers. It's env-derived, so the ~49 pop-and-reimport fixtures reconfigure it for free — zerosetattrretargeting.STATIC_DIR/SLOPPAK_CACHE_DIR(which are patched viasetattr) stay inserver.pyuntil a router that reads them is extracted, and get retargeted then._clean_str→lib/reqfields.py. The pure request-field sanitizer (14 callers across playlists/collections/profile/progression/shop/stats).server.pyimports it back. Unblockswanted/saved/collections/… later.routers/playlists.py. Bodies verbatim;@app→@router,meta_db→appstate.meta_db,CONFIG_DIR→appstate.config_dir,_clean_strfromreqfields,_ART_CACHE_HEADERSas a local const (art keepsserver.py's). The two exclusive cover helpers move with it.Verification
origin/main(143, paths/methods/order).test_playlists_apicalledserver._playlist_cover_pathdirectly → now imports it fromrouters.playlists(which readsappstate.config_dir, configured by theserverfixture). This is the seam working: the helper resolves the fresh config dir across a pop-and-reimport without the test knowing appstate exists.1.pngunderCONFIG_DIRthroughappstate.config_dirand serves200with an mtime cache-bust token; a wrong-typednamefield still400s via_clean_str.pytest2401 passed (28 in playlists+collections+appstate); packaging guard 51 (auto-picked upreqfields);pyflakesclean;npm run lint0; Codex 0 findings.Next:
wanted/savednow unblock (_clean_strhomed). The path-setattrrouters (ws_highway@ 902 lines,song,audio-local-path) needSTATIC_DIR/SLOPPAK_CACHE_DIRin the seam + their 8 test retargets;library/collectionsneed the provider registry;enrichmentneeds the network cluster. Those are the remaining substrates.🤖 Generated with Claude Code
Summary by CodeRabbit