Skip to content

fix(queue): manual additions stack in user queue, not at absolute end#158

Merged
InstaZDLL merged 2 commits into
mainfrom
fix/queue-user-section-spotify-semantics
May 25, 2026
Merged

fix(queue): manual additions stack in user queue, not at absolute end#158
InstaZDLL merged 2 commits into
mainfrom
fix/queue-user-section-spotify-semantics

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 25, 2026

Summary

User flagged that "Add to queue" was dropping tracks at the bottom of the entire queue — behind every remaining album / playlist track. Watching what you actually queued meant scrolling past 100+ context tracks, and the picks fired in the wrong order relative to "Play next" (which already inserts at current+1).

Carve out a real "user queue" block, Spotify-style: the contiguous source_type = 'manual' rows sitting between the current track and the context tail.

How

The queue_item table already carries a source_type column populated by every queue-building path ('album' from Play album, 'playlist' from Play playlist, 'manual' from the two context-menu actions). New helper queue::append_to_user_queue walks that flag:

SELECT MIN(position) FROM queue_item
 WHERE position > :current
   AND source_type != 'manual'

Result = start of the context tail. Insert the picks immediately before it (push the tail down by N, park-shift detour for SQLite's UNIQUE(position)). When the query returns NULL — entire tail is already manual, or current is at the last slot — degenerate into append. When the queue is empty, degenerate into fill_queue so the first "Add to queue" on a fresh session still starts playback.

player_add_to_queue switched to the new helper. "Play next" semantics unchanged — insert_after_current already pushes to current+1, i.e. the top of the user queue.

End state

[…ctx_played, ctx_current (NOW PLAYING),
   manual_play_next_N, manual_play_next_N-1, …, manual_play_next_1,  ← user queue: Play-next stacked on top
   manual_add_1, manual_add_2, …, manual_add_M,                       ← user queue: Add-to-queue stacked at bottom
 ctx_next, ctx_after, …]                                              ← context tail resumes after user queue drains

Test plan

  • cargo check --all-targets clean
  • cargo test --lib queue → 2/2 passed (only pure-fn coverage exists for queue.rs, matches the rest of the codebase's no-SQL-integration-tests convention)
  • Manual smoke (golden path):
    1. Play an album (12 tracks, on track 3)
    2. "Add to queue" track X → X lands at position 4 (right after track 3), context shifts to 5+
    3. "Add to queue" track Y → Y lands at position 5 (after X), context shifts to 6+
    4. "Play next" track Z → Z lands at position 4 (between current and X), X/Y shift to 5/6, context to 7+
    5. Let auto-advance run → playback order is track 3 → Z → X → Y → track 4 → track 5 → …
  • Manual smoke (edge cases):
    • Empty queue + "Add to queue" → fills queue and starts playback at track 0
    • All-manual queue + "Add to queue" → appends at end (no context tail to insert before)
    • Current at last position + "Add to queue" → appends at end

Summary by CodeRabbit

  • Nouvelles fonctionnalités
    • L’action d’« ajouter à la file » place désormais les morceaux dans une zone dédiée entre la piste en cours et le contenu de l’album/playlist (comportement aligné sur Spotify), sans perturber la position de lecture actuelle.
  • Documentation
    • Ajout d’une documentation détaillée expliquant la logique de la file d’attente utilisateur, le marquage des entrées et les cas de placement/recouvrement.

Review Change Stack

…lute end

Adding a track from the context menu's "Add to queue" called
`queue::append`, which dropped the new row at the bottom of the
entire queue — behind every remaining album / playlist / smart-fill
track. Watching what you actually queued meant scrolling past 100+
context tracks, and the user picks fired in the wrong order relative
to "Play next" (which already inserted at current+1).

Carve out a real "user queue" block, Spotify-style: the contiguous
'manual'-sourced rows sitting between the current track and the
context tail.

- New `queue::append_to_user_queue` finds the first non-'manual'
  position past the cursor (= start of the context tail) and inserts
  the picks immediately before it, pushing the tail down. Degenerates
  into `append` when the entire tail is already manual (queue all
  user picks, or current at the last slot) and into `fill_queue`
  when the queue is empty so the first "Add to queue" click on a
  fresh session still starts playback.
- `player_add_to_queue` switched to call the new helper.
- "Play next" semantics unchanged (`insert_after_current` already
  pushes to the top of the user queue at current+1).

End state: Now Playing → user queue (Play-next stacked on top,
Add-to-queue stacked at the bottom) → context tail. Album resumes
where it left off after the user queue drains.
@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets type: fix Bug fix labels May 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

Cette PR ajoute append_to_user_queue pour insérer des pistes dans la portion contiguë "manual" entre la piste courante et la queue de contexte, intègre cet appel dans la commande player_add_to_queue et documente le comportement dans les docs de playback.

Changes

User queue placement strategy

Layer / File(s) Summary
User queue vs context tail specification
docs/features/playback.md
Documentation du comportement de source_type et des règles de placement pour fill_queue, insert_after_current (current_index + 1, 'manual') et append_to_user_queue (insère avant la première entrée non-manuelle post-curseur; repli vers append ou fill_queue).
User queue insertion logic
src-tauri/src/queue.rs
Nouvelle fonction publique append_to_user_queue : gère file vide (initialisation/fill), calcule la frontière de position (MIN des éléments source_type != 'manual' après le curseur) pour déterminer insert_at, insère via transaction en appliquant un park-shift OFFSET quand il faut décaler pour préserver l'unicité de position, puis supprime queue.preshuffle avant commit.
Add-to-queue command integration
src-tauri/src/commands/player.rs
player_add_to_queue remplace l'appel à queue::append(..., "manual", None) par queue::append_to_user_queue(&pool, &track_ids, None), plaçant les pistes dans la zone manuelle contiguë sans déplacer le curseur actuel.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

type: fix

Poem

🎵 Entre le curseur et la fin, un petit rang s'aligne,
Les pistes ajoutées s'assemblent, doucement en bloc,
Le contexte reste intact, la lecture fait sa ligne,
Un ordre manuel qui marche, sans heurt ni choc.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre est directement lié au changement principal : introduction d'une file d'attente utilisateur pour les ajouts manuels, au lieu de les placer à la fin absolue.
Description check ✅ Passed La description couvre tous les éléments du template : résumé du problème et de la solution (2 bullets), plan de test détaillé avec cas nominaux et edge cases, et checklist validée.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/queue-user-section-spotify-semantics

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

@InstaZDLL InstaZDLL added the size: m 50-200 lines label May 25, 2026
@InstaZDLL InstaZDLL self-assigned this May 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@src-tauri/src/queue.rs`:
- Around line 338-359: The boundary/length/current_index calculations must run
inside the same DB transaction as the write to avoid race conditions; start a
transaction with pool.begin() before calling queue_length, read_setting_i64, and
the SELECT MIN(position) query (the boundary computation), then perform
fill_queue or append using that same transaction/connection (use the &mut
sqlx::SqliteConnection transaction type and update calls to fill_queue, append
and any upsert helpers to accept and use the transaction connection instead of
pool) so the decision and insert are atomic and respect SQLite single-writer
semantics.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fcbb4eed-87e8-4cb0-8ac2-6a635e64f438

📥 Commits

Reviewing files that changed from the base of the PR and between 53c271d and d098b69.

📒 Files selected for processing (3)
  • docs/features/playback.md
  • src-tauri/src/commands/player.rs
  • src-tauri/src/queue.rs

Comment thread src-tauri/src/queue.rs Outdated
The boundary computation (queue_length + current_index + MIN(position)
WHERE source_type != 'manual') was running outside the writer
transaction. SQLite WAL allows many readers and one writer, so an
auto-advance / fill_queue / shuffle firing between our reads and our
pool.begin() could invalidate every input we'd already decided on —
the insert then lands at the wrong slot relative to the new cursor or
the new queue contents.

Wrap the whole decision-then-write in a single `pool.begin()`
transaction. Every SQL now runs against `&mut *tx`:
- COUNT(*) for the length check
- SELECT value FROM profile_setting for the cursor
- SELECT MIN(position) for the boundary
- the park-shift INSERT block
- the preshuffle invalidation

Empty-queue and all-manual-tail fallbacks are inlined (their previous
delegation to fill_queue / append would have opened their own nested
txs, defeating the atomicity guarantee). Same SQL shape, no new
helpers — keeps the public queue.rs API surface unchanged.

Note: `insert_after_current` has the same pre-existing structural
race but is out of scope for this PR's review.
@InstaZDLL
Copy link
Copy Markdown
Owner Author

Addressed in 8dbfc26.

Finding valid. Confirmed against current code: queue_length, read_setting_i64("queue.current_index") and the SELECT MIN(position) … WHERE source_type != 'manual' all ran on the raw pool. Between those reads and pool.begin(), an auto-advance / fill_queue / shuffle firing on another task could move current_index or rewrite the queue — our insert then landed at a stale slot.

Fix: open pool.begin() at the very top (after the empty-tracks early return) and route every SQL through &mut *tx:

  • SELECT COUNT(*) for length
  • SELECT value FROM profile_setting for cursor
  • SELECT MIN(position) for boundary
  • the park-shift INSERT block
  • the preshuffle invalidation

Both fallback branches (empty queue → fill, all-manual tail → append) had to be inlined — delegating to fill_queue / append would have opened nested transactions on the raw pool and defeated the atomicity guarantee. SQL shapes are identical, just bound to "manual" since we already know the source. No new public helpers, queue.rs API surface unchanged.

Out of scope, noted: insert_after_current has the same structural race (queue_length(pool) + read_setting_i64(pool) then pool.begin()), pre-existing before this PR. Same fix applies — happy to do it in a follow-up rather than expand this PR's diff.

Tests: 111/111 passed, cargo check clean.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@src-tauri/src/queue.rs`:
- Around line 338-382: The UPDATE on profile_setting for key
'queue.current_index' may do nothing for new profiles; replace that UPDATE with
an INSERT ... ON CONFLICT DO UPDATE pattern so the setting is created if missing
(same pattern used elsewhere, e.g. fill_queue). Locate the sqlx::query that
updates profile_setting for 'queue.current_index' and change it to insert the
key, value ("0") and updated_at (now) and on conflict(key) update value and
updated_at, binding the same parameters (value and now) as needed; leave the
surrounding transaction, DELETE for 'queue.preshuffle', and tx.commit()
unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eef9f440-21e5-4844-a4e3-fb269c3a14d1

📥 Commits

Reviewing files that changed from the base of the PR and between d098b69 and 8dbfc26.

📒 Files selected for processing (1)
  • src-tauri/src/queue.rs

Comment thread src-tauri/src/queue.rs
@InstaZDLL
Copy link
Copy Markdown
Owner Author

InstaZDLL commented May 25, 2026

@coderabbitai Skipping per «verify each finding» — the stated failure mode (fresh profile lacks the row → UPDATE no-ops → cursor undefined) is invalid:

migrations/profile/20260411120000_initial.sql:24 seeds the row at profile creation:

```sql
INSERT INTO profile_setting (key, value, value_type, updated_at) VALUES

('queue.current_index', '0', 'int', strftime('%s','now') * 1000);
```

So every profile — fresh or migrated — has the row at boot. The UPDATE affects 1 row deterministically. This is exactly why fill_queue and write_setting_i64 (the established helper) both use UPDATE rather than INSERT … ON CONFLICT for this key.

The broader CLAUDE.md note ("Pattern: INSERT … ON CONFLICT DO UPDATE") applies to keys that aren't seeded — e.g. wrapped.dismissed_year, mini_player.bounds. For seeded keys, the canonical pattern in this codebase is plain UPDATE, and changing only the new branch would create internal inconsistency with fill_queue two functions down. If we want to flip the policy to defensive INSERT … ON CONFLICT for every profile_setting write — even seeded ones — that's a separate refactor that should sweep queue.rs and the player.* keys together, not a one-line patch on this PR.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

@InstaZDLL Point bien pris — le finding était invalide. La présence garantie de queue.current_index via la migration rend le cas de no-op impossible, et utiliser plain UPDATE est effectivement cohérent avec fill_queue et write_setting_i64. Retrait du commentaire.


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/158

Timestamp: 2026-05-25T22:52:16.470Z
Learning: In the WaveFlow codebase (src-tauri/src/queue.rs, Rust/SQLite via sqlx), `profile_setting` keys fall into two categories:
- **Seeded keys** (e.g. `queue.current_index`, `queue.shuffle`, `player.*`): inserted by `migrations/profile/20260411120000_initial.sql` at profile creation, so they always exist. The canonical write pattern for these is a plain `UPDATE` — not `INSERT … ON CONFLICT DO UPDATE`. Both `fill_queue` and `write_setting_i64` use this pattern.
- **Un-seeded keys** (e.g. `wrapped.dismissed_year`, `mini_player.bounds`): these may be absent and should use `INSERT … ON CONFLICT DO UPDATE` as noted in CLAUDE.md.
Suggesting `INSERT … ON CONFLICT` for seeded keys is incorrect and would create internal inconsistency with the established helpers.

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.

✅ Actions performed

Comments resolved and changes approved.

@InstaZDLL InstaZDLL merged commit cefbe6e into main May 25, 2026
14 checks passed
@InstaZDLL InstaZDLL deleted the fix/queue-user-section-spotify-semantics branch May 25, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets size: m 50-200 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant