diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index eaa1269..5b44fac 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -19,9 +19,10 @@ public class AllDebrid: PollingDebridSource, ObservableObject { getToken() != nil } - public var IAValues: [DebridIA] = [] - public var cloudDownloads: [DebridCloudDownload] = [] - public var cloudTorrents: [DebridCloudTorrent] = [] + @Published public var IAValues: [DebridIA] = [] + @Published public var cloudDownloads: [DebridCloudDownload] = [] + @Published public var cloudTorrents: [DebridCloudTorrent] = [] + public var cloudTTL: Double = 0.0 let baseApiUrl = "https://api.alldebrid.com/v4" let appName = "Ferrite" @@ -163,7 +164,26 @@ public class AllDebrid: PollingDebridSource, ObservableObject { // MARK: - Instant availability public func instantAvailability(magnets: [Magnet]) async throws { - let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) } + let now = Date().timeIntervalSince1970 + + let sendMagnets = magnets.filter { magnet in + if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) { + if now > IAValues[IAIndex].expiryTimeStamp { + IAValues.remove(at: IAIndex) + return true + } else { + return false + } + } else { + return true + } + } + + if sendMagnets.isEmpty { + return + } + + let queryItems = sendMagnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) } var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems)) let data = try await performRequest(request: &request, requestName: #function) diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 2d26a20..1530ba8 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -19,6 +19,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { @Published public var IAValues: [DebridIA] = [] @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] + public var cloudTTL: Double = 0.0 let baseAuthUrl = "https://www.premiumize.me/authorize" let baseApiUrl = "https://www.premiumize.me/api" @@ -127,16 +128,31 @@ public class Premiumize: OAuthDebridSource, ObservableObject { // MARK: - Instant availability public func instantAvailability(magnets: [Magnet]) async throws { - // Only strip magnets that don't have an associated link for PM - let strippedMagnets: [Magnet] = magnets.compactMap { - if let magnetLink = $0.link { - return Magnet(hash: $0.hash, link: magnetLink) + let now = Date().timeIntervalSince1970 + + // Remove magnets that don't have an associated link for PM along with existing TTL logic + let sendMagnets = magnets.filter { magnet in + if magnet.link == nil { + return false + } + + if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) { + if now > IAValues[IAIndex].expiryTimeStamp { + IAValues.remove(at: IAIndex) + return true + } else { + return false + } } else { - return nil + return true } } - let availableMagnets = try await divideCacheRequests(magnets: strippedMagnets) + if sendMagnets.isEmpty { + return + } + + let availableMagnets = try await divideCacheRequests(magnets: sendMagnets) // Split DDL requests into chunks of 10 for chunk in availableMagnets.chunked(into: 10) { diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 85a1fcb..f306a15 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -20,14 +20,10 @@ public class RealDebrid: PollingDebridSource, ObservableObject { FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil } - @Published public var IAValues: [DebridIA] = [] { - willSet { - self.objectWillChange.send() - } - } + @Published public var IAValues: [DebridIA] = [] @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] - var cloudTTL: Double = 0.0 + public var cloudTTL: Double = 0.0 let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" let baseApiUrl = "https://api.real-debrid.com/rest/1.0" @@ -237,7 +233,26 @@ public class RealDebrid: PollingDebridSource, ObservableObject { // Checks if the magnet is streamable on RD public func instantAvailability(magnets: [Magnet]) async throws { - var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.compactMap(\.hash).joined(separator: "/"))")!) + let now = Date().timeIntervalSince1970 + + let sendMagnets = magnets.filter { magnet in + if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) { + if now > IAValues[IAIndex].expiryTimeStamp { + IAValues.remove(at: IAIndex) + return true + } else { + return false + } + } else { + return true + } + } + + if sendMagnets.isEmpty { + return + } + + var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(sendMagnets.compactMap(\.hash).joined(separator: "/"))")!) let data = try await performRequest(request: &request, requestName: #function) diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index fb7ba3d..2012492 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -36,6 +36,7 @@ public protocol DebridSource: AnyObservableObject { // Cloud variables var cloudDownloads: [DebridCloudDownload] { get set } var cloudTorrents: [DebridCloudTorrent] { get set } + var cloudTTL: Double { get set } // User downloads functions func getUserDownloads() async throws -> [DebridCloudDownload] diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 6a712bb..0b714f0 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -27,12 +27,8 @@ public class DebridManager: ObservableObject { } @Published var selectedDebridSource: DebridSource? - - /* - func debridSourceFromName(_ name: String? = nil) -> DebridSource? { - debridSources.first { $0.id.name == name ?? selectedDebridId?.name } - } - */ + var selectedDebridItem: DebridIA? + var selectedDebridFile: DebridIAFile? // Service agnostic variables @Published var enabledDebrids: Set = [] { @@ -184,78 +180,22 @@ public class DebridManager: ObservableObject { // Clears all selected files and items public func clearSelectedDebridItems() { - switch selectedDebridType { - case .realDebrid: - selectedRealDebridFile = nil - selectedRealDebridItem = nil - case .allDebrid: - selectedAllDebridFile = nil - selectedAllDebridItem = nil - case .premiumize: - selectedPremiumizeFile = nil - selectedPremiumizeItem = nil - case .none: - break - } + selectedDebridItem = nil + selectedDebridFile = nil } // Common function to populate hashes for debrid services public func populateDebridIA(_ resultMagnets: [Magnet]) async { - let now = Date() - - // If a hash isn't found in the IA, update it - // If the hash is expired, remove it and update it - let sendMagnets = resultMagnets.filter { magnet in - if let IAIndex = realDebrid.IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.realDebrid) { - if now.timeIntervalSince1970 > realDebrid.IAValues[IAIndex].expiryTimeStamp { - realDebrid.IAValues.remove(at: IAIndex) - return true - } else { - return false - } - } else if let IAIndex = allDebrid.IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.allDebrid) { - if now.timeIntervalSince1970 > allDebrid.IAValues[IAIndex].expiryTimeStamp { - allDebrid.IAValues.remove(at: IAIndex) - return true - } else { - return false - } - } else if let IAIndex = premiumize.IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.premiumize) { - if now.timeIntervalSince1970 > premiumize.IAValues[IAIndex].expiryTimeStamp { - premiumize.IAValues.remove(at: IAIndex) - return true - } else { - return false - } - } else { - return true - } - } - - // Don't exit the function if the API fetch errors - if !sendMagnets.isEmpty { - if enabledDebrids.contains(.realDebrid) { - do { - try await realDebrid.instantAvailability(magnets: sendMagnets) - } catch { - await sendDebridError(error, prefix: "RealDebrid IA fetch error") - } + for debridSource in debridSources { + if !debridSource.isLoggedIn { + continue } - if enabledDebrids.contains(.allDebrid) { - do { - try await allDebrid.instantAvailability(magnets: sendMagnets) - } catch { - await sendDebridError(error, prefix: "AllDebrid IA fetch error") - } - } - - if enabledDebrids.contains(.premiumize) { - do { - try await premiumize.instantAvailability(magnets: sendMagnets) - } catch { - await sendDebridError(error, prefix: "Premiumize IA fetch error") - } + // Don't exit the function if the API fetch errors + do { + try await debridSource.instantAvailability(magnets: resultMagnets) + } catch { + await sendDebridError(error, prefix: "\(debridSource.id) IA fetch error") } } } @@ -281,32 +221,15 @@ public class DebridManager: ObservableObject { return false } - switch selectedDebridSource?.id { - case .some("RealDebrid"): - if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { - selectedRealDebridItem = realDebridItem - return true - } else { - logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)") - return false - } - case .some("AllDebrid"): - if let allDebridItem = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { - selectedAllDebridItem = allDebridItem - return true - } else { - logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)") - return false - } - case .some("Premiumize"): - if let premiumizeItem = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) { - selectedPremiumizeItem = premiumizeItem - return true - } else { - logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)") - return false - } - default: + guard let selectedSource = selectedDebridSource else { + return false + } + + if let IAItem = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) { + selectedDebridItem = IAItem + return true + } else { + logManager?.error("DebridManager: Could not find the associated \(selectedSource.id) entry for magnet hash \(magnetHash)") return false } } @@ -535,23 +458,19 @@ public class DebridManager: ObservableObject { self.currentDebridTask = nil }) - switch selectedDebridType { - case .realDebrid: - await fetchRdDownload(magnet: magnet, cloudInfo: cloudInfo) - case .allDebrid: - await fetchAdDownload(magnet: magnet, cloudInfo: cloudInfo) - case .premiumize: - await fetchPmDownload(magnet: magnet, cloudInfo: cloudInfo) - case .none: - break + guard let debridSource = selectedDebridSource else { + return } - } - func fetchRdDownload(magnet: Magnet?, cloudInfo: String?) async { do { + if let cloudInfo { + downloadUrl = try await debridSource.checkUserDownloads(link: cloudInfo) ?? "" + return + } + if let magnet { - let downloadLink = try await realDebrid.getDownloadLink( - magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile + let downloadLink = try await debridSource.getDownloadLink( + magnet: magnet, ia: selectedDebridItem, iaFile: selectedDebridFile ) // Update the UI @@ -560,6 +479,28 @@ public class DebridManager: ObservableObject { throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") } + // Fetch one more time to add updated data into the RD cloud cache + // TODO: Add common fetch cloud method + //await fetchRdCloud(bypassTTL: true) + } catch { + // TODO: Fix error types and unify errors + print("Error \(error)") + } + } + + func fetchRdDownload(magnet: Magnet?, cloudInfo: String?) async { + do { + guard let magnet else { + throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") + } + + let downloadLink = try await realDebrid.getDownloadLink( + magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile + ) + + // Update the UI + downloadUrl = downloadLink + // Fetch one more time to add updated data into the RD cloud cache await fetchRdCloud(bypassTTL: true) } catch { @@ -631,21 +572,6 @@ public class DebridManager: ObservableObject { } } - func checkRdUserDownloads(userTorrentLink: String) async -> String? { - do { - let existingLinks = realDebridCloudDownloads.first { $0.link == userTorrentLink } - if let existingLink = existingLinks?.fileName { - return existingLink - } else { - return try await realDebrid.unrestrictLink(debridDownloadLink: userTorrentLink) - } - } catch { - await sendDebridError(error, prefix: "RealDebrid download check error") - - return nil - } - } - func fetchAdDownload(magnet: Magnet?, cloudInfo: String?) async { do { if let magnet { @@ -666,22 +592,6 @@ public class DebridManager: ObservableObject { } } - func checkAdUserLinks(lockedLink: String) async -> String? { - do { - let existingLinks = allDebridCloudLinks.first { $0.link == lockedLink } - if let existingLink = existingLinks?.link { - return existingLink - } else { - try await allDebrid.saveLink(link: lockedLink) - return try await allDebrid.unlockLink(lockedLink: lockedLink) - } - } catch { - await sendDebridError(error, prefix: "AllDebrid download check error") - - return nil - } - } - // Refreshes torrents and downloads from a RD user's account public func fetchAdCloud(bypassTTL: Bool = false) async { if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL { diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index be0f38d..1620fdc 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -23,39 +23,14 @@ struct BatchChoiceView: View { var body: some View { NavView { List { - switch debridManager.selectedDebridSource?.id { - case .some("RealDebrid"): - ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in - if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { - Button(file.name) { - debridManager.selectedRealDebridFile = file + ForEach(debridManager.selectedDebridItem?.files ?? [], id: \.self) { file in + if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { + Button(file.name) { + debridManager.selectedDebridFile = file - queueCommonDownload(fileName: file.name) - } + queueCommonDownload(fileName: file.name) } } - case .some("AllDebrid"): - ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in - if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { - Button(file.name) { - debridManager.selectedAllDebridFile = file - - queueCommonDownload(fileName: file.name) - } - } - } - case .some("Premiumize"): - ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in - if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { - Button(file.name) { - debridManager.selectedPremiumizeFile = file - - queueCommonDownload(fileName: file.name) - } - } - } - default: - EmptyView() } } .tint(.primary)