Skip to content

fix(playlists): sync playlist removals from menus - #4097

Open
yeongjo wants to merge 5 commits into
MetrolistGroup:mainfrom
yeongjo:main
Open

yeongjo wants to merge 5 commits into
MetrolistGroup:mainfrom
yeongjo:main

Conversation

@yeongjo

@yeongjo yeongjo commented Jul 5, 2026 •

Copy link
Copy Markdown

Problem

The player menu could not reliably identify the playlist occurrence behind the current song, so Remove from playlist was missing or could act on incomplete playlist context.

Changes

  • Carry playlist identity and per-occurrence IDs through local and online playback, widget/alarm queues, and queue persistence.
  • Remove only the requested occurrence. Resolve missing IDs conservatively and stop when the target is ambiguous.
  • Keep local rows until YouTube confirms success, or a complete fetch confirms the exact occurrence is already absent. Reject unsuccessful edit responses even when HTTP succeeds.
  • Offer a playlist/copy chooser when the current song has no actionable queue context, including local-only additions that never reached YouTube.
  • Refresh playlist views after edits, keep deleted occurrences out of stale responses, and invalidate the removed occurrence's player-menu context.
  • Restore a swiped row after a failed removal so the same row can be retried.

Review follow-up

  • Rebased onto MetrolistGroup/Metrolist:main at 876e35518. The PR contains five commits; equivalent upstream commits are excluded.
  • Preserve historical saved queues, automix queues, and playback position with pinned serialization IDs, guarded compatibility handling, and release keep rules. Historical serialized fixtures cover upgrade compatibility; the previously documented one-time queue loss is no longer expected for those supported formats.
  • Include playlist occurrence metadata when playback starts from widgets and alarms.
  • Cover remote rejection, network failure, repeated removal, retry, pending additions, and duplicate occurrence identity with integration/unit tests.
  • Use occurrence identity for online playlist selection and playback matching. Refresh count metadata from YouTube instead of editing localized count strings.
  • The pre-existing duplicate-row playback highlighting/click behavior in local playlists remains outside this PR and will be handled separately.

Validation

  • After rebase: ./gradlew :app:assembleFossDebug :app:testFossDebugUnitTest :innertube:testDebugUnitTest completed successfully. App: 296 tests; innertube: 39 tests. Zero failures, errors, or skipped tests.
  • Galaxy S23 (SM-S911N), after rebase at 2d4ba68f2: installed the final debug APK and repeated the gg playlist test with two copies of the same song. With Wi-Fi and mobile data disabled, a failed swipe removal restored both rows to their original positions; swiping that same row again issued a second request and restored the rows again. Confirmed two failure callbacks in device logs and no playlist row loss. Network, edit lock, and swipe settings were restored afterward.
  • git diff --check passes.

Summary by CodeRabbit

  • New Features

    • Added support for removing songs from editable playlists during playback.
    • Added “Remove from playlist” actions to song and player menus.
    • Preserved playlist details and song mappings during playback and queue restoration.
  • Bug Fixes

    • Playlist views now accurately reflect removed songs.
    • Improved handling of songs with matching identifiers and playlist mappings.
  • Chores

    • Improved queue and playlist metadata handling across playback screens.
    • Added more reliable queue persistence and restoration.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; all previous findings are addressed and no new actionable issues were found.

Summary

The PR carries playlist and occurrence details into playback queues and routes menu removals through SyncUtils.

  • Playlist playback keeps its source details in the queue.
  • Menus remove a specific song copy from an editable playlist.
  • Online playlists keep duplicate song copies separate.

Reviews (2) · Last reviewed commit: "fix(playlists): preserve queues and reco..." · Reviewed by Greptile

@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

This PR adds editable-playlist metadata to queue types and persisted queues. Playlist playback passes this metadata to queues and menus. Removal actions schedule synchronization and update matching local playlist state.

Changes: Editable Playlist Removal

Layer / File(s) Summary
Queue playlist context and state
app/.../playback/queues/ListQueue.kt, app/.../playback/queues/YouTubePlaylistQueue.kt, app/.../playback/MusicService.kt
Queues expose playlist identifiers and editability. MusicService publishes and reads the active queue through StateFlow.
Playlist metadata persistence
app/.../models/PersistQueue.kt, app/.../extensions/QueueExt.kt
Persisted queues store and restore playlist identifiers and editability.
Playlist playback and local state wiring
app/.../ui/screens/playlist/LocalPlaylistScreen.kt, app/.../ui/screens/playlist/OnlinePlaylistScreen.kt, app/.../viewmodels/OnlinePlaylistViewModel.kt, app/.../playback/MusicService.kt
Playlist screens, widgets, and alarm playback pass playlist context to queues and menus. Local removal matches both song ID and set video ID.
Remove-from-playlist actions
app/.../ui/menu/PlayerMenu.kt, app/.../ui/menu/YouTubeSongMenu.kt
Menus schedule removal for editable playlists, update local mappings when applicable, log the operation, show a toast, and dismiss the menu.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PlayerMenu
  participant LocalSyncUtils
  participant OnlinePlaylistViewModel
  User->>PlayerMenu: select remove from playlist
  PlayerMenu->>LocalSyncUtils: scheduleRemoveFromPlaylist(playlistId, setVideoId)
  PlayerMenu->>OnlinePlaylistViewModel: remove matching local song when applicable
  PlayerMenu->>User: show toast and dismiss menu
Loading

Possibly related PRs

Suggested reviewers: nyxiereal

Pre-merge checks: Passed 4
Check name Status Explanation
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.
Title check Passed The title clearly identifies the main change: synchronizing playlist removals initiated from menus.
Description check Passed The description clearly explains the problem, the implemented solution, and extensive validation results. It does not include the template headings for Cause or Related Issues, and it uses Changes and…
Full details: Description check

Explanation

The description clearly explains the problem, the implemented solution, and extensive validation results. It does not include the template headings for Cause or Related Issues, and it uses Changes and Validation instead of Solution and Testing, but the required information is mostly present.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@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
`@app/src/main/kotlin/com/metrolist/music/playback/queues/YouTubePlaylistQueue.kt`:
- Around line 28-50: `YouTubePlaylistQueue` is accessing `setVideoIds` from
multiple dispatchers, which can cause the recorded `setVideoId` to be missed
when `PlayerMenu` reads it on Main while `toPlaylistItem` writes it on IO. Make
`setVideoIds` thread-safe or confine all reads/writes to the same dispatcher,
and ensure both `getSetVideoId` and `SongItem.toPlaylistItem` use the same
concurrency strategy so the mapping remains visible across
`YouTubePlaylistQueue` and `PlayerMenu`.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt`:
- Around line 158-189: The ListQueue removal target logic in PlayerMenu’s
produceState block is missing the same editability gate used for
YouTubePlaylistQueue, so non-editable local playlists can still show “Remove
from playlist.” Add an isEditable property to ListQueue, pass it through where
LocalPlaylistScreen constructs ListQueue, and then require queue.isEditable ==
true before creating the PlaylistRemovalTarget for the ListQueue branch in
PlayerMenu.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40361d10-714d-4745-ac5b-ad7eb96c9ba0

📥 Commits

Reviewing files that changed from the base of the PR and between ece6c15 and a7b7037.

📒 Files selected for processing (8)
  • app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt
  • app/src/main/kotlin/com/metrolist/music/playback/queues/ListQueue.kt
  • app/src/main/kotlin/com/metrolist/music/playback/queues/YouTubePlaylistQueue.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSongMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/LocalPlaylistScreen.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/OnlinePlaylistScreen.kt
  • app/src/main/kotlin/com/metrolist/music/viewmodels/OnlinePlaylistViewModel.kt

@nyxiereal nyxiereal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Song ID maps collapse duplicate playlist occurrences and can remove the wrong item. Please carry occurrence identity, gate removal on playlist editability, publish metadata safely across threads, and test duplicate songs.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt (1)

4296-4296: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate playlist context through every playlist queue producer.

The new metadata is passed only in the uncached online branch at Line [4296]. The cached online branch at Lines [4289-4292], the local widget branch at Lines [4278-4281], and alarm playback at Lines [4410-4415] still create ListQueue without playlistBrowseId, playlistId, or playlistIsEditable.

Songs started through these paths reach PlayerMenu without editable-playlist context. Remove from playlist remains unavailable. Populate the metadata in every playlist queue producer.

Based on the PR objectives and the supplied queue-construction call sites.

🤖 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 `@app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt` at line
4296, Update every playlist-related ListQueue construction in MusicService,
including the cached online branch, local widget branch, and alarm playback
path, to populate playlistBrowseId, playlistId, and playlistIsEditable
consistently with the already-updated uncached online branch. Ensure all
playlist queue producers propagate the available playlist context so PlayerMenu
receives editable-playlist metadata.
🤖 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.

Outside diff comments:
In `@app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt`:
- Line 4296: Update every playlist-related ListQueue construction in
MusicService, including the cached online branch, local widget branch, and alarm
playback path, to populate playlistBrowseId, playlistId, and playlistIsEditable
consistently with the already-updated uncached online branch. Ensure all
playlist queue producers propagate the available playlist context so PlayerMenu
receives editable-playlist metadata.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d66549e8-01aa-4659-a28c-2f63542b9225

📥 Commits

Reviewing files that changed from the base of the PR and between de5a677 and abcec53.

📒 Files selected for processing (9)
  • app/src/main/kotlin/com/metrolist/music/extensions/QueueExt.kt
  • app/src/main/kotlin/com/metrolist/music/models/PersistQueue.kt
  • app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt
  • app/src/main/kotlin/com/metrolist/music/playback/queues/ListQueue.kt
  • app/src/main/kotlin/com/metrolist/music/playback/queues/YouTubePlaylistQueue.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/LocalPlaylistScreen.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/OnlinePlaylistScreen.kt
  • app/src/main/kotlin/com/metrolist/music/viewmodels/OnlinePlaylistViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/OnlinePlaylistScreen.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/LocalPlaylistScreen.kt

@nyxiereal

Copy link
Copy Markdown
Member

The final occurrence/editability handling looks improved, but this is a destructive and failure-sensitive path without a current build or regression coverage. Please rebase and add tests for duplicate song occurrences, setVideoId identity, restored queues, non-editable playlists, and remote removal failure/retry before merge.

@nyxiereal

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-01T05:50:13.945592Z 5cf7319 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cf7319678

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +150 to +152
val playlistRemovalTarget = remember(currentQueue, mediaMetadata.setVideoId) {
resolvePlaylistRemovalTarget(currentQueue, mediaMetadata.setVideoId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Derive playlist context from each queued item

When a song from playlist B is added via Play next/Add to queue while playlist A is playing, currentQueue remains A but the appended item's setVideoId belongs to B. This resolves a removal target for A, so the player menu exposes an action that sends B's occurrence ID to the wrong playlist and normally fails to remove anything. Associate the playlist ID with the media item, or suppress the action when the item cannot be proven to belong to the queue's playlist.

Useful? React with 👍 / 👎.

Comment on lines +4380 to +4382
playlistBrowseId = playlist?.browseId,
playlistId = playlist?.id,
playlistIsEditable = playlist?.isEditable == true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve occurrence IDs in widget and alarm queues

For an editable playlist started from a widget, the new playlist context is attached but items = songs.map { it.song.toMediaItem() } discards PlaylistSong.map.setVideoId; resolvePlaylistRemovalTarget therefore always returns null and the player menu still lacks Remove from playlist. The cached-online widget path and alarm path construct items the same way, so they need the occurrence-aware conversion used by Playlist.toListQueue.

Useful? React with 👍 / 👎.

Comment on lines +340 to +342
onSongRemovedFromPlaylist = {
songItem.setVideoId?.let { setVideoId ->
viewModel.removeSongFromLocalList(songItem.id, setVideoId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Key duplicate playlist rows by occurrence

When an online playlist contains the same video more than once, this commit now retains both occurrences and removes one by setVideoId, but OnlinePlaylistScreen still tracks selection, active state, anchors, and click behavior solely by songItem.id. Selecting either occurrence consequently checks both, and tapping the second while the first is active executes togglePlayPause() instead of playing that occurrence. Use a compound occurrence identity such as video ID plus setVideoId throughout this screen.

Useful? React with 👍 / 👎.

Comment on lines +274 to +277
val match = Regex("""\d+""").find(countText)
if (match != null) {
val count = match.value.toIntOrNull() ?: return@let countText
countText.replace(match.value, (count - 1).coerceAtLeast(0).toString())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse formatted song counts before decrementing

When YouTube supplies a formatted count such as 1,234 songs or 1.2K songs, Regex("""\d+""") matches only the initial 1, producing values such as 0,234 songs after one removal. Avoid editing the server-formatted label this way, or parse the complete localized/abbreviated count before decrementing it.

Useful? React with 👍 / 👎.

@nyxiereal nyxiereal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please await remote success or support rollback before deleting local/UI rows, preserve old saved queues, and make duplicate playback/selection occurrence-aware. Also fix formatted counts and carry setVideoId through widget/alarm queues. The new helper tests are useful, but add integration coverage for these failure and upgrade paths before merging.

@yeongjo

yeongjo commented Oct 11, 2026

Copy link
Copy Markdown
Author

Testing this build on my phone now. I'll post an update once verified.

Also, I wasn't aware that pushing to my repo would auto-update the PR—sorry for the extra notifications, I'll be more careful!

@nyxiereal nyxiereal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The occurrence-target helpers and new tests improve this patch, but the destructive and upgrade paths still need changes:

  1. PlayerMenu.kt:524-545 schedules remote removal and then deletes the local mapping without awaiting remote success. YouTubeSongMenu likewise removes the visible row immediately. If the queued remote edit fails or is skipped, local/UI state no longer matches YouTube. Await a success result or implement rollback before committing those removals.
  2. Adding fields to Java-serialized PersistQueue changes its automatically computed serialVersionUID. The documented one-time loss of users' saved queue/playback position is avoidable. Preserve compatibility and add an old-version serialized-queue fixture test; a new-version round trip does not test upgrades.
  3. After removing song-ID deduplication, OnlinePlaylistScreen still identifies playing and selected rows by song ID. With two occurrences of a song, tapping the second can pause the first, and selecting one selects both. Carry occurrence identity through playback and selection.
  4. removeSongFromLocalList parses only the first digit run: 1,000 songs becomes 0,000 songs after one removal. Use a numeric count, or parse/format the whole localized value safely. Widget/alarm items also still omit each row's setVideoId, so the new removal action remains unavailable there.

Please add failure/rollback, upgrade, and duplicate-occurrence integration regressions before merge. The helper tests alone do not exercise these data-loss and wrong-row behaviors.

yeongjo and others added 5 commits October 31, 2026 21:36
Removing a song from a playlist is destructive and irreversible from the
UI, so the code that decides *which* row to delete now lives in pure
functions that can be tested directly, instead of inside composables and
a fire-and-forget sync coroutine.

- Extract resolvePlaylistRemovalTarget() and selectLocalPlaylistRowToRemove()
  out of PlayerMenu, so occurrence identity and editability rules are
  testable and shared.
- Extract resolveSetVideoIdForRemoval() out of SyncUtils, making the
  remote lookup budget injectable and pinning the rule that a removal is
  abandoned - never guessed - when no setVideoId can be resolved.
- Deduplicate the three playlist queue builders in LocalPlaylistScreen
  into Playlist.toListQueue().
- Drop the SongItem.toPlaylistItem() indirection, which just called
  toMediaItem().

Tests cover duplicate occurrences of one song, setVideoId identity,
queues restored from disk, non-editable playlists, and remote lookup
failure and retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yeongjo

yeongjo commented Oct 31, 2026

Copy link
Copy Markdown
Author

I've rebased onto the latest main and pushed the follow-up fixes.
The build and all 335 unit tests pass.
I also verified swipe-removal failure recovery and retry on a Galaxy S23.
The PR description has been updated. Thanks!

This branch has not been deployed

No deployments
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.

2 participants