Dark Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Playlist multi add - pre selection of selected lists#4026

Draft
SergioEstevao wants to merge 3 commits intotrunkfrom
sergio/playlist_selection
Draft

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

Conversation

Copy link
Contributor

SergioEstevao commented Feb 26, 2026

| 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 added this to the 8.6 milestone Feb 26, 2026
Copilot AI review requested due to automatic review settings February 26, 2026 22:11
SergioEstevao added the [Project] Playlists label Feb 26, 2026
Copy link
Collaborator

dangermattic commented Feb 26, 2026

1 Message
This PR is still a Draft: some checks will be skipped.

Generated by Danger

Copilot started reviewing on behalf of SergioEstevao February 26, 2026 22:12 View session
Copilot AI reviewed Feb 26, 2026
Copy link
Contributor

Copilot AI left a comment

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.manualPlaylis tUUIDs error: \(error)")
}
}
return uuids
}
Copy link

Copilot AI Feb 26, 2026

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: ",")
Copy link

Copilot AI Feb 26, 2026

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);
Copy link

Copilot AI Feb 26, 2026

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.manualPlaylis tUUIDs error: \(error)")
}
}
return uuids
}
Copy link

Copilot AI Feb 26, 2026

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: [])
Copy link

Copilot AI Feb 26, 2026

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 mentioned this pull request Feb 27, 2026
3 tasks
SergioEstevao changed the base branch from release/8.6 to trunk March 2, 2026 10:21
SergioEstevao modified the milestones: 8.6, 8.7 Mar 2, 2026
pocketcasts modified the milestones: 8.7, 8.8 Mar 2, 2026
Copy link
Contributor

pocketcasts commented Mar 2, 2026

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

pocketcasts modified the milestones: 8.8, 8.9 Mar 16, 2026
Copy link
Contributor

pocketcasts commented Mar 16, 2026

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

Copilot code review Copilot Copilot left review comments

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Projects

None yet

Milestone

8.9

Development

Successfully merging this pull request may close these issues.

4 participants