Skip to content

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

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

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

Conversation

@yeongjo

@yeongjo yeongjo commented Jul 5, 2026 •

Copy link
Copy Markdown

Problem

The Remove from playlist action was missing from the player menu when
selecting a currently playing song from a playlist.

Cause

The player menu could not reliably identify the playlist context for the
currently playing item. Playlist browse IDs and set video IDs were
available in some queue paths, but the local playlist ID was not preserved,

Solution

  • Preserve the local playlist ID in list queues.
  • Pass local playlist IDs when starting playback from local playlists.
  • Use queue playlist context to determine when Remove from playlist
    should be shown in the player menu.
  • Schedule playlist removals through sync utilities instead of relying on
    direct removal calls from the menu.
  • Remove playlist request metadata handling from YouTube playlist queue
    items.
  • Keep the YouTube song menu remove action limited to editable playlists.

Review follow-up

Addressing the request for a rebase and regression coverage on this
destructive path:

  • Rebased onto current main; the two merge commits are gone and the
    branch is now a clean series on top of chore(deps): bump InnerTubeX to 0.4.1.
  • Fixed a build break that the rebase surfaced: adoptQueue() (added
    upstream in Changed the logic of the queue created by AA search #4214) assigned to currentQueue, which this PR turns into
    a read-only StateFlow. It now writes _currentQueue.value.
  • Moved the logic that decides which row to delete out of composables
    and out of the fire-and-forget sync coroutine into pure functions, so it
    can be tested directly:
    • resolvePlaylistRemovalTarget() / selectLocalPlaylistRowToRemove()
      (extracted from PlayerMenu)
    • resolveSetVideoIdForRemoval() (extracted from SyncUtils), which
      makes the remote lookup budget injectable and pins the rule that a
      removal is abandoned, never guessed, when no setVideoId resolves.
  • Deduplicated the three playlist queue builders in LocalPlaylistScreen
    into Playlist.toListQueue(), and dropped the SongItem.toPlaylistItem()
    indirection that just called toMediaItem().

Regression coverage

23 new unit tests, covering each case raised in review:

Case Tests
Duplicate song occurrences duplicate occurrences of one song resolve to different targets, local row lookup picks the occurrence matching the set video id
setVideoId identity missing or blank set video id yields no target, local row lookup ignores the same song in other playlists, local row lookup returns null when no occurrence matches
Restored queues QueuePlaylistContextTest (persist/restore round trip, Java serialization round trip, per-occurrence targets after restore)
Non-editable playlists non editable youtube playlist yields no target, non editable list queue yields no target, list queue without browse id yields no target
Remote removal failure / retry SetVideoIdResolverTest (retry until success, retry after a thrown lookup, give up after the attempt budget instead of removing an unknown occurrence)

Known migration cost

PersistQueue gains three fields, which changes its Java
serialVersionUID. A queue persisted by an older build therefore fails to
deserialize once on upgrade. The failure is already caught and the file is
cleared, so there is no crash — users lose the restored queue a single
time, and it is repopulated on the next playback. Flagging it rather than
pinning the previous UID; happy to do that instead if you would prefer to
keep old queues readable.

Testing

  • ./gradlew :app:assembleFossDebug :app:testFossDebugUnitTest — build successful,
    new tests pass (23 tests, 0 failures, 0 errors).
  • Tested on Galaxy S23 (Android 16, One UI 8.5).
  • Verified the 'Remove from playlist' action across multiple playlists, cross-checking with YouTube to ensure consistent state sync.

Related Issues

  • Closes #
  • Related to #

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.

@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

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 | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
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 and concisely describes the main change: synchronizing playlist removals initiated from menus.
Description check ✅ Passed The description includes the required Problem, Cause, Solution, Testing, and Related Issues sections. It provides detailed context, implementation changes, regression coverage, and test results. The R…
Full details: Description check

Explanation

The description includes the required Problem, Cause, Solution, Testing, and Related Issues sections. It provides detailed context, implementation changes, regression coverage, and test results. The Related Issues placeholders remain empty, but this is non-critical because the rest of the description is complete.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Comment thread app/src/main/kotlin/com/metrolist/music/playback/queues/YouTubePlaylistQueue.kt Outdated
Comment thread app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt Outdated

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

yeongjo and others added 4 commits September 1, 2026 14:18
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>
@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 👍 / 👎.

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