Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,18 @@ class EpisodeDataManager {
loadMultiple(query: "SELECT * from \(DataManager.episodeTableName) WHERE \(columnName) IS NOT NULL", values: nil, dbQueue: dbQueue)
}

func findEpisodesAndPodcastsWhere(customWhere: String, dbQueue: PCDBQueue) -> [Episode] {
func findEpisodesAndPodcastsWhere(customWhere: String, listenedTo: Bool, dbQueue: PCDBQueue) -> [Episode] {
let listenedToQuery: String = """
lastPlaybackInteractionDate IS NOT NULL
AND lastPlaybackInteractionDate > 0
AND
"""
let query = """
SELECT episode.* FROM \(DataManager.episodeTableName) episode
LEFT JOIN \(DataManager.podcastTableName) podcast ON episode.podcast_id = podcast.id
WHERE lastPlaybackInteractionDate IS NOT NULL
AND lastPlaybackInteractionDate > 0
AND (UPPER(episode.title) LIKE '%' || UPPER(?) || '%' ESCAPE '\\'
WHERE
\(listenedTo ? listenedToQuery : "")
(UPPER(episode.title) LIKE '%' || UPPER(?) || '%' ESCAPE '\\'
OR UPPER(podcast.title) LIKE '%' || UPPER(?) || '%' ESCAPE '\\')
ORDER BY lastPlaybackInteractionDate DESC LIMIT 1000
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import PocketCastsUtils
import Foundation

extension Podcast: Sortable {
public var itemUUID: String {
uuid
}

public var itemTitle: String? {
title
}
}

class PodcastDataManager {
private var cachedPodcasts = [String: Podcast]()
private lazy var cachedPodcastsQueue: DispatchQueue = {
Expand Down Expand Up @@ -310,6 +320,34 @@ class PodcastDataManager {
}
}

func searchPodcasts(term: String, dbQueue: PCDBQueue) -> [Podcast] {
let trimmedTerm = term.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedTerm.isEmpty else { return [] }

let locale = Locale.current
let options: String.CompareOptions = [.caseInsensitive, .diacriticInsensitive]

var matchingPodcasts = [Podcast]()
cachedPodcastsQueue.sync {
for podcast in cachedPodcasts.values {
guard podcast.isSubscribed() else { continue }

if podcast.title?.range(of: trimmedTerm, options: options, range: nil, locale: locale) != nil {
matchingPodcasts.append(podcast)
continue
}

if podcast.author?.range(of: trimmedTerm, options: options, range: nil, locale: locale) != nil {
matchingPodcasts.append(podcast)
}
}
}

return matchingPodcasts.sorted(by: { lhs, rhs in
PodcastSorter.sortByNameAndUUID(item1: lhs, item2: rhs)
})
}

func count(dbQueue: PCDBQueue) -> Int {
var count = 0
cachedPodcastsQueue.sync {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ public class DataManager {
podcastManager.allPodcasts(includeUnsubscribed: includeUnsubscribed, reloadFromDatabase: reloadFromDatabase, dbQueue: dbQueue)
}

public func searchPodcasts(term: String) -> [Podcast] {
podcastManager.searchPodcasts(term: term, dbQueue: dbQueue)
}

public func allPodcastsOrderedByTitle(reloadFromDatabase: Bool = false) -> [Podcast] {
podcastManager.allPodcastsOrderedByTitle(reloadFromDatabase: reloadFromDatabase, dbQueue: dbQueue)
}
Expand Down Expand Up @@ -503,8 +507,8 @@ public class DataManager {
episodeManager.findPlaylistEpisodesWhere(query: query, arguments: arguments, dbQueue: dbQueue)
}

public func findEpisodesAndPodcastsWhere(customWhere: String) -> [Episode] {
episodeManager.findEpisodesAndPodcastsWhere(customWhere: customWhere, dbQueue: dbQueue)
public func findEpisodesAndPodcastsWhere(customWhere: String, listenedTo: Bool) -> [Episode] {
episodeManager.findEpisodesAndPodcastsWhere(customWhere: customWhere, listenedTo: listenedTo, dbQueue: dbQueue)
}

public func findLatestEpisode(podcast: Podcast) -> Episode? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ public struct EpisodeSearchResult: Codable, Hashable {
public let duration: Double?
public let podcastUuid: String
public let podcastTitle: String

public init(uuid: String, title: String, publishedDate: Date, duration: Double? = nil, podcastUuid: String, podcastTitle: String) {
self.uuid = uuid
self.title = title
self.publishedDate = publishedDate
self.duration = duration
self.podcastUuid = podcastUuid
self.podcastTitle = podcastTitle
}
}

public class EpisodeSearchTask {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import Foundation
import PocketCastsDataModel

public class ServerPodcastManager: NSObject {
protocol ServerPodcastManaging: AnyObject {
func addFromUuid(podcastUuid: String, subscribe: Bool, autoDownloads: Int, completion: ((Bool) -> Void)?)
func addMissingPodcast(episodeUuid: String, podcastUuid: String)
func addMissingEpisode(episodeUuid: String, podcastUuid: String) -> Episode?
func addMissingPodcastAndEpisode(episodeUuid: String, podcastUuid: String, shouldUpdateEpisode: Bool, completion: ((Episode?) -> Void)?)
}

public class ServerPodcastManager: NSObject, ServerPodcastManaging {
private static let maxAutoDownloadSeperationTime = 12.hours

public static let shared = ServerPodcastManager()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,47 +102,65 @@ extension SyncTask {
}

private func importPodcast(_ podcastItem: Api_SyncUserPodcast) {
let existingPodcast = DataManager.sharedManager.findPodcast(uuid: podcastItem.uuid, includeUnsubscribed: true)
if podcastItem.hasIsDeleted, podcastItem.isDeleted.value {
if let podcast = existingPodcast {
podcast.autoDownloadSetting = AutoDownloadSetting.off.rawValue
podcast.isPushEnabled = false
podcast.autoArchiveEpisodeLimit = 0
podcast.subscribed = 0
podcast.autoAddToUpNext = AutoAddToUpNextSetting.off.rawValue
podcast.settings = PodcastSettings.defaults
if FeatureFlag.settingsSync.enabled {
podcast.processSettings(podcastItem.settings)
}
defer {
NotificationCenter.default.post(name: ServerNotifications.syncProgressPodcastUpto, object: upToPodcast)
NotificationCenter.default.post(name: ServerNotifications.syncProgressPodcastCount, object: totalToImport)
upToPodcast += 1
}

DataManager.sharedManager.save(podcast: podcast)
let existingPodcast = DataManager.sharedManager.findPodcast(uuid: podcastItem.uuid, includeUnsubscribed: true)
let isDeleted = podcastItem.hasIsDeleted && podcastItem.isDeleted.value
let shouldSubscribe: Bool = {
if podcastItem.hasIsDeleted {
return !podcastItem.isDeleted.value
}
if podcastItem.hasSubscribed {
return podcastItem.subscribed.value
}
} else if let podcast = existingPodcast {
return true
}()

if isDeleted {
guard let podcast = existingPodcast else { return }

podcast.autoDownloadSetting = AutoDownloadSetting.off.rawValue
podcast.isPushEnabled = false
podcast.autoArchiveEpisodeLimit = 0
podcast.subscribed = 0
podcast.autoAddToUpNext = AutoAddToUpNextSetting.off.rawValue
podcast.settings = PodcastSettings.defaults
if FeatureFlag.settingsSync.enabled {
podcast.processSettings(podcastItem.settings)
}

DataManager.sharedManager.save(podcast: podcast)
return
}

if let podcast = existingPodcast {
importItem(podcastItem: podcastItem, into: podcast, checkIsDeleted: true)
DataManager.sharedManager.save(podcast: podcast)

ServerConfig.shared.syncDelegate?.podcastUpdated(podcastUuid: podcast.uuid)
} else {
let semaphore = DispatchSemaphore(value: 0)
return
}

let semaphore = DispatchSemaphore(value: 0)

ServerPodcastManager.shared.addFromUuid(podcastUuid: podcastItem.uuid, subscribe: true, completion: { success in
if success {
if let podcast = DataManager.sharedManager.findPodcast(uuid: podcastItem.uuid, includeUnsubscribed: true) {
podcast.syncStatus = SyncStatus.synced.rawValue
self.importItem(podcastItem: podcastItem, into: podcast, checkIsDeleted: false)
serverPodcastManager.addFromUuid(podcastUuid: podcastItem.uuid, subscribe: shouldSubscribe, autoDownloads: 0, completion: { success in
if success {
if let podcast = DataManager.sharedManager.findPodcast(uuid: podcastItem.uuid, includeUnsubscribed: true) {
podcast.syncStatus = SyncStatus.synced.rawValue
self.importItem(podcastItem: podcastItem, into: podcast, checkIsDeleted: false)

DataManager.sharedManager.save(podcast: podcast)
}
DataManager.sharedManager.save(podcast: podcast)
}
}

semaphore.signal()
})
_ = semaphore.wait(timeout: .distantFuture)
}
semaphore.signal()
})
_ = semaphore.wait(timeout: .distantFuture)

NotificationCenter.default.post(name: ServerNotifications.syncProgressPodcastUpto, object: upToPodcast)
NotificationCenter.default.post(name: ServerNotifications.syncProgressPodcastCount, object: totalToImport)
upToPodcast += 1
}

private func importItem(podcastItem: Api_SyncUserPodcast, into podcast: Podcast, checkIsDeleted: Bool) {
Expand All @@ -166,8 +184,12 @@ extension SyncTask {
podcast.sortOrder = podcastItem.sortPosition.value
}

if checkIsDeleted, podcastItem.hasIsDeleted {
podcast.subscribed = podcastItem.isDeleted.value ? 0 : 1
if checkIsDeleted {
if podcastItem.hasIsDeleted {
podcast.subscribed = podcastItem.isDeleted.value ? 0 : 1
} else if podcastItem.hasSubscribed {
podcast.subscribed = podcastItem.subscribed.value ? 1 : 0
}
}

if FeatureFlag.settingsSync.enabled {
Expand All @@ -181,7 +203,7 @@ extension SyncTask {
if existingEpisode == nil {
// we don't have this episode so try and find it
FileLog.shared.addMessage("Trying to find missing episode as part of a sync \(episodeItem.uuid)")
existingEpisode = ServerPodcastManager.shared.addMissingEpisode(episodeUuid: episodeItem.uuid, podcastUuid: episodeItem.podcastUuid)
existingEpisode = serverPodcastManager.addMissingEpisode(episodeUuid: episodeItem.uuid, podcastUuid: episodeItem.podcastUuid)
}

guard let episode = existingEpisode else { return }
Expand Down Expand Up @@ -382,7 +404,7 @@ extension SyncTask {
DataManager.sharedManager.save(playlist: playlist)

addedEpisodes.forEach { addedEpisode in
ServerPodcastManager.shared.addMissingPodcastAndEpisode(episodeUuid: addedEpisode.uuid, podcastUuid: addedEpisode.podcastUuid, shouldUpdateEpisode: true)
serverPodcastManager.addMissingPodcastAndEpisode(episodeUuid: addedEpisode.uuid, podcastUuid: addedEpisode.podcastUuid, shouldUpdateEpisode: true, completion: nil)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ class SyncTask: ApiBaseTask {

var status = UpdateStatus.notStarted

let serverPodcastManager: ServerPodcastManaging

init(dataManager: DataManager = .sharedManager,
serverPodcastManager: ServerPodcastManaging = ServerPodcastManager.shared,
urlConnection: URLConnection = URLConnection(handler: URLSession.shared)) {
self.serverPodcastManager = serverPodcastManager
super.init(dataManager: dataManager, urlConnection: urlConnection)
}

private lazy var legacyLastModifiedFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withYear, .withMonth, .withDay, .withDashSeparatorInDate, .withColonSeparatorInTime, .withTime, .withFractionalSeconds]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
@testable import PocketCastsServer
@testable import PocketCastsDataModel
import XCTest
import GRDB
import SwiftProtobuf

final class SyncTaskTests_PodcastImport: XCTestCase {
private var originalDataManager: DataManager!
private var dataManager: PodcastCapturingDataManager!
private var serverPodcastManager: CapturingServerPodcastManager!
private var syncTask: SyncTask!

override func setUpWithError() throws {
try super.setUpWithError()

originalDataManager = DataManager.sharedManager
dataManager = PodcastCapturingDataManager()
DataManager.sharedManager = dataManager

serverPodcastManager = CapturingServerPodcastManager()
syncTask = SyncTask(dataManager: dataManager, serverPodcastManager: serverPodcastManager)
}

override func tearDownWithError() throws {
DataManager.sharedManager = originalDataManager
syncTask = nil
serverPodcastManager = nil
dataManager = nil
originalDataManager = nil

try super.tearDownWithError()
}

func testDoesNotReSubscribeMissingPodcastWhenServerMarksUnsubscribed() {
let uuid = "pod-missing"

var podcast = Api_SyncUserPodcast()
podcast.uuid = uuid
podcast.subscribed = Self.boolValue(false)

var record = Api_Record()
record.podcast = podcast

var response = Api_SyncUpdateResponse()
response.records = [record]

syncTask.processServerData(response: response)

XCTAssertTrue(serverPodcastManager.addFromUuidCalls.isEmpty)
XCTAssertNil(DataManager.sharedManager.findPodcast(uuid: uuid, includeUnsubscribed: true))
}
}

private final class CapturingServerPodcastManager: ServerPodcastManaging {
private(set) var addFromUuidCalls: [(uuid: String, subscribe: Bool, autoDownloads: Int)] = []

func addFromUuid(podcastUuid: String, subscribe: Bool, autoDownloads: Int, completion: ((Bool) -> Void)?) {
addFromUuidCalls.append((podcastUuid, subscribe, autoDownloads))
completion?(false)
}

func addMissingPodcast(episodeUuid: String, podcastUuid: String) {}

func addMissingEpisode(episodeUuid: String, podcastUuid: String) -> Episode? {
nil
}

func addMissingPodcastAndEpisode(episodeUuid: String, podcastUuid: String, shouldUpdateEpisode: Bool, completion: ((Episode?) -> Void)?) {
completion?(nil)
}
}

private final class PodcastCapturingDataManager: DataManager {
private var storedPodcasts: [String: Podcast] = [:]

init() {
let dbPath = NSTemporaryDirectory().appending("\(UUID().uuidString).sqlite")
let pool = try! DatabasePool(path: dbPath)
super.init(dbQueue: GRDBQueue(dbPool: pool, logger: DataManager.logger))
}

override func findPodcast(uuid: String, includeUnsubscribed: Bool = false) -> Podcast? {
storedPodcasts[uuid]
}

override func save(podcast: Podcast) {
storedPodcasts[podcast.uuid] = podcast
}

func storedPodcastCount() -> Int {
storedPodcasts.count
}

override func markAllSynced(episodeIDs: [String]) {
// no-op for tests
}
}

private extension SyncTaskTests_PodcastImport {
static func boolValue(_ value: Bool) -> SwiftProtobuf.Google_Protobuf_BoolValue {
var bool = SwiftProtobuf.Google_Protobuf_BoolValue()
bool.value = value
return bool
}
}
Loading