diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 7111cd1..34268fa 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -14,6 +14,15 @@ public class AllDebrid: PollingDebridSource { public let website = "https://alldebrid.com" public var authTask: Task? + public var authProcessing: Bool = false + public var isLoggedIn: Bool { + getToken() != nil + } + + public var IAValues: [DebridIA] = [] + public var cloudDownloads: [DebridCloudDownload] = [] + public var cloudTorrents: [DebridCloudTorrent] = [] + let baseApiUrl = "https://api.alldebrid.com/v4" let appName = "Ferrite" @@ -153,7 +162,7 @@ public class AllDebrid: PollingDebridSource { // MARK: - Instant availability - public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { + public func instantAvailability(magnets: [Magnet]) async throws { let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) } var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems)) @@ -175,16 +184,16 @@ public class AllDebrid: PollingDebridSource { ) } - return availableHashes + IAValues += availableHashes } // MARK: - Downloading // Wrapper function to fetch a download link from the API - public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent] = []) async throws -> String { + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { let selectedMagnetId: String - if let existingMagnet = userTorrents.first(where: { $0.hash == magnet.hash && $0.status == "Ready" }) { + if let existingMagnet = cloudTorrents.first(where: { $0.hash == magnet.hash && $0.status == "Ready" }) { selectedMagnetId = existingMagnet.torrentId } else { let magnetId = try await addMagnet(magnet: magnet) @@ -280,7 +289,7 @@ public class AllDebrid: PollingDebridSource { throw ADError.EmptyData } - let torrents = rawResponse.magnets.map { magnetResponse in + cloudTorrents = rawResponse.magnets.map { magnetResponse in DebridCloudTorrent( torrentId: String(magnetResponse.id), source: self.id, @@ -291,7 +300,7 @@ public class AllDebrid: PollingDebridSource { ) } - return torrents + return cloudTorrents } public func deleteTorrent(torrentId: String) async throws { @@ -314,17 +323,17 @@ public class AllDebrid: PollingDebridSource { } // The link is also the ID - let downloads = rawResponse.links.map { link in + cloudDownloads = rawResponse.links.map { link in DebridCloudDownload( downloadId: link.link, source: self.id, fileName: link.filename, link: link.link ) } - return downloads + return cloudDownloads } // Not used - public func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) async throws -> String? { + public func checkUserDownloads(link: String) async throws -> String? { nil } diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 8ec7356..8a08d6b 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -12,6 +12,15 @@ public class Premiumize: OAuthDebridSource { public let abbreviation = "PM" public let website = "https://premiumize.me" + public var authProcessing: Bool = false + public var isLoggedIn: Bool { + getToken() != nil + } + + public var IAValues: [DebridIA] = [] + public var cloudDownloads: [DebridCloudDownload] = [] + public var cloudTorrents: [DebridCloudTorrent] = [] + let baseAuthUrl = "https://www.premiumize.me/authorize" let baseApiUrl = "https://www.premiumize.me/api" let clientId = "791565696" @@ -118,9 +127,7 @@ public class Premiumize: OAuthDebridSource { // MARK: - Instant availability - public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { - var collectedIA: [DebridIA] = [] - + 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 { @@ -135,10 +142,8 @@ public class Premiumize: OAuthDebridSource { // Split DDL requests into chunks of 10 for chunk in availableMagnets.chunked(into: 10) { let tempIA = try await divideDDLRequests(magnetChunk: chunk) - collectedIA += tempIA + IAValues += tempIA } - - return collectedIA } // Function to divide and execute DDL endpoint requests in parallel @@ -251,7 +256,7 @@ public class Premiumize: OAuthDebridSource { // MARK: - Downloading // Wrapper function to fetch a DDL link from the API - public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent] = []) async throws -> String { + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { // Store the item in PM cloud for later use try await createTransfer(magnet: magnet) @@ -294,11 +299,11 @@ public class Premiumize: OAuthDebridSource { } // The "link" is the ID for Premiumize - let downloads = rawResponse.files.map { file in + cloudDownloads = rawResponse.files.map { file in DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id) } - return downloads + return cloudDownloads } func itemDetails(itemID: String) async throws -> ItemDetailsResponse { @@ -316,7 +321,7 @@ public class Premiumize: OAuthDebridSource { return rawResponse } - public func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) async throws -> String? { + public func checkUserDownloads(link: String) async throws -> String? { // Link is the cloud item ID try await itemDetails(itemID: link).link } diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 7bf301e..53efaa2 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -13,6 +13,17 @@ public class RealDebrid: PollingDebridSource { public let website = "https://real-debrid.com" public var authTask: Task? + public var authProcessing: Bool = false + + // Directly checked because the request fetch uses async + public var isLoggedIn: Bool { + FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil + } + + public var IAValues: [DebridIA] = [] + public var cloudDownloads: [DebridCloudDownload] = [] + public var cloudTorrents: [DebridCloudTorrent] = [] + let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" let baseApiUrl = "https://api.real-debrid.com/rest/1.0" let openSourceClientId = "X245A4XAIBGVM" @@ -97,7 +108,7 @@ public class RealDebrid: PollingDebridSource { await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId") FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret") - try await getTokens(deviceCode: deviceCode) + try await getApiTokens(deviceCode: deviceCode) return } else { @@ -110,7 +121,7 @@ public class RealDebrid: PollingDebridSource { } // Fetch all tokens for the user and store in FerriteKeychain.shared - public func getTokens(deviceCode: String) async throws { + public func getApiTokens(deviceCode: String) async throws { guard let clientId = UserDefaults.standard.string(forKey: "RealDebrid.ClientId") else { throw RDError.EmptyData } @@ -144,13 +155,13 @@ public class RealDebrid: PollingDebridSource { await setUserDefaultsValue(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp") } - public func fetchToken() async -> String? { + public func getToken() async -> String? { let accessTokenStamp = UserDefaults.standard.double(forKey: "RealDebrid.AccessTokenStamp") if Date().timeIntervalSince1970 > accessTokenStamp { do { if let refreshToken = FerriteKeychain.shared.get("RealDebrid.RefreshToken") { - try await getTokens(deviceCode: refreshToken) + try await getApiTokens(deviceCode: refreshToken) } } catch { print(error) @@ -195,7 +206,7 @@ public class RealDebrid: PollingDebridSource { // Wrapper request function which matches the responses and returns data @discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data { - guard let token = await fetchToken() else { + guard let token = await getToken() else { throw RDError.InvalidToken } @@ -220,8 +231,7 @@ public class RealDebrid: PollingDebridSource { // MARK: - Instant availability // Checks if the magnet is streamable on RD - public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { - var availableHashes: [DebridIA] = [] + public func instantAvailability(magnets: [Magnet]) async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.compactMap(\.hash).joined(separator: "/"))")!) let data = try await performRequest(request: &request, requestName: #function) @@ -269,7 +279,7 @@ public class RealDebrid: PollingDebridSource { } // TTL: 5 minutes - availableHashes.append( + IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), source: id, @@ -278,7 +288,7 @@ public class RealDebrid: PollingDebridSource { ) ) } else { - availableHashes.append( + IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), source: id, @@ -288,19 +298,17 @@ public class RealDebrid: PollingDebridSource { ) } } - - return availableHashes } // MARK: - Downloading // Wrapper function to fetch a download link from the API - public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent] = []) async throws -> String { - var selectedMagnetId: String = "" + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { + var selectedMagnetId = "" do { // Don't queue a new job if the torrent already exists - if let existingTorrent = userTorrents.first(where: { $0.hash == magnet.hash && $0.status == "downloaded" }) { + if let existingTorrent = cloudTorrents.first(where: { $0.hash == magnet.hash && $0.status == "downloaded" }) { selectedMagnetId = existingTorrent.torrentId } else { selectedMagnetId = try await addMagnet(magnet: magnet) @@ -411,7 +419,7 @@ public class RealDebrid: PollingDebridSource { let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data) - let torrents = rawResponse.map { response in + cloudTorrents = rawResponse.map { response in DebridCloudTorrent( torrentId: response.id, source: self.id, @@ -422,7 +430,7 @@ public class RealDebrid: PollingDebridSource { ) } - return torrents + return cloudTorrents } // Deletes a torrent download from RD @@ -439,15 +447,15 @@ public class RealDebrid: PollingDebridSource { let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data) - let downloads = rawResponse.map { response in + cloudDownloads = rawResponse.map { response in DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download) } - return downloads + return cloudDownloads } // Not used - public func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) -> String? { + public func checkUserDownloads(link: String) -> String? { nil } diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index c2d6e20..3c35014 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -13,20 +13,32 @@ public protocol DebridSource { var abbreviation: String { get } var website: String { get } + // Auth variables + var authProcessing: Bool { get set } + var isLoggedIn: Bool { get } + // Common authentication functions func setApiKey(_ key: String) -> Bool func logout() async - func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] + // Instant availability variables + var IAValues: [DebridIA] { get set } + + // Instant availability functions + func instantAvailability(magnets: [Magnet]) async throws // Fetches a download link from a source // Include the instant availability information with the args // Torrents also checked here - func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent]) async throws -> String + func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String + + // Cloud variables + var cloudDownloads: [DebridCloudDownload] { get set } + var cloudTorrents: [DebridCloudTorrent] { get set } // User downloads functions func getUserDownloads() async throws -> [DebridCloudDownload] - func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) async throws -> String? + func checkUserDownloads(link: String) async throws -> String? func deleteDownload(downloadId: String) async throws // User torrent functions diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 632ae3c..84d9e02 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -16,6 +16,8 @@ public class DebridManager: ObservableObject { let allDebrid: AllDebrid = .init() let premiumize: Premiumize = .init() + lazy var debridSources: [DebridSource] = [realDebrid, allDebrid, premiumize] + // UI Variables @Published var showWebView: Bool = false @Published var showAuthSession: Bool = false @@ -198,22 +200,22 @@ public class DebridManager: ObservableObject { // 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 = realDebridIAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.realDebrid) { - if now.timeIntervalSince1970 > realDebridIAValues[IAIndex].expiryTimeStamp { - realDebridIAValues.remove(at: IAIndex) + 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 = allDebridIAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.allDebrid) { - if now.timeIntervalSince1970 > allDebridIAValues[IAIndex].expiryTimeStamp { - allDebridIAValues.remove(at: IAIndex) + } 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 = premiumizeIAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.premiumize) { - if now.timeIntervalSince1970 > premiumizeIAValues[IAIndex].expiryTimeStamp { + } else if let IAIndex = premiumize.IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.premiumize) { + if now.timeIntervalSince1970 > premiumize.IAValues[IAIndex].expiryTimeStamp { premiumizeIAValues.remove(at: IAIndex) return true } else { @@ -228,8 +230,7 @@ public class DebridManager: ObservableObject { if !sendMagnets.isEmpty { if enabledDebrids.contains(.realDebrid) { do { - let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnets: sendMagnets) - realDebridIAValues += fetchedRealDebridIA + try await realDebrid.instantAvailability(magnets: sendMagnets) } catch { await sendDebridError(error, prefix: "RealDebrid IA fetch error") } @@ -237,8 +238,7 @@ public class DebridManager: ObservableObject { if enabledDebrids.contains(.allDebrid) { do { - let fetchedAllDebridIA = try await allDebrid.instantAvailability(magnets: sendMagnets) - allDebridIAValues += fetchedAllDebridIA + try await allDebrid.instantAvailability(magnets: sendMagnets) } catch { await sendDebridError(error, prefix: "AllDebrid IA fetch error") } @@ -246,8 +246,7 @@ public class DebridManager: ObservableObject { if enabledDebrids.contains(.premiumize) { do { - let fetchedPremiumizeIA = try await premiumize.instantAvailability(magnets: sendMagnets) - premiumizeIAValues += fetchedPremiumizeIA + try await premiumize.instantAvailability(magnets: sendMagnets) } catch { await sendDebridError(error, prefix: "Premiumize IA fetch error") } @@ -263,7 +262,7 @@ public class DebridManager: ObservableObject { switch selectedDebridType { case .realDebrid: - guard let realDebridMatch = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else { + guard let realDebridMatch = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { return .none } @@ -273,7 +272,7 @@ public class DebridManager: ObservableObject { return .full } case .allDebrid: - guard let allDebridMatch = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else { + guard let allDebridMatch = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { return .none } @@ -283,7 +282,7 @@ public class DebridManager: ObservableObject { return .full } case .premiumize: - guard let premiumizeMatch = premiumizeIAValues.first(where: { magnetHash == $0.magnet.hash }) else { + guard let premiumizeMatch = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { return .none } @@ -305,7 +304,7 @@ public class DebridManager: ObservableObject { switch selectedDebridType { case .realDebrid: - if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) { + if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedRealDebridItem = realDebridItem return true } else { @@ -313,7 +312,7 @@ public class DebridManager: ObservableObject { return false } case .allDebrid: - if let allDebridItem = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) { + if let allDebridItem = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedAllDebridItem = allDebridItem return true } else { @@ -321,7 +320,7 @@ public class DebridManager: ObservableObject { return false } case .premiumize: - if let premiumizeItem = premiumizeIAValues.first(where: { magnetHash == $0.magnet.hash }) { + if let premiumizeItem = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedPremiumizeItem = premiumizeItem return true } else { @@ -573,7 +572,7 @@ public class DebridManager: ObservableObject { do { if let magnet { let downloadLink = try await realDebrid.getDownloadLink( - magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile, userTorrents: realDebridCloudTorrents + magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile ) // Update the UI @@ -643,6 +642,8 @@ public class DebridManager: ObservableObject { do { if let torrentID { try await realDebrid.deleteTorrent(torrentId: torrentID) + + await fetchRdCloud(bypassTTL: true) } else { throw RealDebrid.RDError.FailedRequest(description: "No torrent ID was provided") } @@ -670,7 +671,7 @@ public class DebridManager: ObservableObject { do { if let magnet { let downloadLink = try await allDebrid.getDownloadLink( - magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile, userTorrents: allDebridCloudMagnets + magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile ) // Update UI @@ -740,7 +741,7 @@ public class DebridManager: ObservableObject { func fetchPmDownload(magnet: Magnet?, cloudInfo: String? = nil) async { do { if let cloudInfo { - downloadUrl = try await premiumize.checkUserDownloads(link: cloudInfo, userDownloads: premiumizeCloudItems) ?? "" + downloadUrl = try await premiumize.checkUserDownloads(link: cloudInfo) ?? "" return } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 383c1ea..af74379 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -17,7 +17,7 @@ struct RealDebridCloudView: View { var body: some View { Group { DisclosureGroup("Downloads") { - ForEach(debridManager.realDebridCloudDownloads.filter { + ForEach(debridManager.realDebrid.cloudDownloads.filter { searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) }, id: \.self) { cloudDownload in Button(cloudDownload.fileName) { @@ -54,7 +54,7 @@ struct RealDebridCloudView: View { } DisclosureGroup("Torrents") { - ForEach(debridManager.realDebridCloudTorrents.filter { + ForEach(debridManager.realDebrid.cloudTorrents.filter { searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) }, id: \.self) { cloudTorrent in Button {