Skip to content

feat(career): extract the whole setlist before the gig starts (no more waiting between songs)#971

Merged
byrongamatos merged 3 commits into
mainfrom
feat/gig-preextract-setlist
Jul 14, 2026
Merged

feat(career): extract the whole setlist before the gig starts (no more waiting between songs)#971
byrongamatos merged 3 commits into
mainfrom
feat/gig-preextract-setlist

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

A feedpak is a zip, and the first play of one pays for its extraction. Inside a set that cost landed between songs: you finished a number and then sat waiting for the next one to unpack, mid-gig.

A setlist is a known list up front, so unpack it all while the poster is still on screen. New POST /gigs/prepare; the poster's Play button shows "Preparing set…" while it runs.

Best-effort at every level — this is an optimisation and can never be the reason a gig will not start:

  • a corrupt pak does not sink the prepare (reported in failed; the play surfaces the error as it does outside a gig — slow beats blocked)
  • a host without the library resolvers degrades to a no-op rather than a 500
  • a failed request falls through to the old lazy extraction

Ordering is pinned: the set is unpacked before the stage is borrowed (venue/viz overwritten) and before the queue starts, so a proposal cancelled while unpacking leaves nothing half-applied.

Tests unpack real zips rather than mocking the extractor: every song lands on disk before the first note, a re-prepare does not duplicate the unpack, one bad pak still leaves the good one prepared, and no-library / empty-setlist degrade cleanly. 18/18.

The other half of that report — the per-song results popup interrupting the set, and claimAutoExit stalling the queue until it was dismissed — is in feedBack-plugin-notedetect.

Summary by CodeRabbit

  • New Features

    • Added a “prepare gig” step that best-effort pre-extracts the full gig set before starting playback.
    • Introduced POST /api/plugins/career/gigs/prepare to prepare requested songs in advance.
    • “Play the gig” now shows “Preparing set…” during preparation (up to 60s), then proceeds even if preparation fails.
  • Bug Fixes

    • Preparation is non-blocking when the library/cache isn’t available.
    • Added protections against unsafe filenames (prevents escaping the library).
  • Tests

    • Added end-to-end route and UI-flow coverage for preparation, idempotency, partial failures, timeouts, and traversal resistance.

A feedpak is a zip, and the first play of one pays for its extraction into
sloppak_cache. Inside a set that cost landed BETWEEN songs: the player finished
a number and then sat there waiting for the next one to unpack, mid-gig.

A setlist is a known list up front, so unpack it all while the poster is still on
screen. New POST /gigs/prepare walks the set through resolve_source_dir; the
poster's Play button shows "Preparing set…" while it runs.

Best-effort by design, at every level:
  - a corrupt pak in the set does not sink the prepare (it is reported in
    `failed`; the play itself surfaces the error exactly as it does outside a
    gig — slow beats blocked)
  - a host without the library resolvers degrades to a no-op rather than 500
  - a failed request just falls through to the old lazy extraction

Ordering matters and is pinned: the set is unpacked BEFORE the stage is borrowed
(venue/viz overwritten) and before the queue starts, so a proposal cancelled
while unpacking leaves nothing half-applied to unwind.

Tests unpack REAL zips rather than mocking the extractor: every song of the set
lands on disk before the first note, a re-prepare does not duplicate the unpack,
one bad pak still leaves the good one prepared, and no-library / empty-setlist
degrade cleanly. 18/18.

NB the other half of the gig report — the per-song results popup interrupting
the set (and worse, claimAutoExit'ing so the queue would not advance until it was
dismissed) — is fixed in the note_detect plugin repo, which is not part of this
checkout.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 66f9be8b-dc66-43ba-8bb4-724635747f9f

📥 Commits

Reviewing files that changed from the base of the PR and between 4825ad2 and 49da302.

📒 Files selected for processing (2)
  • plugins/career/routes.py
  • tests/plugins/career/test_routes.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/career/routes.py
  • tests/plugins/career/test_routes.py

📝 Walkthrough

Walkthrough

The career plugin prepares gig songs through a bounded backend endpoint before playback, handles per-song extraction failures and unavailable library resolvers, and preserves fallback startup behavior. Tests cover extraction, caching, validation, degradation, timeout handling, and startup ordering.

Changes

Gig preparation flow

Layer / File(s) Summary
Feedpak preparation endpoint
plugins/career/routes.py, tests/plugins/career/test_routes.py
Adds POST /api/plugins/career/gigs/prepare to validate and cap song lists, safely extract feedpak assets into the cache, report failed entries, handle missing resolvers, and support empty or warm-cache setlists.
Pre-playback gig startup
plugins/career/screen.js, tests/js/gig_song_end.test.js
Prepares the set before queue startup, disables and restores the play button around an abortable request, checks for changed proposals, and falls back to lazy extraction when preparation fails.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant CareerScreen
  participant CareerAPI
  participant SloppakCache
  Player->>CareerScreen: Click Play the gig
  CareerScreen->>CareerAPI: POST song filenames for preparation
  CareerAPI->>SloppakCache: Resolve and extract feedpak assets
  SloppakCache-->>CareerAPI: Per-song extraction results
  CareerAPI-->>CareerScreen: Prepared and failed songs
  CareerScreen->>CareerScreen: Start playback queue
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change, but it does not follow the required template sections or checklist items. Rewrite it using the template's What, feedpak surface, and Checklist sections, including the required boxes and any spec/FEP note.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: pre-extracting the full setlist before gig playback starts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gig-preextract-setlist

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@plugins/career/routes.py`:
- Around line 738-777: Tighten input validation in prepare_gig before processing
songs: require songs to be a list, reject lists exceeding the allowed size,
reject non-empty string entries, and reject filenames containing path traversal
such as .. components. Return a failed/error response without calling
sloppak.resolve_source_dir for invalid input, while preserving the existing
per-song best-effort behavior for valid entries.

In `@plugins/career/screen.js`:
- Around line 1136-1147: Update prepareGigSongs so the fetch request to
/gigs/prepare is bounded by an abort timeout, ensuring a stalled server response
rejects and the function continues through its existing fallback path. Keep the
existing button restoration behavior and start-anyway semantics intact.
🪄 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: 9abc2438-dda7-415f-8844-c13128f854ce

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0e3c5 and a82ea14.

📒 Files selected for processing (4)
  • plugins/career/routes.py
  • plugins/career/screen.js
  • tests/js/gig_song_end.test.js
  • tests/plugins/career/test_routes.py

Comment thread plugins/career/routes.py
Comment on lines +738 to +777
@app.post(f"/api/plugins/{PLUGIN_ID}/gigs/prepare")
def prepare_gig(body: dict = Body(...)):
"""Unpack every song of the set BEFORE the gig starts.

A feedpak is a zip: the first play of one pays for its extraction into
sloppak_cache. Inside a set that cost landed BETWEEN songs — the player
finished a number and then sat waiting for the next one to unpack, mid-
gig. A set is a known list up front, so extract it all while the player
is still looking at the poster.

Idempotent and cheap on a warm cache: resolve_source_dir() returns the
already-unpacked dir without rewriting it. Best-effort per song — one
bad feedpak must not block the set from starting (the play itself will
surface the error, exactly as it does outside a gig).
"""
files = [str(f) for f in ((body or {}).get("songs") or []) if f]
if not files:
return {"ok": True, "prepared": 0, "failed": []}

# .get, not []: a host that doesn't hand us the resolvers (or has no
# library configured) must degrade to "extract lazily, as before" — this
# is an optimisation, and it is never allowed to be the thing that stops
# a gig from starting.
get_dlc = context.get("get_dlc_dir")
get_cache = context.get("get_sloppak_cache_dir")
dlc_root = get_dlc() if callable(get_dlc) else None
cache_root = get_cache() if callable(get_cache) else None
if dlc_root is None or cache_root is None:
return {"ok": False, "prepared": 0, "failed": files, "error": "no library"}

prepared, failed = 0, []
for fn in files:
try:
sloppak.resolve_source_dir(fn, Path(dlc_root), Path(cache_root))
prepared += 1
except Exception as exc: # noqa: BLE001 — one bad pak can't sink the set
_state["log"].warning("career: gig pre-extract failed for %s: %s", fn, exc)
failed.append(fn)
return {"ok": True, "prepared": prepared, "failed": failed}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- plugins/career/routes.py outline ---'
ast-grep outline plugins/career/routes.py --view expanded || true

echo '--- relevant line ranges ---'
sed -n '650,830p' plugins/career/routes.py | cat -n

echo '--- search resolve_source_dir ---'
rg -n "def resolve_source_dir|resolve_source_dir\(" -S .

Repository: got-feedBack/feedBack

Length of output: 14344


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map sibling endpoints and the cited function.
rg -n "def log_gig|songs|get_pack_file|resolve_source_dir|prepare_gig" plugins/career/routes.py -n -S

# Show only the nearby slices around log_gig and get_pack_file.
sed -n '1,260p' plugins/career/routes.py | cat -n
sed -n '260,520p' plugins/career/routes.py | cat -n
sed -n '520,830p' plugins/career/routes.py | cat -n

Repository: got-feedBack/feedBack

Length of output: 45961


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow inspection of the targeted area and any resolver definition.
FILE=$(git ls-files 'plugins/career/routes.py' | head -n 1)
echo "FILE=$FILE"
sed -n '700,790p' "$FILE" | cat -n

echo '--- sibling validation around log_gig ---'
rg -n "def log_gig|isinstance\(songs, list\)|len\(songs\) > 8|isinstance\(f, str\)|f.strip\(\)" "$FILE" -n -S

echo '--- resolve_source_dir definition candidates ---'
fd -a 'sloppak*.py|*.py' . | head -n 50
rg -n "def resolve_source_dir|class .*sloppak|resolve_source_dir\(" -S .

Repository: got-feedBack/feedBack

Length of output: 6485


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- lib/sloppak.py resolve_source_dir ---'
sed -n '330,430p' lib/sloppak.py | cat -n

echo '--- lib/sloppak.py nearby helper(s) ---'
sed -n '430,560p' lib/sloppak.py | cat -n

echo '--- plugins/career/routes.py get_pack_file ---'
sed -n '930,1010p' plugins/career/routes.py | cat -n

Repository: got-feedBack/feedBack

Length of output: 13727


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect log_gig for comparison with prepare_gig.
sed -n '834,910p' plugins/career/routes.py | cat -n

# Inspect any filename canonicalization helpers used elsewhere in career routes.
rg -n "strip\(\)|relative_to\(|resolve\(\)|safe_join|filename" plugins/career/routes.py lib/sloppak.py -S

Repository: got-feedBack/feedBack

Length of output: 13980


Tighten prepare_gig input validation plugins/career/routes.py:739-777 — reject non-list/oversized songs values and non-empty strings before the loop; sloppak.resolve_source_dir() trusts the filename, so a crafted .. path can escape the DLC root.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 739-739: Do not perform function call Body in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for 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.

In `@plugins/career/routes.py` around lines 738 - 777, Tighten input validation in
prepare_gig before processing songs: require songs to be a list, reject lists
exceeding the allowed size, reject non-empty string entries, and reject
filenames containing path traversal such as .. components. Return a failed/error
response without calling sloppak.resolve_source_dir for invalid input, while
preserving the existing per-song best-effort behavior for valid entries.

Comment thread plugins/career/screen.js
…review)

Both CodeRabbit findings were right.

1. A HUNG PREPARE COULD BLOCK THE GIG FOREVER.

   `await fetch(...)` only rejects on a network ERROR. A server that accepts the
   connection and then never answers hangs indefinitely — and the gig would never
   start. That makes this optimisation the exact thing the PR promises it can
   never be: the reason you cannot play.

   The request is now bounded by an AbortController (PREPARE_TIMEOUT_MS, generous
   because unpacking a setlist is real work — but a CEILING, not a wait). Past it
   we start the gig and let the first play extract lazily, as it always did. The
   Play button is restored in a `finally`, so a timeout cannot strand the poster
   on "Preparing set…" with Play disabled — which would have been the same bug
   wearing a different hat.

2. THE `songs` BODY WAS UNVALIDATED.

   A str is iterable: "abc" would have prepared three one-character "songs". And
   the endpoint unpacks zips, so an arbitrary caller could ask for unbounded work.
   Now list-only, string entries, blanks dropped, capped at MAX_GIG_SONGS.

Tests: the fetch is abortable and the button is re-enabled on EVERY path
including the abort; non-list bodies, non-string/blank entries, and an
oversized setlist. 50 career tests, JS 5/5, eslint clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@tests/plugins/career/test_routes.py`:
- Around line 284-285: Split the semicolon-separated directory initialization
statements in the test setup into separate statements for dlc and cache,
resolving the Ruff E702 violations without changing their behavior.
- Around line 294-304: Update test_prepare_caps_the_setlist to verify the
request is truncated to MAX_GIG_SONGS rather than relying on prepared == 0;
assert the response’s failed list contains only the capped number of entries,
while preserving the existing cap bound and endpoint request.
🪄 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: aeb44cc2-b958-46a3-86b4-d02b3c3d2602

📥 Commits

Reviewing files that changed from the base of the PR and between a82ea14 and 4825ad2.

📒 Files selected for processing (4)
  • plugins/career/routes.py
  • plugins/career/screen.js
  • tests/js/gig_song_end.test.js
  • tests/plugins/career/test_routes.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/js/gig_song_end.test.js
  • plugins/career/routes.py

Comment thread tests/plugins/career/test_routes.py Outdated
Comment thread tests/plugins/career/test_routes.py Outdated
…y tests the cap

CodeRabbit again, and the first one is a real hole I put there.

1. PATH TRAVERSAL. sloppak.resolve_source_dir() does a bare `dlc_root / filename`
   with NO containment guard — so `../../x` walks straight out of the library, and
   my new endpoint handed it attacker-supplied filenames. Every filename now goes
   through _resolve_dlc_path first, the same check every other filename-bound
   handler applies. Pinned: `..`, backslash traversal, an absolute POSIX path and
   a Windows drive path are all refused, and nothing outside the library is
   unpacked.

2. THE CAP TEST WAS VACUOUS. It asserted `prepared == 0` against a fixture with no
   library — where the endpoint exits before extraction — so it passed whether or
   not MAX_GIG_SONGS existed. It now runs against a real library and asserts the
   endpoint CONSIDERED at most MAX_GIG_SONGS of the 82 it was handed. Verified to
   fail when the cap is removed.

   Same class of mistake as the notedetect gigBlock: a test that passes for the
   wrong reason. Worth saying out loud since it is twice in one day.

3. E702 — semicolon-joined statements in the new tests, split.

51 career tests; full suite green.
@byrongamatos

Copy link
Copy Markdown
Contributor Author

@coderabbitai the input-validation finding is addressed in 49da302 — and you were right about more than the shape of the value.

The path traversal was real and mine. sloppak.resolve_source_dir() does a bare dlc_root / filename with no containment guard, and I was handing it attacker-supplied filenames. Every filename now goes through _resolve_dlc_path first — the same check every other filename-bound handler applies. Pinned: .., backslash traversal, an absolute POSIX path and a Windows drive path are all refused, and the test asserts nothing outside the library is unpacked.

Also in that commit: songs is now list-only with string entries, blanks dropped, capped at MAX_GIG_SONGS (a str is iterable — "abc" would have prepared three one-character "songs").

And your other two: the E702 semicolons are split, and the cap test was vacuous — it asserted prepared == 0 against a fixture with no library, where the endpoint exits before extraction, so it passed whether or not the cap existed. It now runs against a real library and asserts the endpoint considered at most MAX_GIG_SONGS of the 82 it was handed; I verified it fails when the cap is removed.

@byrongamatos
byrongamatos merged commit 1702afa into main Jul 14, 2026
6 checks passed
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
git log --oneline -5
git show 49da302 --stat
git show 49da302 -- plugins/career/routes.py

Length of output: 5469


@byrongamatos Confirmed — I pulled 49da302 and the fix is solid.

routes.py now imports _resolve_dlc_path from dlc_paths, builds root = Path(dlc_root) once, and runs every incoming filename through _resolve_dlc_path(root, fn) before it ever reaches sloppak.resolve_source_dir. Anything that fails containment gets logged and appended to failed instead of touching the filesystem — exactly the same gate the other filename-bound handlers use, so this endpoint is no longer the odd one out. Good catch calling out that resolve_source_dir itself has no guard; that's worth keeping in mind if it ever gets a new caller that forgets this step.

The revised cap test (real library, asserting the endpoint only considers up to MAX_GIG_SONGS of the 82 submitted, verified to fail with the cap removed) is a meaningfully stronger assertion than the original — glad you traced through why the old one passed vacuously rather than just patching it and moving on.

Nice work tightening this up in one pass.


✏️ Learnings added
Learnt from: byrongamatos
URL: https://github.com/got-feedBack/feedBack/pull/971

Timestamp: 2026-07-14T22:36:58.343Z
Learning: In plugins/career/routes.py, `sloppak.resolve_source_dir(filename, dlc_root, cache_root)` performs a bare `dlc_root / filename` join with no path-containment guard, so any caller passing attacker-influenced filenames to it (or to similar sloppak APIs) must first validate the path via `_resolve_dlc_path` (from `dlc_paths`) to prevent path traversal. This pattern is applied in the `/api/plugins/career/gigs/prepare` endpoint.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant