Skip to content

fix(playlists): batch multi-select adds into one playlist edit request#3658

Open
Shrawan13-glitch wants to merge 6 commits intoMetrolistGroup:mainfrom
Shrawan13-glitch:fix-multi-select-playlist-add
Open

fix(playlists): batch multi-select adds into one playlist edit request#3658
Shrawan13-glitch wants to merge 6 commits intoMetrolistGroup:mainfrom
Shrawan13-glitch:fix-multi-select-playlist-add

Conversation

@Shrawan13-glitch
Copy link
Copy Markdown

@Shrawan13-glitch Shrawan13-glitch commented Apr 27, 2026

Problem

Multi-select add-to-playlist was slow and unreliable for large selections. The add work was tied too closely to the UI flow, and the app was issuing one request per song instead of using a single bulk playlist edit.

Cause

The playlist add flow was spread across multiple menu entry points and depended on the dialog/UI staying alive while each individual add request completed. For online selections, the flow also re-resolved songs unnecessarily instead of using the direct add path.

Solution

  • Added support for YouTube Music's multi-select edit command in the InnerTube layer
  • Switched playlist add handling to batch selected songs into a single edit request
  • Updated the shared add-to-playlist flow to use the batch path consistently
  • Simplified online selection handling so selected songs go through the direct add flow instead of a per-song search/re-resolve step

Testing

Manually verified that multi-select add-to-playlist completes correctly and is much faster for large selections.

Related Issues

Summary by CodeRabbit

  • New Features

    • Multi-select support for adding multiple songs to playlists at once.
    • Batch add-and-sync flow to add many items in one operation.
  • Bug Fixes

    • Improved duplicate detection and "skip duplicates" behavior.
    • More reliable ID resolution with fallbacks and robust pending-add tracking.
    • Ensures local insertion of added items so playlist additions persist.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds multi-item playlist operations and centralizes add-and-sync logic: dialog signature gains optional multiSelectParams; menus now return song ID lists and perform local DB inserts only; new InnerTube/YouTube multi-add and multi-select APIs added; PlaylistsViewModel.addSongsAndSync performs batched adds and pending-add sync.

Changes

Cohort / File(s) Summary
Core Dialog Logic
app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt
Adds multiSelectParams: String? = null; resolves/caches song IDs, deduplicates, and delegates add-and-sync to PlaylistsViewModel.addSongsAndSync instead of performing per-item YouTube calls.
Menu Call Sites (UI callbacks simplified)
app/src/main/kotlin/com/metrolist/music/ui/menu/AlbumMenu.kt, app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt, app/src/main/kotlin/com/metrolist/music/ui/menu/QueueMenu.kt, app/src/main/kotlin/com/metrolist/music/ui/menu/SelectionSongsMenu.kt, app/src/main/kotlin/com/metrolist/music/ui/menu/SongMenu.kt, app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeAlbumMenu.kt, app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubePlaylistMenu.kt, app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSongMenu.kt, app/src/main/kotlin/com/metrolist/music/player/MiniPlayer.kt
Removed YouTube-side async side effects from onGetSong handlers; handlers now perform local DB transaction inserts and return song ID lists only.
YouTube Selection Menu
app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSelectionSongMenu.kt
Replaced AddToPlaylistDialogOnline usage with AddToPlaylistDialog; caller no longer prepares mutable DB-entity lists and instead delegates transactional insertion to dialog callback.
Playlists ViewModel
app/src/main/kotlin/com/metrolist/music/viewmodels/PlaylistsViewModel.kt
Adds addSongsAndSync(targetPlaylist, ids, multiSelectParams?): deduplicates IDs, resolves remote IDs via multi-select command (fallback to local IDs), registers pending-adds, calls single or batched YouTube.addToPlaylist, and unregisters pending state. Also makes database private.
InnerTube / YouTube APIs
innertube/src/main/kotlin/com/metrolist/innertube/InnerTube.kt, innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt
Adds InnerTube.addToPlaylist(..., videoIds: List<String>) and InnerTube.getMultiSelectCommand(...); exposes YouTube wrappers for batch add and multi-select command with error handling.
Request/Response Models
innertube/src/main/kotlin/com/metrolist/innertube/models/body/EditPlaylistBody.kt, .../body/GetMultiSelectCommandBody.kt, .../response/GetMultiSelectCommandResponse.kt
Adds dedupeOption to EditPlaylistBody.Action.AddVideoAction; introduces GetMultiSelectCommandBody and GetMultiSelectCommandResponse (with nested addToPlaylistEndpoint.videoIds) to support multi-select resolution.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Menu as Menu/Caller
    participant Dialog as AddToPlaylistDialog
    participant VM as PlaylistsViewModel
    participant DB as LocalDatabase
    participant YT as YouTube/InnerTube

    User->>Menu: select playlist & confirm
    Menu->>Dialog: invoke onGetSong() -> songIds
    activate Dialog
    Dialog->>DB: transaction: insert/check local songs
    DB-->>Dialog: localIds
    Dialog->>Dialog: resolve & dedupe -> distinctIds
    Dialog->>VM: addSongsAndSync(targetPlaylist, distinctIds, multiSelectParams)
    activate VM
    VM->>YT: getMultiSelectCommand(selectedItems, multiSelectParams) [optional]
    YT-->>VM: resolvedVideoIds
    alt resolvedVideoIds present
        VM->>YT: addToPlaylist(playlistId, resolvedVideoIds)  //-- batched
    else
        VM->>YT: addToPlaylist(playlistId, distinctIds)      //-- fallback
    end
    VM->>VM: register/unregister pending-adds (finally)
    YT-->>VM: success/failure
    VM-->>Dialog: done
    deactivate VM
    Dialog-->>User: finished
    deactivate Dialog
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

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 9.52% 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
Title check ✅ Passed The title clearly summarizes the main change: batching multi-select playlist additions into a single edit request instead of individual requests.
Description check ✅ Passed The description follows the template with complete Problem, Cause, Solution, Testing, and Related Issues sections addressing the core issues.
Linked Issues check ✅ Passed The PR fully addresses requirements from issues #2484 (ensuring all selected songs are added) and #3167 (background adds without modal dependency) by batching requests and moving adds to ViewModel scope.
Out of Scope Changes check ✅ Passed All changes are directly related to the batched multi-select playlist add functionality, with no unrelated modifications detected.

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

✨ 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 and usage tips.

Copy link
Copy Markdown
Contributor

@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

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/ui/menu/SelectionSongsMenu.kt (1)

140-149: ⚠️ Potential issue | 🔴 Critical

Critical: addSongsAndSync is launched on a dialog-scoped coroutine that will be cancelled when the dialog is dismissed.

The implementation currently launches addSongsAndSync on rememberCoroutineScope() (line 92, AddToPlaylistDialog.kt), which is tied to the dialog's composition lifecycle. At all three call sites (lines 322, 362, 377), onDismiss() is invoked before or within the same coroutineScope.launch block, causing the dialog to leave composition while addSongsAndSync is still running. Once the composable is removed from the tree, the scope is cancelled, interrupting any pending database transactions and YouTube API calls.

To properly fix #3167 ("adds proceed continuously in background"), addSongsAndSync must be launched on a scope that outlives the dialog—such as a viewmodel scope or an application-scoped sync job—before the dialog is dismissed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/SelectionSongsMenu.kt` around
lines 140 - 149, The dialog currently calls addSongsAndSync from a dialog-scoped
coroutine (via rememberCoroutineScope in AddToPlaylistDialog) so the job is
cancelled when the dialog is dismissed; change the call sites to launch
addSongsAndSync on a longer-lived scope (e.g. a ViewModelScope or an
application-scoped sync job) before dismissing the dialog. Specifically, in
AddToPlaylistDialog replace usage of rememberCoroutineScope-launched
addSongsAndSync with a call into a ViewModel method (or inject a SyncManager)
that performs addSongsAndSync using viewModelScope (or an app-level scope), and
update the three callers (SelectionSongsMenu's usages where onDismiss and
coroutine launch occur) to invoke that ViewModel/SyncManager method and only
then set showChoosePlaylistDialog = false.
🧹 Nitpick comments (5)
app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt (1)

168-177: Consider extracting a shared helper for the repeated onGetSong transaction+ID pattern.

This exact lambda shape is now duplicated across several menus. A small helper (e.g., prepareSongForPlaylist(...)) would reduce drift when this contract changes again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt` around lines
168 - 177, Extract a shared helper to avoid duplicating the transaction+ID
pattern used in AddToPlaylistDialog's onGetSong and similar lambdas: create a
function (e.g., prepareSongForPlaylist(mediaMetadata: MediaMetadata, database:
YourDatabaseType): List<String>) that runs database.withTransaction {
insert(mediaMetadata) } and returns listOf(mediaMetadata.id); then replace the
inline lambdas for onGetSong and onGetSongIds with calls to
prepareSongForPlaylist (or a small wrapper for onGetSongIds if it only needs the
IDs) so AddToPlaylistDialog, other menus, and any future call sites use the
single helper.
app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubePlaylistMenu.kt (1)

147-149: Minor: onGetSongIds may return an empty list when songs is empty.

When YouTubePlaylistMenu is invoked without a preloaded songs list, onGetSong lazily fetches from YouTube.playlist(playlist.id).completed(), but onGetSongIds returns songs.map { it.id } directly — which yields an empty list. The dialog's playlistsContainingSong LaunchedEffect (which only consumes onGetSongIds for read-only highlighting) will therefore not highlight any playlists in this code path, even though the songs would be retrievable.

Since onGetSongIds must remain side-effect-free (no network calls on dialog open per repo learnings), one option is to omit onGetSongIds here so the dialog falls back to invoking onGetSong (with its full resolution) for highlighting. Optional — not a regression introduced by this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubePlaylistMenu.kt`
around lines 147 - 149, The onGetSongIds handler in YouTubePlaylistMenu
currently returns songs.map { it.id } which yields an empty list when the local
songs list is not preloaded; remove or omit setting onGetSongIds so the dialog
will fallback to using onGetSong (which lazily resolves via
YouTube.playlist(playlist.id).completed()) for highlighting, thereby avoiding
returning an empty, misleading list to the playlistsContainingSong
LaunchedEffect.
innertube/src/main/kotlin/com/metrolist/innertube/InnerTube.kt (1)

511-528: LGTM — overload correctly batches multiple AddVideoActions in one edit_playlist request.

The new overload doesn't guard against videoIds.isEmpty(), which would post an actions = emptyList() request. The current caller (addSongsAndSync in AddToPlaylistDialog) only invokes this overload when localIds.size > 1, so this is safe — but require(videoIds.isNotEmpty()) would make the API more robust if reused later.

Regarding batch size: no documented limit on AddVideoAction count per request exists. The ytmusicapi library (reverse-engineered YouTube Music API client) handles batch adds without hardcoded limits, and examples show the approach working for reasonable selections (e.g., albums). The 70+ songs use case should work without chunking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@innertube/src/main/kotlin/com/metrolist/innertube/InnerTube.kt` around lines
511 - 528, The addToPlaylist overload currently allows videoIds to be empty and
would send an edit_playlist request with actions = emptyList(); add a
precondition check in addToPlaylist (e.g., at start of the suspend fun
addToPlaylist) such as require(videoIds.isNotEmpty()) with a clear message (or
throw IllegalArgumentException) to prevent empty-batch requests; ensure the
check references the function name addToPlaylist and leaves the existing body
(httpClient.post(...) / EditPlaylistBody / Action.AddVideoAction) unchanged.
app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSelectionSongMenu.kt (1)

124-137: Remove the multiSelectParams concern; consider adding onGetSongIds in a follow-up.

The multiSelectParams suggestion is not applicable here. SongItem carries no multiSelectParams field, and OnlinePlaylistScreen does not pass one to YouTubeSelectionSongMenu. There is no token available to thread through.

Adding onGetSongIds = { selectedSongs.map { it.id } } would enable highlight-only reads without side effects on dialog open. This aligns with the deferred refactor pattern noted for selection-based menus, so it can be addressed in a follow-up if the maintainer prioritizes splitting the read path from the write path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSelectionSongMenu.kt`
around lines 124 - 137, Remove references to the non-existent multiSelectParams
concern in YouTubeSelectionSongMenu and instead add a new read-only callback to
the AddToPlaylistDialog such as onGetSongIds = { selectedSongs.map { it.id } }
so highlight-only reads can occur without side effects; keep the existing
onGetSong lambda for the write path (database.withTransaction {
selectedSongs.forEach(::insert) }) and preserve onDismiss handling, and update
AddToPlaylistDialog usage/definition to accept this new onGetSongIds parameter
while leaving SongItem and OnlinePlaylistScreen unchanged.
innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt (1)

2600-2606: Nit: drop the redundant local response variable.

The intermediate val response = ...; response doesn't add anything — the call expression itself is already the last expression of the runCatching lambda.

♻️ Optional simplification
     suspend fun getMultiSelectCommand(
         selectedItems: List<String>,
         multiSelectParams: String? = null,
     ) = runCatching {
-        val response = innerTube.getMultiSelectCommand(WEB_REMIX, selectedItems, multiSelectParams).body<GetMultiSelectCommandResponse>()
-        response
+        innerTube.getMultiSelectCommand(WEB_REMIX, selectedItems, multiSelectParams)
+            .body<GetMultiSelectCommandResponse>()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt` around lines
2600 - 2606, In getMultiSelectCommand, remove the redundant local val response
and return the call directly; replace the block inside runCatching so it
directly returns innerTube.getMultiSelectCommand(WEB_REMIX, selectedItems,
multiSelectParams).body<GetMultiSelectCommandResponse>() instead of assigning to
response and then returning it, keeping the suspend function signature and
runCatching usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt`:
- Around line 130-141: The code calls YouTube.getMultiSelectCommand even when
multiSelectParams is null, causing unnecessary network requests; change the
condition that computes remoteIds to only call YouTube.getMultiSelectCommand
when both localIds.size > 1 and multiSelectParams != null (i.e., gate the call
on multiSelectParams), falling back to localIds otherwise; update the remoteIds
computation around getMultiSelectCommand/multiSelectParams to avoid the extra
network hop and preserve the existing orEmpty().ifEmpty { localIds } fallback
logic.

---

Outside diff comments:
In `@app/src/main/kotlin/com/metrolist/music/ui/menu/SelectionSongsMenu.kt`:
- Around line 140-149: The dialog currently calls addSongsAndSync from a
dialog-scoped coroutine (via rememberCoroutineScope in AddToPlaylistDialog) so
the job is cancelled when the dialog is dismissed; change the call sites to
launch addSongsAndSync on a longer-lived scope (e.g. a ViewModelScope or an
application-scoped sync job) before dismissing the dialog. Specifically, in
AddToPlaylistDialog replace usage of rememberCoroutineScope-launched
addSongsAndSync with a call into a ViewModel method (or inject a SyncManager)
that performs addSongsAndSync using viewModelScope (or an app-level scope), and
update the three callers (SelectionSongsMenu's usages where onDismiss and
coroutine launch occur) to invoke that ViewModel/SyncManager method and only
then set showChoosePlaylistDialog = false.

---

Nitpick comments:
In `@app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt`:
- Around line 168-177: Extract a shared helper to avoid duplicating the
transaction+ID pattern used in AddToPlaylistDialog's onGetSong and similar
lambdas: create a function (e.g., prepareSongForPlaylist(mediaMetadata:
MediaMetadata, database: YourDatabaseType): List<String>) that runs
database.withTransaction { insert(mediaMetadata) } and returns
listOf(mediaMetadata.id); then replace the inline lambdas for onGetSong and
onGetSongIds with calls to prepareSongForPlaylist (or a small wrapper for
onGetSongIds if it only needs the IDs) so AddToPlaylistDialog, other menus, and
any future call sites use the single helper.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubePlaylistMenu.kt`:
- Around line 147-149: The onGetSongIds handler in YouTubePlaylistMenu currently
returns songs.map { it.id } which yields an empty list when the local songs list
is not preloaded; remove or omit setting onGetSongIds so the dialog will
fallback to using onGetSong (which lazily resolves via
YouTube.playlist(playlist.id).completed()) for highlighting, thereby avoiding
returning an empty, misleading list to the playlistsContainingSong
LaunchedEffect.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSelectionSongMenu.kt`:
- Around line 124-137: Remove references to the non-existent multiSelectParams
concern in YouTubeSelectionSongMenu and instead add a new read-only callback to
the AddToPlaylistDialog such as onGetSongIds = { selectedSongs.map { it.id } }
so highlight-only reads can occur without side effects; keep the existing
onGetSong lambda for the write path (database.withTransaction {
selectedSongs.forEach(::insert) }) and preserve onDismiss handling, and update
AddToPlaylistDialog usage/definition to accept this new onGetSongIds parameter
while leaving SongItem and OnlinePlaylistScreen unchanged.

In `@innertube/src/main/kotlin/com/metrolist/innertube/InnerTube.kt`:
- Around line 511-528: The addToPlaylist overload currently allows videoIds to
be empty and would send an edit_playlist request with actions = emptyList(); add
a precondition check in addToPlaylist (e.g., at start of the suspend fun
addToPlaylist) such as require(videoIds.isNotEmpty()) with a clear message (or
throw IllegalArgumentException) to prevent empty-batch requests; ensure the
check references the function name addToPlaylist and leaves the existing body
(httpClient.post(...) / EditPlaylistBody / Action.AddVideoAction) unchanged.

In `@innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt`:
- Around line 2600-2606: In getMultiSelectCommand, remove the redundant local
val response and return the call directly; replace the block inside runCatching
so it directly returns innerTube.getMultiSelectCommand(WEB_REMIX, selectedItems,
multiSelectParams).body<GetMultiSelectCommandResponse>() instead of assigning to
response and then returning it, keeping the suspend function signature and
runCatching usage 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 645d32a4-50aa-4b3b-85af-23856c995ef5

📥 Commits

Reviewing files that changed from the base of the PR and between 257b8c0 and d84670c.

📒 Files selected for processing (16)
  • app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/AlbumMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/QueueMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/SelectionSongsMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/SongMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeAlbumMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubePlaylistMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSelectionSongMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSongMenu.kt
  • app/src/main/kotlin/com/metrolist/music/ui/player/MiniPlayer.kt
  • innertube/src/main/kotlin/com/metrolist/innertube/InnerTube.kt
  • innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt
  • innertube/src/main/kotlin/com/metrolist/innertube/models/body/EditPlaylistBody.kt
  • innertube/src/main/kotlin/com/metrolist/innertube/models/body/GetMultiSelectCommandBody.kt
  • innertube/src/main/kotlin/com/metrolist/innertube/models/response/GetMultiSelectCommandResponse.kt

Comment thread app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt Outdated
Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (2)
app/src/main/kotlin/com/metrolist/music/viewmodels/PlaylistsViewModel.kt (2)

82-92: Consider logging YouTube.addToPlaylist failures.

Both overloads return Result<…> (see innertube/.../YouTube.kt:2586-2598) but the result is discarded. On a network/API failure for a synced playlist, songs land in the local DB but never reach YouTube; unregisterPendingAdd then polls for ~30 s, finds nothing, and silently unregisters. That's a regression risk for the #2484 reliability fix this PR targets — at minimum, log the failure so it's diagnosable from logs/Timber.

♻️ Proposed change
             try {
-                if (remoteIds.size == 1) {
-                    YouTube.addToPlaylist(browseId, remoteIds.first())
-                } else {
-                    YouTube.addToPlaylist(browseId, remoteIds)
-                }
+                val result = if (remoteIds.size == 1) {
+                    YouTube.addToPlaylist(browseId, remoteIds.first())
+                } else {
+                    YouTube.addToPlaylist(browseId, remoteIds)
+                }
+                result.onFailure {
+                    Timber.e(it, "addSongsAndSync: addToPlaylist failed for browseId=$browseId, ${remoteIds.size} ids")
+                }
             } finally {
                 remoteIds.forEach { songId ->
                     syncUtils.unregisterPendingAdd(browseId, songId)
                 }
             }

(Add import timber.log.Timber if not already present.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/viewmodels/PlaylistsViewModel.kt`
around lines 82 - 92, The call to YouTube.addToPlaylist in PlaylistsViewModel
discards its Result so failures are invisible; change the branches that call
YouTube.addToPlaylist (both the overload taking a single id and the one taking a
list) to capture the returned Result, check for failure, and log the error using
Timber (include contextual info like browseId and song id(s)). After calling
addToPlaylist, if result.isFailure (or result.exceptionOrNull() != null) call
Timber.e(...) with the exception and a clear message; keep the existing finally
block that calls syncUtils.unregisterPendingAdd.

62-94: Guard against empty ids to avoid a wasteful YouTube edit call.

addSongsAndSync is reachable from the dialog's "Skip duplicates" path with an empty filtered list (songIds!!.filter { !duplicates.contains(it) } when all selections are duplicates) and from any onGetSong that returns an empty list. In that case database.addSongsToPlaylist is a no-op, but for synced playlists you still hit YouTube.addToPlaylist(browseId, emptyList()) (line 86), which sends an empty editPlaylist request. An early return after resolving localIds is cheap insurance.

♻️ Proposed change
     fun addSongsAndSync(
         targetPlaylist: Playlist,
         ids: List<String>,
         multiSelectParams: String? = null,
     ) {
         viewModelScope.launch(Dispatchers.IO) {
             val localIds = ids.distinct()
+            if (localIds.isEmpty()) return@launch
             database.addSongsToPlaylist(targetPlaylist, localIds.map { it to null }, prepend = true)
             val browseId = targetPlaylist.playlist.browseId ?: return@launch
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/viewmodels/PlaylistsViewModel.kt`
around lines 62 - 94, The addSongsAndSync handler should bail out when the
computed localIds is empty to avoid sending an empty YouTube edit request; after
computing val localIds = ids.distinct() add a guard like if (localIds.isEmpty())
return@launch so you skip database.addSongsToPlaylist, the sync registration
loop, and the YouTube.addToPlaylist calls (refer to addSongsAndSync /
viewModelScope.launch, localIds, database.addSongsToPlaylist,
syncUtils.registerPendingAdd/unregisterPendingAdd, and YouTube.addToPlaylist).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/kotlin/com/metrolist/music/viewmodels/PlaylistsViewModel.kt`:
- Around line 82-92: The call to YouTube.addToPlaylist in PlaylistsViewModel
discards its Result so failures are invisible; change the branches that call
YouTube.addToPlaylist (both the overload taking a single id and the one taking a
list) to capture the returned Result, check for failure, and log the error using
Timber (include contextual info like browseId and song id(s)). After calling
addToPlaylist, if result.isFailure (or result.exceptionOrNull() != null) call
Timber.e(...) with the exception and a clear message; keep the existing finally
block that calls syncUtils.unregisterPendingAdd.
- Around line 62-94: The addSongsAndSync handler should bail out when the
computed localIds is empty to avoid sending an empty YouTube edit request; after
computing val localIds = ids.distinct() add a guard like if (localIds.isEmpty())
return@launch so you skip database.addSongsToPlaylist, the sync registration
loop, and the YouTube.addToPlaylist calls (refer to addSongsAndSync /
viewModelScope.launch, localIds, database.addSongsToPlaylist,
syncUtils.registerPendingAdd/unregisterPendingAdd, and YouTube.addToPlaylist).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25f6741f-c615-47cd-a414-3311700219b0

📥 Commits

Reviewing files that changed from the base of the PR and between 21ef357 and 0e86c9a.

📒 Files selected for processing (2)
  • app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt
  • app/src/main/kotlin/com/metrolist/music/viewmodels/PlaylistsViewModel.kt

Copy link
Copy Markdown
Contributor

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

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/ui/menu/AddToPlaylistDialog.kt (1)

322-334: ⚠️ Potential issue | 🟡 Minor

Skip-duplicates can dispatch an empty add when every song is a duplicate.

When duplicates.size == songIds!!.size (all selected songs already exist in the target playlist), the filter { !duplicates.contains(it) } produces an empty list. viewModel.addSongsAndSync will still be invoked, which in turn calls YouTube.addToPlaylist(browseId, emptyList()) (the else branch of the size == 1 check in the ViewModel) — a wasted edit request to YouTube Music. Consider short‑circuiting when the filtered list is empty.

♻️ Proposed fix
                     TextButton(
                         onClick = {
                             showDuplicateDialog = false
                             onDismiss()
-                            viewModel.addSongsAndSync(
-                                selectedPlaylist!!,
-                                songIds!!.filter { !duplicates.contains(it) },
-                                multiSelectParams,
-                            )
+                            val toAdd = songIds!!.filter { it !in duplicates }
+                            if (toAdd.isNotEmpty()) {
+                                viewModel.addSongsAndSync(
+                                    selectedPlaylist!!,
+                                    toAdd,
+                                    multiSelectParams,
+                                )
+                            }
                         }
                     ) {
                         Text(stringResource(R.string.skip_duplicates))
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt`
around lines 322 - 334, The skip-duplicates button can call
viewModel.addSongsAndSync with an empty list when all selected songs are
duplicates; change the onClick handler in AddToPlaylistDialog so it computes the
filtered list (songIds!!.filter { !duplicates.contains(it) }) into a local val
and only calls viewModel.addSongsAndSync(selectedPlaylist!!, filtered,
multiSelectParams) if filtered.isNotEmpty(); otherwise just clear
showDuplicateDialog and call onDismiss() (no YouTube.addToPlaylist request).
Keep references to selectedPlaylist, songIds, duplicates, and
viewModel.addSongsAndSync to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt`:
- Around line 322-334: The skip-duplicates button can call
viewModel.addSongsAndSync with an empty list when all selected songs are
duplicates; change the onClick handler in AddToPlaylistDialog so it computes the
filtered list (songIds!!.filter { !duplicates.contains(it) }) into a local val
and only calls viewModel.addSongsAndSync(selectedPlaylist!!, filtered,
multiSelectParams) if filtered.isNotEmpty(); otherwise just clear
showDuplicateDialog and call onDismiss() (no YouTube.addToPlaylist request).
Keep references to selectedPlaylist, songIds, duplicates, and
viewModel.addSongsAndSync to locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 33d0b299-0386-4e1f-af53-d4a564cdee6f

📥 Commits

Reviewing files that changed from the base of the PR and between 0e86c9a and 6a29ff6.

📒 Files selected for processing (1)
  • app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt

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.

Bug while adding multiple songs in a playlist at once Multiple selection not working for playlists

2 participants