Skip to content

Playlist multi add - pre selection of selected lists#4026

Draft
SergioEstevao wants to merge 3 commits into
trunkfrom
sergio/playlist_selection
Draft

Playlist multi add - pre selection of selected lists#4026
SergioEstevao wants to merge 3 commits into
trunkfrom
sergio/playlist_selection

Conversation

@SergioEstevao

Copy link
Copy Markdown
Contributor

| 📘 Part of: # |
|:---:|

Fixes PCIOS-

To test

Checklist

  • I have considered if this change warrants user-facing release notes and have added them to CHANGELOG.md if necessary.
  • I have considered adding unit tests for my changes.
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics.

@SergioEstevao SergioEstevao added this to the 8.6 ❄️ milestone Feb 26, 2026
Copilot AI review requested due to automatic review settings February 26, 2026 22:11
@dangermattic

Copy link
Copy Markdown
Collaborator
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

Copilot AI 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.

Pull request overview

This PR adds functionality to pre-select playlists that contain all episodes in a multi-episode selection when adding episodes to manual playlists. The change allows the UI to show which playlists already contain all the selected episodes, improving the user experience in the playlist multi-add flow.

Changes:

  • Added a new public API method in DataManager to get manual playlist UUIDs for multiple episodes
  • Implemented the underlying database query in PlaylistDataManager to find playlists containing all specified episodes
  • The ManualPlaylistsChooserViewController now uses this method to pre-select common playlists

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
Modules/DataModel/Sources/PocketCastsDataModel/Public/DataManager.swift Adds public API method manualPlaylistUUIDs(for:) that accepts an array of episode UUIDs
Modules/DataModel/Sources/PocketCastsDataModel/Private/Managers/PlaylistDataManager.swift Implements database query to find playlists containing all episodes from an array using SQL IN clause and HAVING COUNT

Comment on lines +189 to +214
func manualPlaylistUUIDs(for episodesUUIDs: [String], dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodesUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count);
"""
let resultSet = try db.executeQuery(query, values: [])
defer { resultSet.close() }

while resultSet.next() {
if let uuid = resultSet.string(forColumn: "playlist_uuid") {
uuids.append(uuid)
}
}
} catch {
FileLog.shared.addMessage("PlaylistDataManager.manualPlaylistUUIDs error: \(error)")
}
}
return uuids
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

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

Missing empty array guard. The function should handle empty arrays gracefully by returning early. When an empty array is passed to convertArrayToInString, it produces an empty string wrapped in quotes (''), which results in invalid SQL syntax. Other similar functions in the codebase (e.g., UserEpisodeDataManager.delete at line 546, PlaylistDataManager.deleteEpisodes at line 297) use a guard statement to check for empty arrays before proceeding with database operations.

Copilot uses AI. Check for mistakes.

func manualPlaylistUUIDs(for episodesUUIDs: [String], dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

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

Commented-out code should be removed. The commented line appears to be leftover from development and should be deleted to keep the code clean.

Suggested change
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +199
func manualPlaylistUUIDs(for episodesUUIDs: [String], dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodesUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count);

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

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

Inconsistent parameter naming. The parameter is named 'episodesUUIDs' (with an 's' before 'UUIDs') which is inconsistent with the existing single-episode method parameter 'episodeUUID' and the public API parameter 'episodeUUIDs'. The parameter should be renamed to 'episodeUUIDs' for consistency.

Suggested change
func manualPlaylistUUIDs(for episodesUUIDs: [String], dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodesUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count);
func manualPlaylistUUIDs(for episodeUUIDs: [String], dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
//let episodeUUIDs = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodeUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodeUUIDs.count);

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +214
func manualPlaylistUUIDs(for episodesUUIDs: [String], dbQueue: PCDBQueue) -> [String] {
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodesUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count);
"""
let resultSet = try db.executeQuery(query, values: [])
defer { resultSet.close() }

while resultSet.next() {
if let uuid = resultSet.string(forColumn: "playlist_uuid") {
uuids.append(uuid)
}
}
} catch {
FileLog.shared.addMessage("PlaylistDataManager.manualPlaylistUUIDs error: \(error)")
}
}
return uuids
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

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

Missing test coverage for the new function. The PlaylistDataManagerTests file contains comprehensive test coverage for other PlaylistDataManager methods, but this new function that returns playlists containing all episodes from an array lacks test coverage. Consider adding tests to verify the behavior with multiple episodes, empty arrays, episodes that are only in some playlists, and episodes that share common playlists.

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +201
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodesUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count);
"""
let resultSet = try db.executeQuery(query, values: [])

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

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

manualPlaylistUUIDs(for episodesUUIDs: [String]) builds a SQL IN clause by interpolating the result of DataHelper.convertArrayToInString(episodesUUIDs) directly into the query string, which is vulnerable to SQL injection if any episodeUuid contains quotes or SQL metacharacters. Since episode.uuid values are populated from server JSON (episodeJson["uuid"]) and then passed here (e.g. via episodes.map { $0.uuid }), an attacker controlling server responses or intercepting traffic could inject arbitrary SQL and read or modify data in the local database. Instead of string-concatenating the IN list, construct the query using ? placeholders for each UUID and pass the episode UUIDs via the values array so the database layer performs proper escaping/binding.

Suggested change
var uuids: [String] = []
//let episodesUUIDS = episodeUUIDs.joined(separator: ",")
dbQueue.read { db in
do {
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(DataHelper.convertArrayToInString(episodesUUIDs)))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = \(episodesUUIDs.count);
"""
let resultSet = try db.executeQuery(query, values: [])
// If no episode UUIDs are provided, there can be no matching playlists.
guard !episodesUUIDs.isEmpty else { return [] }
var uuids: [String] = []
dbQueue.read { db in
do {
let placeholders = Array(repeating: "?", count: episodesUUIDs.count).joined(separator: ", ")
let query = """
SELECT playlist_uuid
FROM \(DataManager.playlistEpisodeTableName)
WHERE episodeUuid IN (\(placeholders))
GROUP BY playlist_uuid
HAVING COUNT(DISTINCT episodeUuid) = ?
"""
var values: [Any] = episodesUUIDs
values.append(episodesUUIDs.count)
let resultSet = try db.executeQuery(query, values: values)

Copilot uses AI. Check for mistakes.
@SergioEstevao SergioEstevao changed the base branch from release/8.6 to trunk March 2, 2026 10:21
@SergioEstevao SergioEstevao modified the milestones: 8.6, 8.7 Mar 2, 2026
@pocketcasts pocketcasts modified the milestones: 8.7, 8.8 Mar 2, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.7 has now entered code-freeze, so the milestone of this PR has been updated to 8.8.

@pocketcasts pocketcasts modified the milestones: 8.8, 8.9 Mar 16, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.8 has now entered code-freeze, so the milestone of this PR has been updated to 8.9.

@pocketcasts pocketcasts modified the milestones: 8.9, 8.10 Mar 30, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.9 has now entered code-freeze, so the milestone of this PR has been updated to 8.10.

@pocketcasts pocketcasts modified the milestones: 8.10, 8.11 Apr 14, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.10 has now entered code-freeze, so the milestone of this PR has been updated to 8.11.

@pocketcasts pocketcasts modified the milestones: 8.11, 8.12 Apr 27, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.11 has now entered code-freeze, so the milestone of this PR has been updated to 8.12.

@pocketcasts pocketcasts modified the milestones: 8.12, 8.13 May 11, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.12 has now entered code-freeze, so the milestone of this PR has been updated to 8.13.

@pocketcasts pocketcasts modified the milestones: 8.13, 8.14 May 25, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.13 has now entered code-freeze, so the milestone of this PR has been updated to 8.14.

@pocketcasts pocketcasts modified the milestones: 8.14, 8.15 Jun 8, 2026
@pocketcasts

Copy link
Copy Markdown
Contributor

Version 8.14 has now entered code-freeze, so the milestone of this PR has been updated to 8.15.

@kean kean modified the milestones: 8.15, Future Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants