|
| 1 | +--- |
| 2 | +id: TASK-332 |
| 3 | +title: >- |
| 4 | + Queue broken: double-click doesn't enqueue full library, track-ended doesn't |
| 5 | + auto-advance |
| 6 | +status: Done |
| 7 | +assignee: [] |
| 8 | +created_date: '2026-04-14 15:23' |
| 9 | +updated_date: '2026-04-14 16:08' |
| 10 | +labels: |
| 11 | + - bug |
| 12 | + - queue |
| 13 | + - playback |
| 14 | + - frontend |
| 15 | +dependencies: [] |
| 16 | +references: |
| 17 | + - app/frontend/js/utils/queue-builder.js |
| 18 | + - app/frontend/js/components/library-browser.js |
| 19 | + - app/frontend/js/stores/library.js |
| 20 | + - app/frontend/js/stores/player.js |
| 21 | + - app/frontend/js/stores/queue.js |
| 22 | + - crates/mt-tauri/src/audio/engine.rs |
| 23 | + - crates/mt-tauri/src/commands/audio.rs |
| 24 | +priority: high |
| 25 | +ordinal: 1250 |
| 26 | +--- |
| 27 | + |
| 28 | +## Description |
| 29 | + |
| 30 | +<!-- SECTION:DESCRIPTION:BEGIN --> |
| 31 | +## Problem |
| 32 | + |
| 33 | +Two related queue/playback regressions observed on 2026-04-14: |
| 34 | + |
| 35 | +### Bug 1: Double-clicking a track in library view doesn't enqueue subsequent tracks |
| 36 | + |
| 37 | +When double-clicking a track (e.g. LCD Soundsystem - Losing My Edge) in the main music library view, only the clicked track plays. The rest of the library is not enqueued as context. |
| 38 | + |
| 39 | +**Root cause (likely):** `library-browser.js:456` passes `this.library.filteredTracks` to `handleDoubleClickPlay()`. The library uses pagination (500 tracks/page, 9641 total). The `filteredTracks` getter (`library.js:284-298`) only returns currently loaded pages — it does NOT load all pages first. |
| 40 | + |
| 41 | +Compare with the "Add All to Queue" flow (`library.js:628-631`) which explicitly calls `_loadAllPages()` before queueing. The double-click path in `queue-builder.js:27` skips this step, so only tracks from loaded pages are passed to `queue_play_context`. |
| 42 | + |
| 43 | +If the user scrolled to a track in the middle of the library but earlier/later pages aren't loaded, the queue would be incomplete or the `index` parameter would be wrong relative to the partial track list. |
| 44 | + |
| 45 | +### Bug 2: Playback doesn't auto-advance to next track (stuck at last ~2 seconds) |
| 46 | + |
| 47 | +After manually adding a "Play Next" track (The La's - Endless), playback doesn't advance when the first track finishes. The UI shows the track stuck at approximately the last 2 seconds. |
| 48 | + |
| 49 | +**Possible causes (investigation needed):** |
| 50 | + |
| 51 | +1. **`is_finished()` never returns true** — `engine.rs:272-279` checks `sink.empty() && self.state == Playing`. If the sink doesn't fully drain (rodio/symphonia decoder issue), or if there's a timing race where `self.state` changes before the check, the condition never triggers. |
| 52 | + |
| 53 | +2. **`audio://track-ended` event emitted but not handled** — The frontend listener (`player.js:45-48`) calls `queue.playNext()` which invokes `queue_play_next_track` on the backend. If the queue has only 1 item and no next track, `playNext` may silently stop. |
| 54 | + |
| 55 | +3. **Progress polling stops before track ends** — The progress emission (`audio.rs:309`) only fires when `is_playing` is true. But `get_state()` returns `Stopped` when `is_finished()` is true, so progress emission stops the same tick `is_finished` first triggers. If the last progress update showed ~2s remaining, the UI would freeze there. |
| 56 | + |
| 57 | +### Reproduction |
| 58 | + |
| 59 | +1. Launch mt, open main library view (All tracks, sorted by artist) |
| 60 | +2. Scroll to LCD Soundsystem - Losing My Edge, double-click |
| 61 | +3. Open Now Playing / queue panel — observe only 1 track (or a small subset) is queued |
| 62 | +4. Right-click The La's - Endless, select "Play Next" |
| 63 | +5. Wait for Losing My Edge to finish — observe playback doesn't advance |
| 64 | + |
| 65 | +### Log evidence |
| 66 | + |
| 67 | +``` |
| 68 | +2026-04-14T15:12:56.168887Z INFO mt_lib::commands::audio: Lazily initializing audio engine on first use |
| 69 | +2026-04-14T15:12:56.341239Z INFO mt_lib::audio::engine: Track loaded path=".../01 - Losing My Edge.mp3" duration_ms=473066 |
| 70 | +2026-04-14T15:13:20.826571Z WARN symphonia_bundle_mp3::layer3: mpa: invalid main_data_begin, underflow by 218 bytes |
| 71 | +``` |
| 72 | + |
| 73 | +No `audio://track-ended` event visible in logs (backend doesn't log it). The symphonia warning about MP3 main_data underflow may be related to Bug 2 — decoder issues could prevent the sink from fully draining. |
| 74 | + |
| 75 | +## Investigation needed |
| 76 | + |
| 77 | +- Add `debug!` logging around `is_finished()` transitions and `audio://track-ended` emission to confirm whether the event fires |
| 78 | +- Check if symphonia MP3 decode errors prevent `sink.empty()` from ever returning true |
| 79 | +- Verify `filteredTracks` page coverage when double-clicking from different scroll positions |
| 80 | +- Check if `queue_play_context` receives the full track list or a partial one (log the count) |
| 81 | +<!-- SECTION:DESCRIPTION:END --> |
| 82 | + |
| 83 | +## Acceptance Criteria |
| 84 | +<!-- AC:BEGIN --> |
| 85 | +- [x] #1 Double-clicking a track in the main library view enqueues the full library (all pages) as context, not just loaded pages |
| 86 | +- [x] #2 Playback auto-advances to the next queued track when the current track finishes |
| 87 | +- [x] #3 Play Next tracks are played when the preceding track ends |
| 88 | +- [x] #4 No regression in shuffle, loop, or other queue navigation modes |
| 89 | +<!-- AC:END --> |
| 90 | + |
| 91 | +## Implementation Notes |
| 92 | + |
| 93 | +<!-- SECTION:NOTES:BEGIN --> |
| 94 | +## Investigation Results (2026-04-14) |
| 95 | + |
| 96 | +### Bug 1: Root Cause Confirmed |
| 97 | + |
| 98 | +`library-browser.js:452` passes `this.library.filteredTracks` to `handleDoubleClickPlay()`. The `filteredTracks` getter only returns tracks from loaded pages (pagination with 500 tracks/page). When a user double-clicks a track at global index 2500 but only pages 0 and 5 are loaded, `filteredTracks` has ~1000 entries and the index 2500 is out of bounds. The guard in `queue-builder.js:16` (`index >= allTracks.length`) triggers, falling back to single-track playback via `playTrack()`. |
| 99 | + |
| 100 | +### Bug 2: Root Cause Analysis |
| 101 | + |
| 102 | +`is_finished()` in `engine.rs:271-279` checks `sink.empty() && self.state == Playing`. The symphonia MP3 decode error (`invalid main_data_begin, underflow by 218 bytes`) near the end of an MP3 file can prevent the sink from fully draining. If `sink.empty()` never returns true, `is_finished()` stays false, `audio://track-ended` never fires, and the frontend's `playNext()` is never called. The UI shows progress frozen at ~2s remaining because `is_playing` remains true but the position stops advancing. |
| 103 | + |
| 104 | +## Fixes Applied |
| 105 | + |
| 106 | +### Bug 1 Fix: `library-browser.js` |
| 107 | +- Added `_loadAllPages()` call before `handleDoubleClickPlay()` in `handleDoubleClick()`, matching the pattern used by `addAllToQueue()` |
| 108 | + |
| 109 | +### Bug 2 Fix: `audio.rs` (audio_thread) |
| 110 | +- Added stall detection: if playback position hasn't advanced for ~1 second (10 poll cycles at 100ms) and the position is within 5 seconds of the track end, treat the track as finished |
| 111 | +- Added debug logging on track-ended emission |
| 112 | +- Reset stall counters on Load/LoadAndPlay commands |
| 113 | +<!-- SECTION:NOTES:END --> |
| 114 | + |
| 115 | +## Final Summary |
| 116 | + |
| 117 | +<!-- SECTION:FINAL_SUMMARY:BEGIN --> |
| 118 | +## Bug 1: Double-click doesn't enqueue full library\n\n**Root cause:** `library-browser.js` passed `filteredTracks` (only loaded pages) to `handleDoubleClickPlay()`. With 500 tracks/page pagination, tracks at high global indices were out of bounds, causing fallback to single-track playback.\n\n**Fix:** Added `_loadAllPages()` call before `handleDoubleClickPlay()` in `handleDoubleClick()`, matching the pattern used by `addAllToQueue()`.\n\n## Bug 2: Playback doesn't auto-advance (stuck at last ~2s)\n\n**Root cause:** Symphonia MP3 decode errors (`invalid main_data_begin`) near end-of-track prevent rodio sink from draining. `sink.empty()` never returns true, so `is_finished()` stays false and `audio://track-ended` never fires.\n\n**Fix:** Added `StallDetector` in `commands/audio.rs` that monitors playback position during the audio thread poll loop. If position hasn't advanced for 10 consecutive polls (~1s) and is within 5s of track end, treats the track as finished.\n\n## Files changed\n- `app/frontend/js/components/library-browser.js` — `_loadAllPages()` before double-click queue\n- `crates/mt-tauri/src/commands/audio.rs` — `StallDetector` struct + integration in audio_thread + 9 unit tests\n- `app/frontend/__tests__/queue-builder.test.js` — 2 regression tests for paginated double-click |
| 119 | +<!-- SECTION:FINAL_SUMMARY:END --> |
0 commit comments