From 0fe1cbc8886dd09e684b178cf03f51df6239c0d9 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sun, 2 Jun 2024 12:31:19 -0400 Subject: [PATCH 01/27] Debrid: Begin using common protocols Unifying the debrid services under a protocol will help slim down on excess redundant code and allow for easy addition of new services in the future. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 8 +++ Ferrite/API/AllDebridWrapper.swift | 28 ++++++--- Ferrite/API/PremiumizeWrapper.swift | 13 +++-- Ferrite/API/RealDebridWrapper.swift | 79 ++++++++++++++------------ Ferrite/Models/DebridModels.swift | 16 ++++++ Ferrite/Protocols/Debrid.swift | 34 +++++++++++ Ferrite/ViewModels/DebridManager.swift | 26 ++++----- 7 files changed, 140 insertions(+), 64 deletions(-) create mode 100644 Ferrite/Models/DebridModels.swift create mode 100644 Ferrite/Protocols/Debrid.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index de3bb7d..14919aa 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -154,6 +154,8 @@ 0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; }; 0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; }; 0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */; }; + 0CF1ABDC2C0C04B2009F6C26 /* Debrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */; }; + 0CF1ABE22C0C3D2F009F6C26 /* DebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */; }; 0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF2C0A429D1EBD400E716DD /* UIApplication.swift */; }; /* End PBXBuildFile section */ @@ -300,6 +302,8 @@ 0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = ""; }; 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterHeaderView.swift; sourceTree = ""; }; 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPickerView.swift; sourceTree = ""; }; + 0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debrid.swift; sourceTree = ""; }; + 0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridModels.swift; sourceTree = ""; }; 0CF2C0A429D1EBD400E716DD /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -399,6 +403,7 @@ 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */, 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */, 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */, + 0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */, ); path = Models; sourceTree = ""; @@ -492,6 +497,7 @@ isa = PBXGroup; children = ( 0CE1C4172981E8D700418F20 /* Plugin.swift */, + 0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */, ); path = Protocols; sourceTree = ""; @@ -923,6 +929,7 @@ 0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */, 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */, 0CC2CA7429E24F63000A8585 /* ExpandedSearchable.swift in Sources */, + 0CF1ABE22C0C3D2F009F6C26 /* DebridModels.swift in Sources */, 0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */, 0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, @@ -942,6 +949,7 @@ 0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */, 0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */, 0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */, + 0CF1ABDC2C0C04B2009F6C26 /* Debrid.swift in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */, 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */, 0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */, diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 2b850ff..0f2c50e 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -8,24 +8,36 @@ import Foundation // TODO: Fix errors -public class AllDebrid { - let jsonDecoder = JSONDecoder() +public class AllDebrid: PollingDebridSource { + + public let id = "AllDebrid" + public var authTask: Task? let baseApiUrl = "https://api.alldebrid.com/v4" let appName = "Ferrite" - var authTask: Task? + let jsonDecoder = JSONDecoder() // Fetches information for PIN auth - public func getPinInfo() async throws -> PinResponse { + public func getAuthUrl() async throws -> URL { let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get") let request = URLRequest(url: url) do { let (data, _) = try await URLSession.shared.data(for: request) - let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data - return rawResponse + // Validate the URL before doing anything else + let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data + guard let userUrl = URL(string: rawResponse.userURL) else { + throw ADError.AuthQuery(description: "The login URL is invalid") + } + + // Spawn the polling task separately + authTask = Task { + try await getApiKey(checkID: rawResponse.check, pin: rawResponse.pin) + } + + return userUrl } catch { print("Couldn't get pin information!") throw ADError.AuthQuery(description: error.localizedDescription) @@ -88,7 +100,7 @@ public class AllDebrid { } // Clears tokens. No endpoint to deregister a device - public func deleteTokens() { + public func logout() { FerriteKeychain.shared.delete("AllDebrid.ApiKey") UserDefaults.standard.removeObject(forKey: "AllDebrid.UseManualKey") } @@ -110,7 +122,7 @@ public class AllDebrid { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - deleteTokens() + logout() throw ADError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.") } else { throw ADError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 3ad235c..d771536 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -7,14 +7,17 @@ import Foundation -public class Premiumize { - let jsonDecoder = JSONDecoder() +public class Premiumize: OAuthDebridSource { + + public let id = "Premiumize" let baseAuthUrl = "https://www.premiumize.me/authorize" let baseApiUrl = "https://www.premiumize.me/api" let clientId = "791565696" - public func buildAuthUrl() throws -> URL { + let jsonDecoder = JSONDecoder() + + public func getAuthUrl() throws -> URL { var urlComponents = URLComponents(string: baseAuthUrl)! urlComponents.queryItems = [ URLQueryItem(name: "client_id", value: clientId), @@ -59,7 +62,7 @@ public class Premiumize { } // Clears tokens. No endpoint to deregister a device - public func deleteTokens() { + public func logout() { FerriteKeychain.shared.delete("Premiumize.AccessToken") UserDefaults.standard.removeObject(forKey: "Premiumize.UseManualKey") } @@ -101,7 +104,7 @@ public class Premiumize { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - deleteTokens() + logout() throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.") } else { throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 1291b2e..14e46a5 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -7,14 +7,16 @@ import Foundation -public class RealDebrid { - let jsonDecoder = JSONDecoder() +public class RealDebrid: PollingDebridSource { + + public let id = "RealDebrid" + public var authTask: Task? let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" let baseApiUrl = "https://api.real-debrid.com/rest/1.0" let openSourceClientId = "X245A4XAIBGVM" - var authTask: Task? + let jsonDecoder = JSONDecoder() @MainActor func setUserDefaultsValue(_ value: Any, forKey: String) { @@ -27,7 +29,7 @@ public class RealDebrid { } // Fetches the device code from RD - public func getVerificationInfo() async throws -> DeviceCodeResponse { + public func getAuthUrl() async throws -> URL { var urlComponents = URLComponents(string: "\(baseAuthUrl)/device/code")! urlComponents.queryItems = [ URLQueryItem(name: "client_id", value: openSourceClientId), @@ -42,8 +44,18 @@ public class RealDebrid { do { let (data, _) = try await URLSession.shared.data(for: request) + // Validate the URL before doing anything else let rawResponse = try jsonDecoder.decode(DeviceCodeResponse.self, from: data) - return rawResponse + guard let directVerificationUrl = URL(string: rawResponse.directVerificationURL) else { + throw RDError.AuthQuery(description: "The verification URL is invalid") + } + + // Spawn the polling task separately + authTask = Task { + try await getDeviceCredentials(deviceCode: rawResponse.deviceCode) + } + + return directVerificationUrl } catch { print("Couldn't get the new client creds!") throw RDError.AuthQuery(description: error.localizedDescription) @@ -65,39 +77,33 @@ public class RealDebrid { let request = URLRequest(url: url) // Timer to poll RD API for credentials - authTask = Task { - var count = 0 + var count = 0 - while count < 12 { - if Task.isCancelled { - throw RDError.AuthQuery(description: "Token request cancelled.") - } - - let (data, _) = try await URLSession.shared.data(for: request) - - // We don't care if this fails - let rawResponse = try? self.jsonDecoder.decode(DeviceCredentialsResponse.self, from: data) - - // If there's a client ID from the response, end the task successfully - if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret { - await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId") - FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret") - - try await getTokens(deviceCode: deviceCode) - - return - } else { - try await Task.sleep(seconds: 5) - count += 1 - } + while count < 12 { + if Task.isCancelled { + throw RDError.AuthQuery(description: "Token request cancelled.") } - throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") + let (data, _) = try await URLSession.shared.data(for: request) + + // We don't care if this fails + let rawResponse = try? self.jsonDecoder.decode(DeviceCredentialsResponse.self, from: data) + + // If there's a client ID from the response, end the task successfully + if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret { + await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId") + FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret") + + try await getTokens(deviceCode: deviceCode) + + return + } else { + try await Task.sleep(seconds: 5) + count += 1 + } } - if case let .failure(error) = await authTask?.result { - throw error - } + throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") } // Fetch all tokens for the user and store in FerriteKeychain.shared @@ -163,8 +169,9 @@ public class RealDebrid { return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key } - - public func deleteTokens() async throws { + + // Deletes tokens from device and RD's servers + public func logout() async { FerriteKeychain.shared.delete("RealDebrid.RefreshToken") FerriteKeychain.shared.delete("RealDebrid.ClientSecret") await removeUserDefaultsValue(forKey: "RealDebrid.ClientId") @@ -198,7 +205,7 @@ public class RealDebrid { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - try await deleteTokens() + await logout() throw RDError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.") } else { throw RDError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift new file mode 100644 index 0000000..011c61f --- /dev/null +++ b/Ferrite/Models/DebridModels.swift @@ -0,0 +1,16 @@ +// +// DebridModels.swift +// Ferrite +// +// Created by Brian Dashore on 6/2/24. +// + +import Foundation + +public struct DebridIAFile { + +} + +public struct DebridCloudFile { + +} diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift new file mode 100644 index 0000000..3a51061 --- /dev/null +++ b/Ferrite/Protocols/Debrid.swift @@ -0,0 +1,34 @@ +// +// Debrid.swift +// Ferrite +// +// Created by Brian Dashore on 6/1/24. +// + +import Foundation + +public protocol DebridSource { + // ID of the service + var id: String { get } + + // Common authentication functions + func setApiKey(_ key: String) -> Bool + func logout() async +} + +public protocol PollingDebridSource: DebridSource { + // Task reference for polling + var authTask: Task? { get set } + + // Fetches the Auth URL + func getAuthUrl() async throws -> URL +} + +public protocol OAuthDebridSource: DebridSource { + + // Fetches the auth URL + func getAuthUrl() throws -> URL + + // Handles an OAuth callback + func handleAuthCallback(url: URL) throws +} diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index e7da300..5d464fd 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -450,10 +450,10 @@ public class DebridManager: ObservableObject { private func authenticateRd() async -> Bool { do { realDebridAuthProcessing = true - let verificationResponse = try await realDebrid.getVerificationInfo() + let authUrl = try await realDebrid.getAuthUrl() - if validateAuthUrl(URL(string: verificationResponse.directVerificationURL)) { - try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode) + if validateAuthUrl(authUrl) { + try await realDebrid.authTask?.value return true } else { throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid") @@ -469,10 +469,10 @@ public class DebridManager: ObservableObject { private func authenticateAd() async -> Bool { do { allDebridAuthProcessing = true - let pinResponse = try await allDebrid.getPinInfo() + let authUrl = try await allDebrid.getAuthUrl() - if validateAuthUrl(URL(string: pinResponse.userURL)) { - try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin) + if validateAuthUrl(authUrl) { + try await allDebrid.authTask?.value return true } else { throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid") @@ -488,7 +488,7 @@ public class DebridManager: ObservableObject { private func authenticatePm() async { do { premiumizeAuthProcessing = true - let tempAuthUrl = try premiumize.buildAuthUrl() + let tempAuthUrl = try premiumize.getAuthUrl() validateAuthUrl(tempAuthUrl, useAuthSession: true) } catch { @@ -538,16 +538,12 @@ public class DebridManager: ObservableObject { } private func logoutRd() async { - do { - try await realDebrid.deleteTokens() - enabledDebrids.remove(.realDebrid) - } catch { - await sendDebridError(error, prefix: "RealDebrid logout error") - } + await realDebrid.logout() + enabledDebrids.remove(.realDebrid) } private func logoutAd() { - allDebrid.deleteTokens() + allDebrid.logout() enabledDebrids.remove(.allDebrid) logManager?.info( @@ -557,7 +553,7 @@ public class DebridManager: ObservableObject { } private func logoutPm() { - premiumize.deleteTokens() + premiumize.logout() enabledDebrids.remove(.premiumize) } -- 2.45.2 From 37450ef97999a6cf9de1687f28e587446800f873 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sun, 2 Jun 2024 23:08:05 -0400 Subject: [PATCH 02/27] Debrid: Add InstantAvailability and download to protocol Unify IA into a passable client side structure and add a common download method to the DebridSource protocol. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 2 +- Ferrite/API/AllDebridWrapper.swift | 23 ++- Ferrite/API/PremiumizeWrapper.swift | 30 ++-- Ferrite/API/RealDebridWrapper.swift | 61 ++++---- Ferrite/Models/DebridModels.swift | 22 ++- Ferrite/Models/RealDebridModels.swift | 13 -- Ferrite/Protocols/Debrid.swift | 5 +- Ferrite/ViewModels/DebridManager.swift | 134 +++++++----------- .../Views/CommonViews/HybridSecureField.swift | 4 +- Ferrite/Views/SettingsView.swift | 2 +- .../Views/SheetViews/BatchChoiceView.swift | 6 +- 11 files changed, 160 insertions(+), 142 deletions(-) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 14919aa..ed626ca 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -394,6 +394,7 @@ 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */, 0C7ED14028D61BBA009E29AD /* BackupModels.swift */, 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */, + 0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */, 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */, 0C68135128BC1A7C00FAD890 /* GithubModels.swift */, 0C422E7F293542F300486D65 /* PremiumizeModels.swift */, @@ -403,7 +404,6 @@ 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */, 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */, 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */, - 0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */, ); path = Models; sourceTree = ""; diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 0f2c50e..c5616ee 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -9,7 +9,6 @@ import Foundation // TODO: Fix errors public class AllDebrid: PollingDebridSource { - public let id = "AllDebrid" public var authTask: Task? @@ -96,7 +95,7 @@ public class AllDebrid: PollingDebridSource { } public func getToken() -> String? { - return FerriteKeychain.shared.get("AllDebrid.ApiKey") + FerriteKeychain.shared.get("AllDebrid.ApiKey") } // Clears tokens. No endpoint to deregister a device @@ -146,6 +145,20 @@ public class AllDebrid: PollingDebridSource { } } + // Wrapper function to fetch a download link from the API + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { + let magnetID = try await addMagnet(magnet: magnet) + let lockedLink = try await fetchMagnetStatus( + magnetId: magnetID, + selectedIndex: iaFile?.fileId ?? 0 + ) + + try await saveLink(link: lockedLink) + let downloadUrl = try await unlockLink(lockedLink: lockedLink) + + return downloadUrl + } + // Adds a magnet link to the user's AD account public func addMagnet(magnet: Magnet) async throws -> Int { guard let magnetLink = magnet.link else { @@ -255,7 +268,7 @@ public class AllDebrid: PollingDebridSource { try await performRequest(request: &request, requestName: #function) } - public func instantAvailability(magnets: [Magnet]) async throws -> [IA] { + public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) } var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems)) @@ -266,10 +279,10 @@ public class AllDebrid: PollingDebridSource { let availableHashes = filteredMagnets.map { magnetResp in // Force unwrap is OK here since the filter caught any nil values let files = magnetResp.files!.enumerated().map { index, magnetFile in - IAFile(id: index, fileName: magnetFile.name) + DebridIAFile(fileId: index, name: magnetFile.name) } - return IA( + return DebridIA( magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index d771536..d0ab2eb 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -8,7 +8,6 @@ import Foundation public class Premiumize: OAuthDebridSource { - public let id = "Premiumize" let baseAuthUrl = "https://www.premiumize.me/authorize" @@ -58,7 +57,7 @@ public class Premiumize: OAuthDebridSource { } public func getToken() -> String? { - return FerriteKeychain.shared.get("Premiumize.AccessToken") + FerriteKeychain.shared.get("Premiumize.AccessToken") } // Clears tokens. No endpoint to deregister a device @@ -162,15 +161,15 @@ public class Premiumize: OAuthDebridSource { // Function to divide and execute DDL endpoint requests in parallel // Calls this for 10 requests at a time to not overwhelm API servers - public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [IA] { - let tempIA = try await withThrowingTaskGroup(of: Premiumize.IA.self) { group in + public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [DebridIA] { + let tempIA = try await withThrowingTaskGroup(of: DebridIA.self) { group in for magnet in magnetChunk { group.addTask { try await self.fetchDDL(magnet: magnet) } } - var chunkedIA: [Premiumize.IA] = [] + var chunkedIA: [DebridIA] = [] for try await ia in group { chunkedIA.append(ia) } @@ -181,7 +180,7 @@ public class Premiumize: OAuthDebridSource { } // Grabs DDL links - func fetchDDL(magnet: Magnet) async throws -> IA { + func fetchDDL(magnet: Magnet) async throws -> DebridIA { if magnet.hash == nil { throw PMError.EmptyData } @@ -200,13 +199,14 @@ public class Premiumize: OAuthDebridSource { if !rawResponse.content.isEmpty { let files = rawResponse.content.map { file in - IAFile( + DebridIAFile( + fileId: 0, name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path, streamUrlString: file.link ) } - return IA( + return DebridIA( magnet: magnet, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files @@ -216,6 +216,20 @@ public class Premiumize: OAuthDebridSource { } } + // Wrapper function to fetch a DDL link from the API + 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) + + if let iaFile, let streamUrlString = iaFile.streamUrlString { + return streamUrlString + } else if let premiumizeItem = ia, let firstFile = premiumizeItem.files[safe: 0], let streamUrlString = firstFile.streamUrlString { + return streamUrlString + } else { + throw PMError.FailedRequest(description: "Could not fetch your file from the Premiumize API") + } + } + func createTransfer(magnet: Magnet) async throws { guard let magnetLink = magnet.link else { throw PMError.FailedRequest(description: "The magnet link is invalid") diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 14e46a5..f11812d 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -8,7 +8,6 @@ import Foundation public class RealDebrid: PollingDebridSource { - public let id = "RealDebrid" public var authTask: Task? @@ -87,7 +86,7 @@ public class RealDebrid: PollingDebridSource { let (data, _) = try await URLSession.shared.data(for: request) // We don't care if this fails - let rawResponse = try? self.jsonDecoder.decode(DeviceCredentialsResponse.self, from: data) + let rawResponse = try? jsonDecoder.decode(DeviceCredentialsResponse.self, from: data) // If there's a client ID from the response, end the task successfully if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret { @@ -169,7 +168,7 @@ public class RealDebrid: PollingDebridSource { return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key } - + // Deletes tokens from device and RD's servers public func logout() async { FerriteKeychain.shared.delete("RealDebrid.RefreshToken") @@ -213,9 +212,8 @@ public class RealDebrid: PollingDebridSource { } // Checks if the magnet is streamable on RD - // Currently does not work for batch links - public func instantAvailability(magnets: [Magnet]) async throws -> [IA] { - var availableHashes: [RealDebrid.IA] = [] + public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { + var availableHashes: [DebridIA] = [] var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.compactMap(\.hash).joined(separator: "/"))")!) let data = try await performRequest(request: &request, requestName: #function) @@ -232,7 +230,7 @@ public class RealDebrid: PollingDebridSource { continue } - // Is this a batch + // Is this a batch? if data.rd.count > 1 || data.rd[0].count > 1 { // Batch array let batches = data.rd.map { fileDict in @@ -244,22 +242,18 @@ public class RealDebrid: PollingDebridSource { return RealDebrid.IABatch(files: batchFiles) } - // RD files array - // Possibly sort this in the future, but not sure how at the moment - var files: [RealDebrid.IAFile] = [] + var files: [DebridIAFile] = [] - for index in batches.indices { - let batchFiles = batches[index].files + for batch in batches { + let batchFileIds = batch.files.map(\.id) - for batchFileIndex in batchFiles.indices { - let batchFile = batchFiles[batchFileIndex] - - if !files.contains(where: { $0.name == batchFile.fileName }) { + for batchFile in batch.files { + if !files.contains(where: { $0.fileId == batchFile.id }) { files.append( - RealDebrid.IAFile( + DebridIAFile( + fileId: batchFile.id, name: batchFile.fileName, - batchIndex: index, - batchFileIndex: batchFileIndex + batchIds: batchFileIds ) ) } @@ -268,18 +262,18 @@ public class RealDebrid: PollingDebridSource { // TTL: 5 minutes availableHashes.append( - RealDebrid.IA( + DebridIA( magnet: Magnet(hash: hash, link: nil), expiryTimeStamp: Date().timeIntervalSince1970 + 300, - files: files, - batches: batches + files: files ) ) } else { availableHashes.append( - RealDebrid.IA( + DebridIA( magnet: Magnet(hash: hash, link: nil), - expiryTimeStamp: Date().timeIntervalSince1970 + 300 + expiryTimeStamp: Date().timeIntervalSince1970 + 300, + files: [] ) ) } @@ -288,6 +282,21 @@ public class RealDebrid: PollingDebridSource { return availableHashes } + // Wrapper function to fetch a download link from the API + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { + let selectedMagnetId = try await addMagnet(magnet: magnet) + + try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? []) + + let torrentLink = try await torrentInfo( + debridID: selectedMagnetId, + selectedIndex: iaFile?.fileId ?? 0 + ) + let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink) + + return downloadLink + } + // Adds a magnet link to the user's RD account public func addMagnet(magnet: Magnet) async throws -> String { guard let magnetLink = magnet.link else { @@ -335,9 +344,11 @@ public class RealDebrid: PollingDebridSource { let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data) + let filteredFiles = rawResponse.files.filter { $0.selected == 1 } + let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedIndex }) // Let the user know if a torrent is downloading - if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" { + if let torrentLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" { return torrentLink } else if rawResponse.status == "downloading" || rawResponse.status == "queued" { throw RDError.EmptyTorrents diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 011c61f..14a46a1 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -7,10 +7,24 @@ import Foundation -public struct DebridIAFile { - +public struct DebridIA: Sendable, Hashable { + let magnet: Magnet + let expiryTimeStamp: Double + var files: [DebridIAFile] } -public struct DebridCloudFile { - +public struct DebridIAFile: Hashable, Sendable { + let fileId: Int + let name: String + let streamUrlString: String? + let batchIds: [Int] + + init(fileId: Int, name: String, streamUrlString: String? = nil, batchIds: [Int] = []) { + self.fileId = fileId + self.name = name + self.streamUrlString = streamUrlString + self.batchIds = batchIds + } } + +public struct DebridCloudFile {} diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index 393acb6..134026f 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -92,13 +92,6 @@ public extension RealDebrid { // MARK: - Instant Availability client side structures - struct IA: Codable, Hashable, Sendable { - let magnet: Magnet - let expiryTimeStamp: Double - var files: [IAFile] = [] - var batches: [IABatch] = [] - } - struct IABatch: Codable, Hashable, Sendable { let files: [IABatchFile] } @@ -108,12 +101,6 @@ public extension RealDebrid { let fileName: String } - struct IAFile: Codable, Hashable, Sendable { - let name: String - let batchIndex: Int - let batchFileIndex: Int - } - // MARK: - addMagnet endpoint struct AddMagnetResponse: Codable, Sendable { diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 3a51061..396ebe6 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -14,6 +14,10 @@ public protocol DebridSource { // Common authentication functions func setApiKey(_ key: String) -> Bool func logout() async + + // Fetches a download link from a source + // Include the instant availability information with the args + func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String } public protocol PollingDebridSource: DebridSource { @@ -25,7 +29,6 @@ public protocol PollingDebridSource: DebridSource { } public protocol OAuthDebridSource: DebridSource { - // Fetches the auth URL func getAuthUrl() throws -> URL diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 5d464fd..c39c904 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -59,12 +59,12 @@ public class DebridManager: ObservableObject { var realDebridAuthProcessing: Bool = false // RealDebrid fetch variables - @Published var realDebridIAValues: [RealDebrid.IA] = [] + @Published var realDebridIAValues: [DebridIA] = [] @Published var showDeleteAlert: Bool = false - var selectedRealDebridItem: RealDebrid.IA? - var selectedRealDebridFile: RealDebrid.IAFile? + var selectedRealDebridItem: DebridIA? + var selectedRealDebridFile: DebridIAFile? var selectedRealDebridID: String? // TODO: Maybe make these generic? @@ -77,10 +77,10 @@ public class DebridManager: ObservableObject { var allDebridAuthProcessing: Bool = false // AllDebrid fetch variables - @Published var allDebridIAValues: [AllDebrid.IA] = [] + @Published var allDebridIAValues: [DebridIA] = [] - var selectedAllDebridItem: AllDebrid.IA? - var selectedAllDebridFile: AllDebrid.IAFile? + var selectedAllDebridItem: DebridIA? + var selectedAllDebridFile: DebridIAFile? // AllDebrid cloud variables @Published var allDebridCloudMagnets: [AllDebrid.MagnetStatusData] = [] @@ -91,10 +91,10 @@ public class DebridManager: ObservableObject { var premiumizeAuthProcessing: Bool = false // Premiumize fetch variables - @Published var premiumizeIAValues: [Premiumize.IA] = [] + @Published var premiumizeIAValues: [DebridIA] = [] - var selectedPremiumizeItem: Premiumize.IA? - var selectedPremiumizeFile: Premiumize.IAFile? + var selectedPremiumizeItem: DebridIA? + var selectedPremiumizeFile: DebridIAFile? // Premiumize cloud variables @Published var premiumizeCloudItems: [Premiumize.UserItem] = [] @@ -282,10 +282,10 @@ public class DebridManager: ObservableObject { return .none } - if realDebridMatch.batches.isEmpty { - return .full - } else { + if realDebridMatch.files.count > 1 { return .partial + } else { + return .full } case .allDebrid: guard let allDebridMatch = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else { @@ -578,7 +578,7 @@ public class DebridManager: ObservableObject { case .allDebrid: await fetchAdDownload(magnet: magnet, existingLockedLink: cloudInfo) case .premiumize: - await fetchPmDownload(cloudItemId: cloudInfo) + await fetchPmDownload(magnet: magnet, cloudItemId: cloudInfo) case .none: break } @@ -586,6 +586,7 @@ public class DebridManager: ObservableObject { func fetchRdDownload(magnet: Magnet?, existingLink: String?) async { // If an existing link is passed in args, set it to that. Otherwise, find one from RD cloud. + /* let torrentLink: String? if let existingLink { torrentLink = existingLink @@ -596,42 +597,23 @@ public class DebridManager: ObservableObject { let existingTorrent = realDebridCloudTorrents.first { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" } torrentLink = existingTorrent?.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] } + */ do { // If the links match from a user's downloads, no need to re-run a download + /* if let torrentLink, let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink) { downloadUrl = downloadLink - } else if let magnet { - // Add a magnet after all the cache checks fail - selectedRealDebridID = try await realDebrid.addMagnet(magnet: magnet) + } else */ + if let magnet { + let downloadLink = try await realDebrid.getDownloadLink( + magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile + ) - var fileIds: [Int] = [] - if let iaFile = selectedRealDebridFile { - guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else { - return - } - - fileIds = iaBatchFromFile.files.map(\.id) - } - - if let realDebridId = selectedRealDebridID { - try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds) - - let torrentLink = try await realDebrid.torrentInfo( - debridID: realDebridId, - selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0 - ) - let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) - - downloadUrl = downloadLink - } else { - logManager?.error( - "RealDebrid: Could not cache torrent with hash \(String(describing: magnet.hash))", - description: "Could not cache this torrent. Aborting." - ) - } + // Update the UI + downloadUrl = downloadLink } else { throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") } @@ -645,7 +627,7 @@ public class DebridManager: ObservableObject { default: await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled") - await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false) + // await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false) } logManager?.hideIndeterminateToast() @@ -695,8 +677,6 @@ public class DebridManager: ObservableObject { do { if let torrentID { try await realDebrid.deleteTorrent(debridID: torrentID) - } else if let selectedTorrentID = selectedRealDebridID { - try await realDebrid.deleteTorrent(debridID: selectedTorrentID) } else { throw RealDebrid.RDError.FailedRequest(description: "No torrent ID was provided") } @@ -720,34 +700,36 @@ public class DebridManager: ObservableObject { } } - // TODO: Integrate with AD saved links func fetchAdDownload(magnet: Magnet?, existingLockedLink: String?) async { // If an existing link is passed in args, set it to that. Otherwise, find one from AD cloud. - let lockedLink: String? - if let existingLockedLink { - lockedLink = existingLockedLink - } else { - // Bypass the TTL for up to date information - await fetchAdCloud(bypassTTL: true) + /* + let lockedLink: String? + if let existingLockedLink { + lockedLink = existingLockedLink + } else { + // Bypass the TTL for up to date information + await fetchAdCloud(bypassTTL: true) - let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" } - lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.id ?? 0]?.link - } + let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" } + lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.fileId ?? 0]?.link + } + */ do { - if let lockedLink, - let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink) - { - downloadUrl = unlockedLink - } else if let magnet { - let magnetID = try await allDebrid.addMagnet(magnet: magnet) - let lockedLink = try await allDebrid.fetchMagnetStatus( - magnetId: magnetID, - selectedIndex: selectedAllDebridFile?.id ?? 0 + /* + if let lockedLink, + let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink) + { + downloadUrl = unlockedLink + } else if let magnet { + */ + if let magnet { + let downloadLink = try await allDebrid.getDownloadLink( + magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile ) - try await allDebrid.saveLink(link: lockedLink) - downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink) + // Update UI + downloadUrl = downloadLink } else { throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API") } @@ -810,28 +792,22 @@ public class DebridManager: ObservableObject { } } - func fetchPmDownload(cloudItemId: String? = nil) async { + func fetchPmDownload(magnet: Magnet?, cloudItemId: String? = nil) async { do { if let cloudItemId { downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link - } else if let premiumizeFile = selectedPremiumizeFile { - downloadUrl = premiumizeFile.streamUrlString - } else if - let premiumizeItem = selectedPremiumizeItem, - let firstFile = premiumizeItem.files[safe: 0] - { - downloadUrl = firstFile.streamUrlString + } else if let magnet { + let downloadLink = try await premiumize.getDownloadLink( + magnet: magnet, ia: selectedPremiumizeItem, iaFile: selectedPremiumizeFile + ) + + downloadUrl = downloadLink } else { - throw Premiumize.PMError.FailedRequest(description: "There were no items or files found!") + throw Premiumize.PMError.FailedRequest(description: "Could not fetch your file from Premiumize's cache or API") } // Fetch one more time to add updated data into the PM cloud cache await fetchPmCloud(bypassTTL: true) - - // Add a PM transfer if the item exists - if let premiumizeItem = selectedPremiumizeItem { - try await premiumize.createTransfer(magnet: premiumizeItem.magnet) - } } catch { await sendDebridError(error, prefix: "Premiumize download error", cancelString: "Download or transfer cancelled") } diff --git a/Ferrite/Views/CommonViews/HybridSecureField.swift b/Ferrite/Views/CommonViews/HybridSecureField.swift index d1ac923..665623e 100644 --- a/Ferrite/Views/CommonViews/HybridSecureField.swift +++ b/Ferrite/Views/CommonViews/HybridSecureField.swift @@ -21,7 +21,7 @@ struct HybridSecureField: View { private var isFieldDisabled: Bool = false init(text: Binding, onCommit: (() -> Void)? = nil, showPassword: Bool = false) { - self._text = text + _text = text if let onCommit { self.onCommit = onCommit } @@ -57,6 +57,6 @@ struct HybridSecureField: View { extension HybridSecureField { public func fieldDisabled(_ isFieldDisabled: Bool) -> Self { - modifyViewProp({ $0.isFieldDisabled = isFieldDisabled }) + modifyViewProp { $0.isFieldDisabled = isFieldDisabled } } } diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 28f18fc..019c6bb 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -96,7 +96,7 @@ struct SettingsView: View { if changed { Task { let dataRecords = await WKWebsiteDataStore.default().dataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) - + await WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: dataRecords) } } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 771395f..5e7e9da 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -36,11 +36,11 @@ struct BatchChoiceView: View { } case .allDebrid: ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in - if file.fileName.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { - Button(file.fileName) { + if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { + Button(file.name) { debridManager.selectedAllDebridFile = file - queueCommonDownload(fileName: file.fileName) + queueCommonDownload(fileName: file.name) } } } -- 2.45.2 From 9e306eff1eb2e3fba8105ffc7cdc33a57abe9926 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jun 2024 15:46:18 -0400 Subject: [PATCH 03/27] Debrid: Add protocol for cloud handling Cloud downloads and torrents are now unified under their own protocol and models. Downloads and torrents are separated. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 75 ++++++++++++------- Ferrite/API/PremiumizeWrapper.swift | 24 +++++- Ferrite/API/RealDebridWrapper.swift | 58 ++++++++------ Ferrite/Models/DebridModels.swift | 16 +++- Ferrite/Protocols/Debrid.swift | 10 +++ Ferrite/ViewModels/DebridManager.swift | 34 ++++----- .../Library/Cloud/AllDebridCloudView.swift | 54 ++++++------- .../Library/Cloud/PremiumizeCloudView.swift | 18 ++--- .../Library/Cloud/RealDebridCloudView.swift | 44 +++++------ 9 files changed, 204 insertions(+), 129 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index c5616ee..d0251e5 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -10,6 +10,8 @@ import Foundation // TODO: Fix errors public class AllDebrid: PollingDebridSource { public let id = "AllDebrid" + public let abbreviation = "AD" + public let website = "https://alldebrid.com" public var authTask: Task? let baseApiUrl = "https://api.alldebrid.com/v4" @@ -203,28 +205,6 @@ public class AllDebrid: PollingDebridSource { } } - public func userMagnets() async throws -> [MagnetStatusData] { - var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status")) - - let data = try await performRequest(request: &request, requestName: #function) - let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data - - if rawResponse.magnets.isEmpty { - throw ADError.EmptyData - } else { - return rawResponse.magnets - } - } - - public func deleteMagnet(magnetId: Int) async throws { - let queryItems = [ - URLQueryItem(name: "id", value: String(magnetId)) - ] - var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems)) - - try await performRequest(request: &request, requestName: #function) - } - public func unlockLink(lockedLink: String) async throws -> String { let queryItems = [ URLQueryItem(name: "link", value: lockedLink) @@ -246,7 +226,40 @@ public class AllDebrid: PollingDebridSource { try await performRequest(request: &request, requestName: #function) } - public func savedLinks() async throws -> [SavedLink] { + // Referred to as "User magnets" in AllDebrid's API + public func getUserTorrents() async throws -> [DebridCloudTorrent] { + var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status")) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data + + if rawResponse.magnets.isEmpty { + throw ADError.EmptyData + } + + let torrents = rawResponse.magnets.map { magnetResponse in + DebridCloudTorrent( + torrentId: String(magnetResponse.id), + fileName: magnetResponse.filename, + status: magnetResponse.status, + hash: magnetResponse.hash, + links: magnetResponse.links.map { $0.link } + ) + } + + return torrents + } + + public func deleteTorrent(torrentId: String) async throws { + let queryItems = [ + URLQueryItem(name: "id", value: torrentId) + ] + var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems)) + + try await performRequest(request: &request, requestName: #function) + } + + public func getUserDownloads() async throws -> [DebridCloudDownload] { var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links")) let data = try await performRequest(request: &request, requestName: #function) @@ -254,14 +267,22 @@ public class AllDebrid: PollingDebridSource { if rawResponse.links.isEmpty { throw ADError.EmptyData - } else { - return rawResponse.links } + + // The link is also the ID + let downloads = rawResponse.links.map { link in + DebridCloudDownload( + downloadId: link.link, fileName: link.filename, link: link.link + ) + } + + return downloads } - public func deleteLink(link: String) async throws { + // The downloadId is actually the download link + public func deleteDownload(downloadId: String) async throws { let queryItems = [ - URLQueryItem(name: "link", value: link) + URLQueryItem(name: "link", value: downloadId) ] var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links/delete", queryItems: queryItems)) diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index d0ab2eb..54d489f 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -9,6 +9,8 @@ import Foundation public class Premiumize: OAuthDebridSource { public let id = "Premiumize" + public let abbreviation = "PM" + public let website = "https://premiumize.me" let baseAuthUrl = "https://www.premiumize.me/authorize" let baseApiUrl = "https://www.premiumize.me/api" @@ -247,7 +249,7 @@ public class Premiumize: OAuthDebridSource { try await performRequest(request: &request, requestName: #function) } - func userItems() async throws -> [UserItem] { + public func getUserDownloads() async throws -> [DebridCloudDownload] { var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!) let data = try await performRequest(request: &request, requestName: #function) @@ -257,7 +259,12 @@ public class Premiumize: OAuthDebridSource { throw PMError.EmptyData } - return rawResponse.files + // The "link" is the ID for Premiumize + let downloads = rawResponse.files.map { file in + DebridCloudDownload(downloadId: file.id, fileName: file.name, link: file.id) + } + + return downloads } func itemDetails(itemID: String) async throws -> ItemDetailsResponse { @@ -275,16 +282,25 @@ public class Premiumize: OAuthDebridSource { return rawResponse } - func deleteItem(itemID: String) async throws { + public func deleteDownload(downloadId: String) async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/delete")!) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") var bodyComponents = URLComponents() - bodyComponents.queryItems = [URLQueryItem(name: "id", value: itemID)] + bodyComponents.queryItems = [URLQueryItem(name: "id", value: downloadId)] request.httpBody = bodyComponents.query?.data(using: .utf8) try await performRequest(request: &request, requestName: #function) } + + // No user torrents for Premiumize + public func getUserTorrents() async throws -> [DebridCloudTorrent] { + return [] + } + + public func deleteTorrent(torrentId: String) async throws { + return + } } diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index f11812d..59a362b 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -9,6 +9,8 @@ import Foundation public class RealDebrid: PollingDebridSource { public let id = "RealDebrid" + public let abbreviation = "RD" + public let website = "https://real-debrid.com" public var authTask: Task? let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" @@ -357,24 +359,6 @@ public class RealDebrid: PollingDebridSource { } } - // Gets the user's torrent library - public func userTorrents() async throws -> [UserTorrentsResponse] { - var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!) - - let data = try await performRequest(request: &request, requestName: #function) - let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data) - - return rawResponse - } - - // Deletes a torrent download from RD - public func deleteTorrent(debridID: String) async throws { - var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!) - request.httpMethod = "DELETE" - - try await performRequest(request: &request, requestName: #function) - } - // Downloads link from selectFiles for playback public func unrestrictLink(debridDownloadLink: String) async throws -> String { var request = URLRequest(url: URL(string: "\(baseApiUrl)/unrestrict/link")!) @@ -392,18 +376,48 @@ public class RealDebrid: PollingDebridSource { return rawResponse.download } + // Gets the user's torrent library + public func getUserTorrents() async throws -> [DebridCloudTorrent] { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data) + let torrents = rawResponse.map { response in + DebridCloudTorrent( + torrentId: response.id, + fileName: response.filename, + status: response.status, + hash: response.hash, + links: response.links + ) + } + + return torrents + } + + // Deletes a torrent download from RD + public func deleteTorrent(torrentId: String) async throws { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(torrentId)")!) + request.httpMethod = "DELETE" + + try await performRequest(request: &request, requestName: #function) + } + // Gets the user's downloads - public func userDownloads() async throws -> [UserDownloadsResponse] { + public func getUserDownloads() async throws -> [DebridCloudDownload] { var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads")!) let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data) + let downloads = rawResponse.map { response in + DebridCloudDownload(downloadId: response.id, fileName: response.filename, link: response.download) + } - return rawResponse + return downloads } - public func deleteDownload(debridID: String) async throws { - var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(debridID)")!) + public func deleteDownload(downloadId: String) async throws { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(downloadId)")!) request.httpMethod = "DELETE" try await performRequest(request: &request, requestName: #function) diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 14a46a1..1431807 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -7,7 +7,7 @@ import Foundation -public struct DebridIA: Sendable, Hashable { +public struct DebridIA: Hashable, Sendable { let magnet: Magnet let expiryTimeStamp: Double var files: [DebridIAFile] @@ -27,4 +27,16 @@ public struct DebridIAFile: Hashable, Sendable { } } -public struct DebridCloudFile {} +public struct DebridCloudDownload: Hashable, Sendable { + let downloadId: String + let fileName: String + let link: String +} + +public struct DebridCloudTorrent: Hashable, Sendable { + let torrentId: String + let fileName: String + let status: String + let hash: String + let links: [String] +} diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 396ebe6..b4cd275 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -10,6 +10,8 @@ import Foundation public protocol DebridSource { // ID of the service var id: String { get } + var abbreviation: String { get } + var website: String { get } // Common authentication functions func setApiKey(_ key: String) -> Bool @@ -18,6 +20,14 @@ public protocol DebridSource { // Fetches a download link from a source // Include the instant availability information with the args func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String + + // Fetches cloud information from the service + func getUserDownloads() async throws -> [DebridCloudDownload] + func getUserTorrents() async throws -> [DebridCloudTorrent] + + // Deletes information from the service + func deleteDownload(downloadId: String) async throws + func deleteTorrent(torrentId: String) async throws } public protocol PollingDebridSource: DebridSource { diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index c39c904..87139b3 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -69,8 +69,8 @@ public class DebridManager: ObservableObject { // TODO: Maybe make these generic? // RealDebrid cloud variables - @Published var realDebridCloudTorrents: [RealDebrid.UserTorrentsResponse] = [] - @Published var realDebridCloudDownloads: [RealDebrid.UserDownloadsResponse] = [] + @Published var realDebridCloudTorrents: [DebridCloudTorrent] = [] + @Published var realDebridCloudDownloads: [DebridCloudDownload] = [] var realDebridCloudTTL: Double = 0.0 // AllDebrid auth variables @@ -83,8 +83,8 @@ public class DebridManager: ObservableObject { var selectedAllDebridFile: DebridIAFile? // AllDebrid cloud variables - @Published var allDebridCloudMagnets: [AllDebrid.MagnetStatusData] = [] - @Published var allDebridCloudLinks: [AllDebrid.SavedLink] = [] + @Published var allDebridCloudMagnets: [DebridCloudTorrent] = [] + @Published var allDebridCloudLinks: [DebridCloudDownload] = [] var allDebridCloudTTL: Double = 0.0 // Premiumize auth variables @@ -97,7 +97,7 @@ public class DebridManager: ObservableObject { var selectedPremiumizeFile: DebridIAFile? // Premiumize cloud variables - @Published var premiumizeCloudItems: [Premiumize.UserItem] = [] + @Published var premiumizeCloudItems: [DebridCloudDownload] = [] var premiumizeCloudTTL: Double = 0.0 init() { @@ -651,8 +651,8 @@ public class DebridManager: ObservableObject { public func fetchRdCloud(bypassTTL: Bool = false) async { if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL { do { - realDebridCloudTorrents = try await realDebrid.userTorrents() - realDebridCloudDownloads = try await realDebrid.userDownloads() + realDebridCloudTorrents = try await realDebrid.getUserTorrents() + realDebridCloudDownloads = try await realDebrid.getUserDownloads() // 5 minutes realDebridCloudTTL = Date().timeIntervalSince1970 + 300 @@ -664,7 +664,7 @@ public class DebridManager: ObservableObject { func deleteRdDownload(downloadID: String) async { do { - try await realDebrid.deleteDownload(debridID: downloadID) + try await realDebrid.deleteDownload(downloadId: downloadID) // Bypass TTL to get current RD values await fetchRdCloud(bypassTTL: true) @@ -676,7 +676,7 @@ public class DebridManager: ObservableObject { func deleteRdTorrent(torrentID: String? = nil, presentError: Bool = true) async { do { if let torrentID { - try await realDebrid.deleteTorrent(debridID: torrentID) + try await realDebrid.deleteTorrent(torrentId: torrentID) } else { throw RealDebrid.RDError.FailedRequest(description: "No torrent ID was provided") } @@ -688,7 +688,7 @@ public class DebridManager: ObservableObject { func checkRdUserDownloads(userTorrentLink: String) async -> String? { do { let existingLinks = realDebridCloudDownloads.first { $0.link == userTorrentLink } - if let existingLink = existingLinks?.download { + if let existingLink = existingLinks?.fileName { return existingLink } else { return try await realDebrid.unrestrictLink(debridDownloadLink: userTorrentLink) @@ -761,8 +761,8 @@ public class DebridManager: ObservableObject { public func fetchAdCloud(bypassTTL: Bool = false) async { if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL { do { - allDebridCloudMagnets = try await allDebrid.userMagnets() - allDebridCloudLinks = try await allDebrid.savedLinks() + allDebridCloudMagnets = try await allDebrid.getUserTorrents() + allDebridCloudLinks = try await allDebrid.getUserDownloads() // 5 minutes allDebridCloudTTL = Date().timeIntervalSince1970 + 300 @@ -774,7 +774,7 @@ public class DebridManager: ObservableObject { func deleteAdLink(link: String) async { do { - try await allDebrid.deleteLink(link: link) + try await allDebrid.deleteDownload(downloadId: link) await fetchAdCloud(bypassTTL: true) } catch { @@ -782,9 +782,9 @@ public class DebridManager: ObservableObject { } } - func deleteAdMagnet(magnetId: Int) async { + func deleteAdMagnet(magnetId: String) async { do { - try await allDebrid.deleteMagnet(magnetId: magnetId) + try await allDebrid.deleteTorrent(torrentId: magnetId) await fetchAdCloud(bypassTTL: true) } catch { @@ -817,7 +817,7 @@ public class DebridManager: ObservableObject { public func fetchPmCloud(bypassTTL: Bool = false) async { if bypassTTL || Date().timeIntervalSince1970 > premiumizeCloudTTL { do { - let userItems = try await premiumize.userItems() + let userItems = try await premiumize.getUserDownloads() withAnimation { premiumizeCloudItems = userItems } @@ -835,7 +835,7 @@ public class DebridManager: ObservableObject { public func deletePmItem(id: String) async { do { - try await premiumize.deleteItem(itemID: id) + try await premiumize.deleteDownload(downloadId: id) // Bypass TTL to get current RD values await fetchPmCloud(bypassTTL: true) diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index f0cf0ce..ed09447 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -17,17 +17,17 @@ struct AllDebridCloudView: View { var body: some View { DisclosureGroup("Links") { ForEach(debridManager.allDebridCloudLinks.filter { - searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased()) - }, id: \.self) { downloadResponse in - Button(downloadResponse.filename) { + searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) + }, id: \.self) { cloudDownload in + Button(cloudDownload.fileName) { navModel.resultFromCloud = true - navModel.selectedTitle = downloadResponse.filename - debridManager.downloadUrl = downloadResponse.link + navModel.selectedTitle = cloudDownload.fileName + debridManager.downloadUrl = cloudDownload.link PersistenceController.shared.createHistory( HistoryEntryJson( - name: downloadResponse.filename, - url: downloadResponse.link, + name: cloudDownload.fileName, + url: cloudDownload.link, source: DebridType.allDebrid.toString() ), performSave: true @@ -43,9 +43,9 @@ struct AllDebridCloudView: View { } .onDelete { offsets in for index in offsets { - if let savedLink = debridManager.allDebridCloudLinks[safe: index] { + if let cloudDownload = debridManager.allDebridCloudLinks[safe: index] { Task { - await debridManager.deleteAdLink(link: savedLink.link) + await debridManager.deleteAdLink(link: cloudDownload.downloadId) } } } @@ -54,26 +54,26 @@ struct AllDebridCloudView: View { DisclosureGroup("Magnets") { ForEach(debridManager.allDebridCloudMagnets.filter { - searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased()) - }, id: \.id) { magnet in + searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) + }, id: \.self) { cloudTorrent in Button { - if magnet.status == "Ready", !magnet.links.isEmpty { + if cloudTorrent.status == "Ready", !cloudTorrent.links.isEmpty { navModel.resultFromCloud = true - navModel.selectedTitle = magnet.filename + navModel.selectedTitle = cloudTorrent.fileName var historyInfo = HistoryEntryJson( - name: magnet.filename, + name: cloudTorrent.fileName, source: DebridType.allDebrid.toString() ) Task { - if magnet.links.count == 1 { - if let lockedLink = magnet.links[safe: 0]?.link { - await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: lockedLink) - + if cloudTorrent.links.count == 1 { + if let torrentLink = cloudTorrent.links[safe: 0] { + await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink) if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel @@ -81,7 +81,7 @@ struct AllDebridCloudView: View { } } } else { - let magnet = Magnet(hash: magnet.hash, link: nil) + let magnet = Magnet(hash: cloudTorrent.hash, link: nil) // Do not clear old IA values await debridManager.populateDebridIA([magnet]) @@ -93,27 +93,29 @@ struct AllDebridCloudView: View { } } } - } label: { VStack(alignment: .leading, spacing: 10) { - Text(magnet.filename) + Text(cloudTorrent.fileName) + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(4) HStack { - Text(magnet.status) + Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() - DebridLabelView(cloudLinks: magnet.links.map(\.link)) + DebridLabelView(cloudLinks: cloudTorrent.links) } .font(.caption) } } - .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.9, animation: .easeOut(duration: 0.2)) + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) .tint(.primary) } .onDelete { offsets in for index in offsets { - if let magnet = debridManager.allDebridCloudMagnets[safe: index] { + if let cloudTorrent = debridManager.allDebridCloudMagnets[safe: index] { Task { - await debridManager.deleteAdMagnet(magnetId: magnet.id) + await debridManager.deleteAdMagnet(magnetId: cloudTorrent.torrentId) } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index d309684..e9acb9a 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -17,20 +17,20 @@ struct PremiumizeCloudView: View { var body: some View { DisclosureGroup("Items") { ForEach(debridManager.premiumizeCloudItems.filter { - searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased()) - }, id: \.id) { item in - Button(item.name) { + searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) + }, id: \.self) { cloudDownload in + Button(cloudDownload.fileName) { Task { navModel.resultFromCloud = true - navModel.selectedTitle = item.name + navModel.selectedTitle = cloudDownload.fileName - await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: item.id) + await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: cloudDownload.downloadId) if !debridManager.downloadUrl.isEmpty { PersistenceController.shared.createHistory( HistoryEntryJson( - name: item.name, - url: debridManager.downloadUrl, + name: cloudDownload.fileName, + url: cloudDownload.link, source: DebridType.premiumize.toString() ), performSave: true @@ -48,9 +48,9 @@ struct PremiumizeCloudView: View { } .onDelete { offsets in for index in offsets { - if let item = debridManager.premiumizeCloudItems[safe: index] { + if let cloudDownload = debridManager.premiumizeCloudItems[safe: index] { Task { - await debridManager.deletePmItem(id: item.id) + await debridManager.deletePmItem(id: cloudDownload.downloadId) } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index b133ef8..0cc39cf 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -18,17 +18,17 @@ struct RealDebridCloudView: View { Group { DisclosureGroup("Downloads") { ForEach(debridManager.realDebridCloudDownloads.filter { - searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased()) - }, id: \.self) { downloadResponse in - Button(downloadResponse.filename) { + searchText.isEmpty ? true : $0.fileName .lowercased().contains(searchText.lowercased()) + }, id: \.self) { cloudDownload in + Button(cloudDownload.fileName) { navModel.resultFromCloud = true - navModel.selectedTitle = downloadResponse.filename - debridManager.downloadUrl = downloadResponse.download + navModel.selectedTitle = cloudDownload.fileName + debridManager.downloadUrl = cloudDownload.link PersistenceController.shared.createHistory( HistoryEntryJson( - name: downloadResponse.filename, - url: downloadResponse.download, + name: cloudDownload.fileName, + url: cloudDownload.link, source: DebridType.realDebrid.toString() ), performSave: true @@ -44,9 +44,9 @@ struct RealDebridCloudView: View { } .onDelete { offsets in for index in offsets { - if let downloadResponse = debridManager.realDebridCloudDownloads[safe: index] { + if let cloudDownload = debridManager.realDebridCloudDownloads[safe: index] { Task { - await debridManager.deleteRdDownload(downloadID: downloadResponse.id) + await debridManager.deleteRdDownload(downloadID: cloudDownload.downloadId) } } } @@ -55,21 +55,21 @@ struct RealDebridCloudView: View { DisclosureGroup("Torrents") { ForEach(debridManager.realDebridCloudTorrents.filter { - searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased()) - }, id: \.self) { torrentResponse in + searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) + }, id: \.self) { cloudTorrent in Button { - if torrentResponse.status == "downloaded", !torrentResponse.links.isEmpty { + if cloudTorrent.status == "downloaded", !cloudTorrent.links.isEmpty { navModel.resultFromCloud = true - navModel.selectedTitle = torrentResponse.filename + navModel.selectedTitle = cloudTorrent.fileName var historyInfo = HistoryEntryJson( - name: torrentResponse.filename, + name: cloudTorrent.fileName, source: DebridType.realDebrid.toString() ) Task { - if torrentResponse.links.count == 1 { - if let torrentLink = torrentResponse.links[safe: 0] { + if cloudTorrent.links.count == 1 { + if let torrentLink = cloudTorrent.links[safe: 0] { await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink) if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl @@ -82,7 +82,7 @@ struct RealDebridCloudView: View { } } } else { - let magnet = Magnet(hash: torrentResponse.hash, link: nil) + let magnet = Magnet(hash: cloudTorrent.hash, link: nil) // Do not clear old IA values await debridManager.populateDebridIA([magnet]) @@ -96,15 +96,15 @@ struct RealDebridCloudView: View { } } label: { VStack(alignment: .leading, spacing: 10) { - Text(torrentResponse.filename) + Text(cloudTorrent.fileName) .font(.callout) .fixedSize(horizontal: false, vertical: true) .lineLimit(4) HStack { - Text(torrentResponse.status.capitalizingFirstLetter()) + Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() - DebridLabelView(cloudLinks: torrentResponse.links) + DebridLabelView(cloudLinks: cloudTorrent.links) } .font(.caption) } @@ -114,9 +114,9 @@ struct RealDebridCloudView: View { } .onDelete { offsets in for index in offsets { - if let torrentResponse = debridManager.realDebridCloudTorrents[safe: index] { + if let cloudTorrent = debridManager.realDebridCloudTorrents[safe: index] { Task { - await debridManager.deleteRdTorrent(torrentID: torrentResponse.id) + await debridManager.deleteRdTorrent(torrentID: cloudTorrent.torrentId) } } } -- 2.45.2 From 37ef64224eae8a1004aa03cfda0c1016288cd24d Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jun 2024 16:00:40 -0400 Subject: [PATCH 04/27] Debrid: Order API implementations Reorder everything and mark off where different functions are located. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 58 ++++++++------- Ferrite/API/PremiumizeWrapper.swift | 106 +++++++++++++++------------- Ferrite/API/RealDebridWrapper.swift | 10 +++ 3 files changed, 102 insertions(+), 72 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index d0251e5..95d4ea4 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -19,6 +19,8 @@ public class AllDebrid: PollingDebridSource { let jsonDecoder = JSONDecoder() + // MARK: - Auth + // Fetches information for PIN auth public func getAuthUrl() async throws -> URL { let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get") @@ -106,6 +108,8 @@ public class AllDebrid: PollingDebridSource { UserDefaults.standard.removeObject(forKey: "AllDebrid.UseManualKey") } + // MARK: - Common request + // 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 = getToken() else { @@ -147,6 +151,34 @@ public class AllDebrid: PollingDebridSource { } } + // MARK: - Instant availability + + public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { + let queryItems = magnets.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) + let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data + + let filteredMagnets = rawResponse.magnets.filter { $0.instant == true && $0.files != nil } + let availableHashes = filteredMagnets.map { magnetResp in + // Force unwrap is OK here since the filter caught any nil values + let files = magnetResp.files!.enumerated().map { index, magnetFile in + DebridIAFile(fileId: index, name: magnetFile.name) + } + + return DebridIA( + magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), + expiryTimeStamp: Date().timeIntervalSince1970 + 300, + files: files + ) + } + + return availableHashes + } + + // MARK: - Downloading + // Wrapper function to fetch a download link from the API public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { let magnetID = try await addMagnet(magnet: magnet) @@ -226,6 +258,8 @@ public class AllDebrid: PollingDebridSource { try await performRequest(request: &request, requestName: #function) } + // MARK: - Cloud methods + // Referred to as "User magnets" in AllDebrid's API public func getUserTorrents() async throws -> [DebridCloudTorrent] { var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status")) @@ -288,28 +322,4 @@ public class AllDebrid: PollingDebridSource { try await performRequest(request: &request, requestName: #function) } - - public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { - let queryItems = magnets.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) - let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data - - let filteredMagnets = rawResponse.magnets.filter { $0.instant == true && $0.files != nil } - let availableHashes = filteredMagnets.map { magnetResp in - // Force unwrap is OK here since the filter caught any nil values - let files = magnetResp.files!.enumerated().map { index, magnetFile in - DebridIAFile(fileId: index, name: magnetFile.name) - } - - return DebridIA( - magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), - expiryTimeStamp: Date().timeIntervalSince1970 + 300, - files: files - ) - } - - return availableHashes - } } diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 54d489f..3216ad6 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -18,6 +18,8 @@ public class Premiumize: OAuthDebridSource { let jsonDecoder = JSONDecoder() + // MARK: - Auth + public func getAuthUrl() throws -> URL { var urlComponents = URLComponents(string: baseAuthUrl)! urlComponents.queryItems = [ @@ -68,6 +70,8 @@ public class Premiumize: OAuthDebridSource { UserDefaults.standard.removeObject(forKey: "Premiumize.UseManualKey") } + // MARK: - Common request + // 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 = getToken() else { @@ -112,54 +116,7 @@ public class Premiumize: OAuthDebridSource { } } - // Function to divide and execute cache endpoint requests in parallel - // Calls this for 100 hashes at a time due to API limits - public func divideCacheRequests(magnets: [Magnet]) async throws -> [Magnet] { - let availableMagnets = try await withThrowingTaskGroup(of: [Magnet].self) { group in - for chunk in magnets.chunked(into: 100) { - group.addTask { - try await self.checkCache(magnets: chunk) - } - } - - var chunkedMagnets: [Magnet] = [] - for try await magnetArray in group { - chunkedMagnets += magnetArray - } - - return chunkedMagnets - } - - return availableMagnets - } - - // Parent function for initial checking of the cache - func checkCache(magnets: [Magnet]) async throws -> [Magnet] { - var urlComponents = URLComponents(string: "\(baseApiUrl)/cache/check")! - urlComponents.queryItems = magnets.map { URLQueryItem(name: "items[]", value: $0.hash) } - guard let url = urlComponents.url else { - throw PMError.InvalidUrl - } - - var request = URLRequest(url: url) - - let data = try await performRequest(request: &request, requestName: #function) - let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data) - - if rawResponse.response.isEmpty { - throw PMError.EmptyData - } else { - let availableMagnets = magnets.enumerated().compactMap { index, magnet in - if rawResponse.response[safe: index] == true { - return magnet - } else { - return nil - } - } - - return availableMagnets - } - } + // MARK: - Instant availability // Function to divide and execute DDL endpoint requests in parallel // Calls this for 10 requests at a time to not overwhelm API servers @@ -218,6 +175,57 @@ public class Premiumize: OAuthDebridSource { } } + // Function to divide and execute cache endpoint requests in parallel + // Calls this for 100 hashes at a time due to API limits + public func divideCacheRequests(magnets: [Magnet]) async throws -> [Magnet] { + let availableMagnets = try await withThrowingTaskGroup(of: [Magnet].self) { group in + for chunk in magnets.chunked(into: 100) { + group.addTask { + try await self.checkCache(magnets: chunk) + } + } + + var chunkedMagnets: [Magnet] = [] + for try await magnetArray in group { + chunkedMagnets += magnetArray + } + + return chunkedMagnets + } + + return availableMagnets + } + + // Parent function for initial checking of the cache + func checkCache(magnets: [Magnet]) async throws -> [Magnet] { + var urlComponents = URLComponents(string: "\(baseApiUrl)/cache/check")! + urlComponents.queryItems = magnets.map { URLQueryItem(name: "items[]", value: $0.hash) } + guard let url = urlComponents.url else { + throw PMError.InvalidUrl + } + + var request = URLRequest(url: url) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data) + + if rawResponse.response.isEmpty { + throw PMError.EmptyData + } else { + let availableMagnets = magnets.enumerated().compactMap { index, magnet in + if rawResponse.response[safe: index] == true { + return magnet + } else { + return nil + } + } + + return availableMagnets + } + } + + // MARK: - Downloading + // Wrapper function to fetch a DDL link from the API public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { // Store the item in PM cloud for later use @@ -249,6 +257,8 @@ public class Premiumize: OAuthDebridSource { try await performRequest(request: &request, requestName: #function) } + // MARK: - Cloud methods + public func getUserDownloads() async throws -> [DebridCloudDownload] { var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!) diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 59a362b..38256bb 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -29,6 +29,8 @@ public class RealDebrid: PollingDebridSource { UserDefaults.standard.removeObject(forKey: forKey) } + // MARK: - Auth + // Fetches the device code from RD public func getAuthUrl() async throws -> URL { var urlComponents = URLComponents(string: "\(baseAuthUrl)/device/code")! @@ -189,6 +191,8 @@ public class RealDebrid: PollingDebridSource { } } + // MARK: - Common request + // 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 { @@ -213,6 +217,8 @@ 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] = [] @@ -284,6 +290,8 @@ 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?) async throws -> String { let selectedMagnetId = try await addMagnet(magnet: magnet) @@ -376,6 +384,8 @@ public class RealDebrid: PollingDebridSource { return rawResponse.download } + // MARK: - Cloud methods + // Gets the user's torrent library public func getUserTorrents() async throws -> [DebridCloudTorrent] { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!) -- 2.45.2 From f902142fee0f2bb9e153fd08c9da53683e8afd72 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jun 2024 16:15:17 -0400 Subject: [PATCH 05/27] Debrid: Add Premiumize to InstantAvailability Also add the requirement to the protocol. Signed-off-by: kingbri --- Ferrite/API/PremiumizeWrapper.swift | 23 +++++++++++++++++++++++ Ferrite/Protocols/Debrid.swift | 2 ++ Ferrite/ViewModels/DebridManager.swift | 19 ++----------------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 3216ad6..43884b1 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -118,6 +118,29 @@ public class Premiumize: OAuthDebridSource { // MARK: - Instant availability + public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] { + var collectedIA: [DebridIA] = [] + + // 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) + } else { + return nil + } + } + + let availableMagnets = try await divideCacheRequests(magnets: strippedMagnets) + + // Split DDL requests into chunks of 10 + for chunk in availableMagnets.chunked(into: 10) { + let tempIA = try await divideDDLRequests(magnetChunk: chunk) + collectedIA += tempIA + } + + return collectedIA + } + // Function to divide and execute DDL endpoint requests in parallel // Calls this for 10 requests at a time to not overwhelm API servers public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [DebridIA] { diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index b4cd275..8853145 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -17,6 +17,8 @@ public protocol DebridSource { func setApiKey(_ key: String) -> Bool func logout() async + func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] + // Fetches a download link from a source // Include the instant availability information with the args func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 87139b3..57b1feb 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -246,23 +246,8 @@ public class DebridManager: ObservableObject { if enabledDebrids.contains(.premiumize) { do { - // Only strip magnets that don't have an associated link for PM - let strippedResultMagnets: [Magnet] = resultMagnets.compactMap { - if let magnetLink = $0.link { - return Magnet(hash: $0.hash, link: magnetLink) - } else { - return nil - } - } - - let availableMagnets = try await premiumize.divideCacheRequests(magnets: strippedResultMagnets) - - // Split DDL requests into chunks of 10 - for chunk in availableMagnets.chunked(into: 10) { - let tempIA = try await premiumize.divideDDLRequests(magnetChunk: chunk) - - premiumizeIAValues += tempIA - } + let fetchedPremiumizeIA = try await premiumize.instantAvailability(magnets: sendMagnets) + premiumizeIAValues += fetchedPremiumizeIA } catch { await sendDebridError(error, prefix: "Premiumize IA fetch error") } -- 2.45.2 From c641fdf300c84c9b0612d1f6dfb3b6d81d5fd9d1 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jun 2024 16:24:07 -0400 Subject: [PATCH 06/27] Debrid: Add source to all models Gives an ID of where the struct came from. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 4 +++- Ferrite/API/PremiumizeWrapper.swift | 3 ++- Ferrite/API/RealDebridWrapper.swift | 5 ++++- Ferrite/Models/DebridModels.swift | 3 +++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 95d4ea4..c32f4e8 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -169,6 +169,7 @@ public class AllDebrid: PollingDebridSource { return DebridIA( magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), + source: self.id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -274,6 +275,7 @@ public class AllDebrid: PollingDebridSource { let torrents = rawResponse.magnets.map { magnetResponse in DebridCloudTorrent( torrentId: String(magnetResponse.id), + source: self.id, fileName: magnetResponse.filename, status: magnetResponse.status, hash: magnetResponse.hash, @@ -306,7 +308,7 @@ public class AllDebrid: PollingDebridSource { // The link is also the ID let downloads = rawResponse.links.map { link in DebridCloudDownload( - downloadId: link.link, fileName: link.filename, link: link.link + downloadId: link.link, source: self.id, fileName: link.filename, link: link.link ) } diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 43884b1..c7d8bbe 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -190,6 +190,7 @@ public class Premiumize: OAuthDebridSource { return DebridIA( magnet: magnet, + source: self.id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -294,7 +295,7 @@ public class Premiumize: OAuthDebridSource { // The "link" is the ID for Premiumize let downloads = rawResponse.files.map { file in - DebridCloudDownload(downloadId: file.id, fileName: file.name, link: file.id) + DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id) } return downloads diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 38256bb..7d9257b 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -272,6 +272,7 @@ public class RealDebrid: PollingDebridSource { availableHashes.append( DebridIA( magnet: Magnet(hash: hash, link: nil), + source: self.id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -280,6 +281,7 @@ public class RealDebrid: PollingDebridSource { availableHashes.append( DebridIA( magnet: Magnet(hash: hash, link: nil), + source: self.id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: [] ) @@ -395,6 +397,7 @@ public class RealDebrid: PollingDebridSource { let torrents = rawResponse.map { response in DebridCloudTorrent( torrentId: response.id, + source: self.id, fileName: response.filename, status: response.status, hash: response.hash, @@ -420,7 +423,7 @@ 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 - DebridCloudDownload(downloadId: response.id, fileName: response.filename, link: response.download) + DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download) } return downloads diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 1431807..3eb4e5f 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -9,6 +9,7 @@ import Foundation public struct DebridIA: Hashable, Sendable { let magnet: Magnet + let source: String let expiryTimeStamp: Double var files: [DebridIAFile] } @@ -29,12 +30,14 @@ public struct DebridIAFile: Hashable, Sendable { public struct DebridCloudDownload: Hashable, Sendable { let downloadId: String + let source: String let fileName: String let link: String } public struct DebridCloudTorrent: Hashable, Sendable { let torrentId: String + let source: String let fileName: String let status: String let hash: String -- 2.45.2 From f9ecc746a1004a3ba5902be473a883e29f44251d Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jun 2024 16:25:32 -0400 Subject: [PATCH 07/27] Tree: Format Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 2 +- Ferrite/API/PremiumizeWrapper.swift | 8 ++--- Ferrite/API/RealDebridWrapper.swift | 4 +-- Ferrite/ViewModels/DebridManager.swift | 30 +++++++++---------- .../Library/Cloud/RealDebridCloudView.swift | 2 +- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index c32f4e8..eb0cbb7 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -279,7 +279,7 @@ public class AllDebrid: PollingDebridSource { fileName: magnetResponse.filename, status: magnetResponse.status, hash: magnetResponse.hash, - links: magnetResponse.links.map { $0.link } + links: magnetResponse.links.map(\.link) ) } diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index c7d8bbe..7d2b6fb 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -190,7 +190,7 @@ public class Premiumize: OAuthDebridSource { return DebridIA( magnet: magnet, - source: self.id, + source: id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -331,10 +331,8 @@ public class Premiumize: OAuthDebridSource { // No user torrents for Premiumize public func getUserTorrents() async throws -> [DebridCloudTorrent] { - return [] + [] } - public func deleteTorrent(torrentId: String) async throws { - return - } + public func deleteTorrent(torrentId: String) async throws {} } diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 7d9257b..379df7b 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -272,7 +272,7 @@ public class RealDebrid: PollingDebridSource { availableHashes.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: self.id, + source: id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -281,7 +281,7 @@ public class RealDebrid: PollingDebridSource { availableHashes.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: self.id, + source: id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: [] ) diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 57b1feb..4cb85bd 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -572,26 +572,26 @@ public class DebridManager: ObservableObject { func fetchRdDownload(magnet: Magnet?, existingLink: String?) async { // If an existing link is passed in args, set it to that. Otherwise, find one from RD cloud. /* - let torrentLink: String? - if let existingLink { - torrentLink = existingLink - } else { - // Bypass the TTL for up to date information - await fetchRdCloud(bypassTTL: true) + let torrentLink: String? + if let existingLink { + torrentLink = existingLink + } else { + // Bypass the TTL for up to date information + await fetchRdCloud(bypassTTL: true) - let existingTorrent = realDebridCloudTorrents.first { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" } - torrentLink = existingTorrent?.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] - } - */ + let existingTorrent = realDebridCloudTorrents.first { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" } + torrentLink = existingTorrent?.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] + } + */ do { // If the links match from a user's downloads, no need to re-run a download /* - if let torrentLink, - let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink) - { - downloadUrl = downloadLink - } else */ + if let torrentLink, + let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink) + { + downloadUrl = downloadLink + } else */ if let magnet { let downloadLink = try await realDebrid.getDownloadLink( magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 0cc39cf..f1a2224 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -18,7 +18,7 @@ struct RealDebridCloudView: View { Group { DisclosureGroup("Downloads") { ForEach(debridManager.realDebridCloudDownloads.filter { - searchText.isEmpty ? true : $0.fileName .lowercased().contains(searchText.lowercased()) + searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) }, id: \.self) { cloudDownload in Button(cloudDownload.fileName) { navModel.resultFromCloud = true -- 2.45.2 From 273403b7112bcbd9197b6c3e1c8b5c9688fec3e9 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jun 2024 16:27:47 -0400 Subject: [PATCH 08/27] Debrid: Remove per-API IA structures These aren't required since IA is a unified type. Only keep batch IA for RealDebrid since it helps clear up confusion when gathering InstantAvailability results. Signed-off-by: kingbri --- Ferrite/Models/AllDebridModels.swift | 13 ------------- Ferrite/Models/PremiumizeModels.swift | 13 ------------- Ferrite/Models/RealDebridModels.swift | 2 +- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/Ferrite/Models/AllDebridModels.swift b/Ferrite/Models/AllDebridModels.swift index c12b5c1..02b07b1 100644 --- a/Ferrite/Models/AllDebridModels.swift +++ b/Ferrite/Models/AllDebridModels.swift @@ -166,17 +166,4 @@ public extension AllDebrid { case name = "n" } } - - // MARK: - InstantAvailablity client side structures - - struct IA: Codable, Hashable { - let magnet: Magnet - let expiryTimeStamp: Double - var files: [IAFile] - } - - struct IAFile: Codable, Hashable { - let id: Int - let fileName: String - } } diff --git a/Ferrite/Models/PremiumizeModels.swift b/Ferrite/Models/PremiumizeModels.swift index a6f30e7..4572526 100644 --- a/Ferrite/Models/PremiumizeModels.swift +++ b/Ferrite/Models/PremiumizeModels.swift @@ -51,19 +51,6 @@ public extension Premiumize { } } - // MARK: - InstantAvailability client side structures - - struct IA: Codable, Hashable { - let magnet: Magnet - let expiryTimeStamp: Double - let files: [IAFile] - } - - struct IAFile: Codable, Hashable { - let name: String - let streamUrlString: String - } - // MARK: - AllItemsResponse (listall endpoint) struct AllItemsResponse: Codable { diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index 134026f..ba455b1 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -90,7 +90,7 @@ public extension RealDebrid { var filesize: Int } - // MARK: - Instant Availability client side structures + // MARK: - Instant Availability batch structures (used for client-side conversion) struct IABatch: Codable, Hashable, Sendable { let files: [IABatchFile] -- 2.45.2 From 853b956105efe96b03eb14de90b143bbc46e726b Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 3 Jun 2024 23:56:56 -0400 Subject: [PATCH 09/27] Debrid: Add common functions for existing torrents/downloads This fixes cloud torrent fetching and also doesn't duplicate torrents inside the cloud service. Unrestricted links don't get duplicated, so no need to check against those. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 23 ++++-- Ferrite/API/PremiumizeWrapper.swift | 7 +- Ferrite/API/RealDebridWrapper.swift | 25 +++++-- Ferrite/Protocols/Debrid.swift | 12 ++-- Ferrite/ViewModels/DebridManager.swift | 71 +++++-------------- .../Library/Cloud/AllDebridCloudView.swift | 21 +++--- .../Library/Cloud/RealDebridCloudView.swift | 21 +++--- .../Views/SheetViews/BatchChoiceView.swift | 2 +- 8 files changed, 88 insertions(+), 94 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index eb0cbb7..7111cd1 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -181,10 +181,18 @@ public class AllDebrid: PollingDebridSource { // MARK: - Downloading // Wrapper function to fetch a download link from the API - public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { - let magnetID = try await addMagnet(magnet: magnet) + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent] = []) async throws -> String { + let selectedMagnetId: String + + if let existingMagnet = userTorrents.first(where: { $0.hash == magnet.hash && $0.status == "Ready" }) { + selectedMagnetId = existingMagnet.torrentId + } else { + let magnetId = try await addMagnet(magnet: magnet) + selectedMagnetId = String(magnetId) + } + let lockedLink = try await fetchMagnetStatus( - magnetId: magnetID, + magnetId: selectedMagnetId, selectedIndex: iaFile?.fileId ?? 0 ) @@ -221,9 +229,9 @@ public class AllDebrid: PollingDebridSource { } } - public func fetchMagnetStatus(magnetId: Int, selectedIndex: Int?) async throws -> String { + public func fetchMagnetStatus(magnetId: String, selectedIndex: Int?) async throws -> String { let queryItems = [ - URLQueryItem(name: "id", value: String(magnetId)) + URLQueryItem(name: "id", value: magnetId) ] var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems)) @@ -315,6 +323,11 @@ public class AllDebrid: PollingDebridSource { return downloads } + // Not used + public func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) async throws -> String? { + nil + } + // The downloadId is actually the download link public func deleteDownload(downloadId: String) async throws { let queryItems = [ diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 7d2b6fb..8ec7356 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -251,7 +251,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?) async throws -> String { + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent] = []) async throws -> String { // Store the item in PM cloud for later use try await createTransfer(magnet: magnet) @@ -316,6 +316,11 @@ public class Premiumize: OAuthDebridSource { return rawResponse } + public func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) async throws -> String? { + // Link is the cloud item ID + try await itemDetails(itemID: link).link + } + public func deleteDownload(downloadId: String) async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/delete")!) request.httpMethod = "POST" diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 379df7b..2ec74ee 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -295,14 +295,22 @@ public class RealDebrid: PollingDebridSource { // MARK: - Downloading // Wrapper function to fetch a download link from the API - public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String { - let selectedMagnetId = try await addMagnet(magnet: magnet) + public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent] = []) async throws -> String { + let selectedMagnetId: String - try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? []) + // Don't queue a new job if the torrent already exists + if let existingTorrent = userTorrents.first(where: { $0.hash == magnet.hash && $0.status == "downloaded" }) { + selectedMagnetId = existingTorrent.torrentId + } else { + selectedMagnetId = try await addMagnet(magnet: magnet) + try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? []) + } + + // RealDebrid has 1 as the first ID for a file let torrentLink = try await torrentInfo( debridID: selectedMagnetId, - selectedIndex: iaFile?.fileId ?? 0 + selectedFileId: iaFile?.fileId ?? 1 ) let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink) @@ -351,13 +359,13 @@ public class RealDebrid: PollingDebridSource { } // Gets the info of a torrent from a given ID - public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String { + public func torrentInfo(debridID: String, selectedFileId: Int?) async throws -> String { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!) let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data) let filteredFiles = rawResponse.files.filter { $0.selected == 1 } - let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedIndex }) + let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedFileId }) // Let the user know if a torrent is downloading if let torrentLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" { @@ -429,6 +437,11 @@ public class RealDebrid: PollingDebridSource { return downloads } + // Not used + public func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) -> String? { + nil + } + public func deleteDownload(downloadId: String) async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(downloadId)")!) request.httpMethod = "DELETE" diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 8853145..c2d6e20 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -21,14 +21,16 @@ public protocol DebridSource { // Fetches a download link from a source // Include the instant availability information with the args - func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String + // Torrents also checked here + func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent]) async throws -> String - // Fetches cloud information from the service + // User downloads functions func getUserDownloads() async throws -> [DebridCloudDownload] - func getUserTorrents() async throws -> [DebridCloudTorrent] - - // Deletes information from the service + func checkUserDownloads(link: String, userDownloads: [DebridCloudDownload]) async throws -> String? func deleteDownload(downloadId: String) async throws + + // User torrent functions + func getUserTorrents() async throws -> [DebridCloudTorrent] func deleteTorrent(torrentId: String) async throws } diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 4cb85bd..632ae3c 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -559,42 +559,21 @@ public class DebridManager: ObservableObject { switch selectedDebridType { case .realDebrid: - await fetchRdDownload(magnet: magnet, existingLink: cloudInfo) + await fetchRdDownload(magnet: magnet, cloudInfo: cloudInfo) case .allDebrid: - await fetchAdDownload(magnet: magnet, existingLockedLink: cloudInfo) + await fetchAdDownload(magnet: magnet, cloudInfo: cloudInfo) case .premiumize: - await fetchPmDownload(magnet: magnet, cloudItemId: cloudInfo) + await fetchPmDownload(magnet: magnet, cloudInfo: cloudInfo) case .none: break } } - func fetchRdDownload(magnet: Magnet?, existingLink: String?) async { - // If an existing link is passed in args, set it to that. Otherwise, find one from RD cloud. - /* - let torrentLink: String? - if let existingLink { - torrentLink = existingLink - } else { - // Bypass the TTL for up to date information - await fetchRdCloud(bypassTTL: true) - - let existingTorrent = realDebridCloudTorrents.first { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" } - torrentLink = existingTorrent?.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] - } - */ - + func fetchRdDownload(magnet: Magnet?, cloudInfo: String?) async { do { - // If the links match from a user's downloads, no need to re-run a download - /* - if let torrentLink, - let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink) - { - downloadUrl = downloadLink - } else */ if let magnet { let downloadLink = try await realDebrid.getDownloadLink( - magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile + magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile, userTorrents: realDebridCloudTorrents ) // Update the UI @@ -612,7 +591,9 @@ public class DebridManager: ObservableObject { default: await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled") - // await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false) + if let torrentId = selectedRealDebridID { + try? await realDebrid.deleteTorrent(torrentId: torrentId) + } } logManager?.hideIndeterminateToast() @@ -685,32 +666,11 @@ public class DebridManager: ObservableObject { } } - func fetchAdDownload(magnet: Magnet?, existingLockedLink: String?) async { - // If an existing link is passed in args, set it to that. Otherwise, find one from AD cloud. - /* - let lockedLink: String? - if let existingLockedLink { - lockedLink = existingLockedLink - } else { - // Bypass the TTL for up to date information - await fetchAdCloud(bypassTTL: true) - - let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" } - lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.fileId ?? 0]?.link - } - */ - + func fetchAdDownload(magnet: Magnet?, cloudInfo: String?) async { do { - /* - if let lockedLink, - let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink) - { - downloadUrl = unlockedLink - } else if let magnet { - */ if let magnet { let downloadLink = try await allDebrid.getDownloadLink( - magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile + magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile, userTorrents: allDebridCloudMagnets ) // Update UI @@ -777,11 +737,14 @@ public class DebridManager: ObservableObject { } } - func fetchPmDownload(magnet: Magnet?, cloudItemId: String? = nil) async { + func fetchPmDownload(magnet: Magnet?, cloudInfo: String? = nil) async { do { - if let cloudItemId { - downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link - } else if let magnet { + if let cloudInfo { + downloadUrl = try await premiumize.checkUserDownloads(link: cloudInfo, userDownloads: premiumizeCloudItems) ?? "" + return + } + + if let magnet { let downloadLink = try await premiumize.getDownloadLink( magnet: magnet, ia: selectedPremiumizeItem, iaFile: selectedPremiumizeFile ) diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index ed09447..0f342b5 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -67,9 +67,14 @@ struct AllDebridCloudView: View { ) Task { - if cloudTorrent.links.count == 1 { - if let torrentLink = cloudTorrent.links[safe: 0] { - await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink) + let magnet = Magnet(hash: cloudTorrent.hash, link: nil) + await debridManager.populateDebridIA([magnet]) + if debridManager.selectDebridResult(magnet: magnet) { + // Is this a batch? + + if cloudTorrent.links.count == 1 { + await debridManager.fetchDebridDownload(magnet: magnet) + if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) @@ -79,14 +84,8 @@ struct AllDebridCloudView: View { navModel: navModel ) } - } - } else { - let magnet = Magnet(hash: cloudTorrent.hash, link: nil) - - // Do not clear old IA values - await debridManager.populateDebridIA([magnet]) - - if debridManager.selectDebridResult(magnet: magnet) { + } else { + navModel.selectedMagnet = magnet navModel.selectedHistoryInfo = historyInfo navModel.currentChoiceSheet = .batch } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index f1a2224..383c1ea 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -68,9 +68,14 @@ struct RealDebridCloudView: View { ) Task { - if cloudTorrent.links.count == 1 { - if let torrentLink = cloudTorrent.links[safe: 0] { - await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink) + let magnet = Magnet(hash: cloudTorrent.hash, link: nil) + await debridManager.populateDebridIA([magnet]) + if debridManager.selectDebridResult(magnet: magnet) { + // Is this a batch? + + if cloudTorrent.links.count == 1 { + await debridManager.fetchDebridDownload(magnet: magnet) + if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) @@ -80,14 +85,8 @@ struct RealDebridCloudView: View { navModel: navModel ) } - } - } else { - let magnet = Magnet(hash: cloudTorrent.hash, link: nil) - - // Do not clear old IA values - await debridManager.populateDebridIA([magnet]) - - if debridManager.selectDebridResult(magnet: magnet) { + } else { + navModel.selectedMagnet = magnet navModel.selectedHistoryInfo = historyInfo navModel.currentChoiceSheet = .batch } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 5e7e9da..7d2d3f4 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -85,7 +85,7 @@ struct BatchChoiceView: View { // Common function to communicate betwen VMs and queue/display a download func queueCommonDownload(fileName: String) { debridManager.currentDebridTask = Task { - await debridManager.fetchDebridDownload(magnet: navModel.resultFromCloud ? nil : navModel.selectedMagnet) + await debridManager.fetchDebridDownload(magnet: navModel.selectedMagnet) if !debridManager.downloadUrl.isEmpty { try? await Task.sleep(seconds: 1) -- 2.45.2 From 1d8a965bb7b031b87d48716b75759ec7ae649ad3 Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jun 2024 10:52:35 -0400 Subject: [PATCH 10/27] Debrid: Fix RealDebrid download handling The torrent ID is no longer stored in the DebridManager. Signed-off-by: kingbri --- Ferrite/API/RealDebridWrapper.swift | 41 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 2ec74ee..7bf301e 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -296,25 +296,34 @@ public class RealDebrid: PollingDebridSource { // Wrapper function to fetch a download link from the API public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?, userTorrents: [DebridCloudTorrent] = []) async throws -> String { - let selectedMagnetId: String + var selectedMagnetId: String = "" - // Don't queue a new job if the torrent already exists - if let existingTorrent = userTorrents.first(where: { $0.hash == magnet.hash && $0.status == "downloaded" }) { - selectedMagnetId = existingTorrent.torrentId - } else { - selectedMagnetId = try await addMagnet(magnet: magnet) + 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" }) { + selectedMagnetId = existingTorrent.torrentId + } else { + selectedMagnetId = try await addMagnet(magnet: magnet) - try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? []) + try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? []) + } + + // RealDebrid has 1 as the first ID for a file + let torrentLink = try await torrentInfo( + debridID: selectedMagnetId, + selectedFileId: iaFile?.fileId ?? 1 + ) + let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink) + + return downloadLink + } catch { + if case RealDebrid.RDError.EmptyTorrents = error, !selectedMagnetId.isEmpty { + try? await deleteTorrent(torrentId: selectedMagnetId) + } + + // Re-raise the error to the calling function + throw error } - - // RealDebrid has 1 as the first ID for a file - let torrentLink = try await torrentInfo( - debridID: selectedMagnetId, - selectedFileId: iaFile?.fileId ?? 1 - ) - let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink) - - return downloadLink } // Adds a magnet link to the user's RD account -- 2.45.2 From 3137d2065651fb0af3dad3d36fb2a129d08beb7c Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jun 2024 12:09:19 -0400 Subject: [PATCH 11/27] Debrid: Migrate common arrays to their API classes Add convenience vars which makes the API classes the source of truth for any interaction. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 27 +++++++---- Ferrite/API/PremiumizeWrapper.swift | 25 ++++++---- Ferrite/API/RealDebridWrapper.swift | 46 ++++++++++-------- Ferrite/Protocols/Debrid.swift | 18 +++++-- Ferrite/ViewModels/DebridManager.swift | 47 ++++++++++--------- .../Library/Cloud/RealDebridCloudView.swift | 4 +- 6 files changed, 101 insertions(+), 66 deletions(-) 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 { -- 2.45.2 From 449b0eaa7e779f05ac4ea40bcfe870f12c3d0f5f Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 4 Jun 2024 13:56:48 -0400 Subject: [PATCH 12/27] Debrid: Allow for UI updates Mark as an ObservableObject so the UI can see parameters that are being updated in the class. Signed-off-by: kingbri --- Ferrite/API/PremiumizeWrapper.swift | 8 ++--- Ferrite/API/RealDebridWrapper.swift | 8 ++--- Ferrite/Protocols/Debrid.swift | 2 +- Ferrite/ViewModels/DebridManager.swift | 29 ++++++++----------- Ferrite/ViewModels/ScrapingViewModel.swift | 4 +-- .../Library/BookmarksView.swift | 2 +- .../Library/LibraryPickerView.swift | 2 +- .../SearchResult/SearchFilterHeaderView.swift | 2 +- Ferrite/Views/SettingsView.swift | 2 +- 9 files changed, 27 insertions(+), 32 deletions(-) diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 8a08d6b..9fcd234 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -12,14 +12,14 @@ public class Premiumize: OAuthDebridSource { public let abbreviation = "PM" public let website = "https://premiumize.me" - public var authProcessing: Bool = false + @Published public var authProcessing: Bool = false public var isLoggedIn: Bool { 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] = [] let baseAuthUrl = "https://www.premiumize.me/authorize" let baseApiUrl = "https://www.premiumize.me/api" diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 53efaa2..e6f46de 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -13,16 +13,16 @@ public class RealDebrid: PollingDebridSource { public let website = "https://real-debrid.com" public var authTask: Task? - public var authProcessing: Bool = false + @Published 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] = [] + @Published public var IAValues: [DebridIA] = [] + @Published public var cloudDownloads: [DebridCloudDownload] = [] + @Published public var cloudTorrents: [DebridCloudTorrent] = [] let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" let baseApiUrl = "https://api.real-debrid.com/rest/1.0" diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 3c35014..8af5adf 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -7,7 +7,7 @@ import Foundation -public protocol DebridSource { +public protocol DebridSource: ObservableObject { // ID of the service var id: String { get } var abbreviation: String { get } diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 84d9e02..6e30bcf 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -12,16 +12,20 @@ import SwiftUI public class DebridManager: ObservableObject { // Linked classes var logManager: LoggingManager? - let realDebrid: RealDebrid = .init() - let allDebrid: AllDebrid = .init() - let premiumize: Premiumize = .init() + @Published var realDebrid: RealDebrid = .init() + @Published var allDebrid: AllDebrid = .init() + @Published var premiumize: Premiumize = .init() - lazy var debridSources: [DebridSource] = [realDebrid, allDebrid, premiumize] + lazy var debridSources: [any DebridSource] = [realDebrid, allDebrid, premiumize] // UI Variables @Published var showWebView: Bool = false @Published var showAuthSession: Bool = false + var hasEnabledDebrids: Bool { + debridSources.contains { $0.isLoggedIn } + } + // Service agnostic variables @Published var enabledDebrids: Set = [] { didSet { @@ -60,9 +64,6 @@ public class DebridManager: ObservableObject { // RealDebrid auth variables var realDebridAuthProcessing: Bool = false - // RealDebrid fetch variables - @Published var realDebridIAValues: [DebridIA] = [] - @Published var showDeleteAlert: Bool = false var selectedRealDebridItem: DebridIA? @@ -78,9 +79,6 @@ public class DebridManager: ObservableObject { // AllDebrid auth variables var allDebridAuthProcessing: Bool = false - // AllDebrid fetch variables - @Published var allDebridIAValues: [DebridIA] = [] - var selectedAllDebridItem: DebridIA? var selectedAllDebridFile: DebridIAFile? @@ -92,9 +90,6 @@ public class DebridManager: ObservableObject { // Premiumize auth variables var premiumizeAuthProcessing: Bool = false - // Premiumize fetch variables - @Published var premiumizeIAValues: [DebridIA] = [] - var selectedPremiumizeItem: DebridIA? var selectedPremiumizeFile: DebridIAFile? @@ -171,9 +166,9 @@ public class DebridManager: ObservableObject { // Cleans all cached IA values in the event of a full IA refresh public func clearIAValues() { - realDebridIAValues = [] - allDebridIAValues = [] - premiumizeIAValues = [] + for debridSource in debridSources { + debridSource.IAValues = [] + } } // Clears all selected files and items @@ -216,7 +211,7 @@ public class DebridManager: ObservableObject { } } 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) + premiumize.IAValues.remove(at: IAIndex) return true } else { return false diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 46720dd..13c65a0 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -80,7 +80,7 @@ class ScrapingViewModel: ObservableObject { cleanedSearchText = searchText.lowercased() - if await !debridManager.enabledDebrids.isEmpty { + if await !debridManager.hasEnabledDebrids { await debridManager.clearIAValues() } @@ -114,7 +114,7 @@ class ScrapingViewModel: ObservableObject { var failedSourceNames: [String] = [] for await (requestResult, sourceName) in group { if let requestResult { - if await !debridManager.enabledDebrids.isEmpty { + if await !debridManager.hasEnabledDebrids { await debridManager.populateDebridIA(requestResult.magnets) } diff --git a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift index 60bd3cf..eb0d632 100644 --- a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift +++ b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift @@ -56,7 +56,7 @@ struct BookmarksView: View { .frame(height: 15) } .task { - if debridManager.enabledDebrids.count > 0 { + if debridManager.hasEnabledDebrids { let magnets = bookmarks.compactMap { if let magnetHash = $0.magnetHash { return Magnet(hash: magnetHash, link: $0.magnetLink) diff --git a/Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift b/Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift index 8c97e58..d3c88f6 100644 --- a/Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift +++ b/Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift @@ -19,7 +19,7 @@ struct LibraryPickerView: View { Text("Bookmarks").tag(NavigationViewModel.LibraryPickerSegment.bookmarks) Text("History").tag(NavigationViewModel.LibraryPickerSegment.history) - if !debridManager.enabledDebrids.isEmpty { + if debridManager.hasEnabledDebrids { Text("Cloud").tag(NavigationViewModel.LibraryPickerSegment.debridCloud) } } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift index 2602c68..8797a1a 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift @@ -60,7 +60,7 @@ struct SearchFilterHeaderView: View { // MARK: - Cache status picker - if !debridManager.enabledDebrids.isEmpty { + if debridManager.hasEnabledDebrids { IAFilterView() } diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 019c6bb..8914583 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -128,7 +128,7 @@ struct SettingsView: View { } Section(header: InlineHeader("Default actions")) { - if debridManager.enabledDebrids.count > 0 { + if debridManager.hasEnabledDebrids { NavigationLink { DefaultActionPickerView( actionRequirement: .debrid, -- 2.45.2 From 46e66ab457c6e722d5af70d28c4a3f1dd8a8dc5d Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 5 Jun 2024 12:33:11 -0400 Subject: [PATCH 13/27] Debrid: Migrate more components to the protocol Protocols can't be used in ObservedObjects. Observable in iOS 17 and up solves this, but Ferrite targets iOS 16 and up, so add a type-erased StateObject which supports protocols. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 + Ferrite/API/AllDebridWrapper.swift | 14 +- Ferrite/API/PremiumizeWrapper.swift | 12 +- Ferrite/API/RealDebridWrapper.swift | 23 +-- Ferrite/Models/DebridModels.swift | 6 + Ferrite/Protocols/Debrid.swift | 6 +- Ferrite/Utils/Store.swift | 148 ++++++++++++++++++ Ferrite/ViewModels/DebridManager.swift | 61 +++----- Ferrite/ViewModels/ScrapingViewModel.swift | 2 +- .../Debrid/DebridLabelView.swift | 38 ++--- .../Filters/SelectedDebridFilterView.swift | 16 +- .../Library/Cloud/AllDebridCloudView.swift | 2 +- .../Library/Cloud/RealDebridCloudView.swift | 2 +- .../SearchResult/SearchFilterHeaderView.swift | 2 +- .../SearchResult/SearchResultInfoView.swift | 4 +- .../Settings/SettingsDebridInfoView.swift | 32 ++-- Ferrite/Views/LibraryView.swift | 4 +- Ferrite/Views/SettingsView.swift | 8 +- .../Views/SheetViews/BatchChoiceView.swift | 10 +- 19 files changed, 272 insertions(+), 122 deletions(-) create mode 100644 Ferrite/Utils/Store.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index ed626ca..f957fd7 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ 0C84FCE729E4B61A00B0DFE4 /* FilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */; }; 0C84FCE929E5ADEF00B0DFE4 /* FilterAmountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */; }; 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.swift */; }; + 0C8AE2482C0FFB6600701675 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8AE2472C0FFB6600701675 /* Store.swift */; }; 0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */; }; 0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; }; 0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */; }; @@ -244,6 +245,7 @@ 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterModels.swift; sourceTree = ""; }; 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterAmountLabelView.swift; sourceTree = ""; }; 0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = ""; }; + 0C8AE2472C0FFB6600701675 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = ""; }; 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = ""; }; 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = ""; }; @@ -457,6 +459,7 @@ 0C44E2A728D4DDDC007711AE /* Application.swift */, 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */, 0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */, + 0C8AE2472C0FFB6600701675 /* Store.swift */, ); path = Utils; sourceTree = ""; @@ -858,6 +861,7 @@ 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, + 0C8AE2482C0FFB6600701675 /* Store.swift in Sources */, 0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 34268fa..0be235f 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -8,10 +8,10 @@ import Foundation // TODO: Fix errors -public class AllDebrid: PollingDebridSource { - public let id = "AllDebrid" - public let abbreviation = "AD" - public let website = "https://alldebrid.com" +public class AllDebrid: PollingDebridSource, ObservableObject { + public let id = DebridInfo( + name: "AllDebrid", abbreviation: "AD", website: "https://alldebrid.com" + ) public var authTask: Task? public var authProcessing: Bool = false @@ -178,7 +178,7 @@ public class AllDebrid: PollingDebridSource { return DebridIA( magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), - source: self.id, + source: self.id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -292,7 +292,7 @@ public class AllDebrid: PollingDebridSource { cloudTorrents = rawResponse.magnets.map { magnetResponse in DebridCloudTorrent( torrentId: String(magnetResponse.id), - source: self.id, + source: self.id.name, fileName: magnetResponse.filename, status: magnetResponse.status, hash: magnetResponse.hash, @@ -325,7 +325,7 @@ public class AllDebrid: PollingDebridSource { // The link is also the ID cloudDownloads = rawResponse.links.map { link in DebridCloudDownload( - downloadId: link.link, source: self.id, fileName: link.filename, link: link.link + downloadId: link.link, source: self.id.name, fileName: link.filename, link: link.link ) } diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 9fcd234..939386f 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -7,10 +7,10 @@ import Foundation -public class Premiumize: OAuthDebridSource { - public let id = "Premiumize" - public let abbreviation = "PM" - public let website = "https://premiumize.me" +public class Premiumize: OAuthDebridSource, ObservableObject { + public let id = DebridInfo( + name: "Premiumize", abbreviation: "PM", website: "https://premiumize.me" + ) @Published public var authProcessing: Bool = false public var isLoggedIn: Bool { @@ -195,7 +195,7 @@ public class Premiumize: OAuthDebridSource { return DebridIA( magnet: magnet, - source: id, + source: id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -300,7 +300,7 @@ public class Premiumize: OAuthDebridSource { // The "link" is the ID for Premiumize cloudDownloads = rawResponse.files.map { file in - DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id) + DebridCloudDownload(downloadId: file.id, source: self.id.name, fileName: file.name, link: file.id) } return cloudDownloads diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index e6f46de..7204cee 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -7,10 +7,10 @@ import Foundation -public class RealDebrid: PollingDebridSource { - public let id = "RealDebrid" - public let abbreviation = "RD" - public let website = "https://real-debrid.com" +public class RealDebrid: PollingDebridSource, ObservableObject { + public let id = DebridInfo( + name: "RealDebrid", abbreviation: "RD", website: "https://real-debrid.com" + ) public var authTask: Task? @Published public var authProcessing: Bool = false @@ -20,9 +20,14 @@ public class RealDebrid: PollingDebridSource { FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil } - @Published public var IAValues: [DebridIA] = [] + @Published public var IAValues: [DebridIA] = [] { + willSet { + self.objectWillChange.send() + } + } @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] + var cloudTTL: Double = 0.0 let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" let baseApiUrl = "https://api.real-debrid.com/rest/1.0" @@ -282,7 +287,7 @@ public class RealDebrid: PollingDebridSource { IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: id, + source: id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -291,7 +296,7 @@ public class RealDebrid: PollingDebridSource { IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: id, + source: id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: [] ) @@ -422,7 +427,7 @@ public class RealDebrid: PollingDebridSource { cloudTorrents = rawResponse.map { response in DebridCloudTorrent( torrentId: response.id, - source: self.id, + source: self.id.name, fileName: response.filename, status: response.status, hash: response.hash, @@ -448,7 +453,7 @@ public class RealDebrid: PollingDebridSource { let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data) cloudDownloads = rawResponse.map { response in - DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download) + DebridCloudDownload(downloadId: response.id, source: self.id.name, fileName: response.filename, link: response.download) } return cloudDownloads diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 3eb4e5f..0a4316d 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -7,6 +7,12 @@ import Foundation +public struct DebridInfo: Hashable, Sendable { + let name: String + let abbreviation: String + let website: String +} + public struct DebridIA: Hashable, Sendable { let magnet: Magnet let source: String diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 8af5adf..3a842cb 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -7,11 +7,9 @@ import Foundation -public protocol DebridSource: ObservableObject { +public protocol DebridSource: AnyObservableObject { // ID of the service - var id: String { get } - var abbreviation: String { get } - var website: String { get } + var id: DebridInfo { get } // Auth variables var authProcessing: Bool { get set } diff --git a/Ferrite/Utils/Store.swift b/Ferrite/Utils/Store.swift new file mode 100644 index 0000000..a663fe6 --- /dev/null +++ b/Ferrite/Utils/Store.swift @@ -0,0 +1,148 @@ +// +// Store.swift +// Ferrite +// +// +// Originally created by William Baker on 09/06/2022. +// https://github.com/Tiny-Home-Consulting/Dependiject/blob/master/Dependiject/Store.swift +// Copyright (c) 2022 Tiny Home Consulting LLC. All rights reserved. +// +// Combined together by Brian Dashore +// +// TODO: Replace with Observable when minVersion >= iOS 17 +// + +import SwiftUI +import Combine + +class ErasedObservableObject: ObservableObject { + let objectWillChange: AnyPublisher + + init(objectWillChange: AnyPublisher) { + self.objectWillChange = objectWillChange + } + + static func empty() -> ErasedObservableObject { + .init(objectWillChange: Empty().eraseToAnyPublisher()) + } +} + +public protocol AnyObservableObject: AnyObject { + var objectWillChange: ObservableObjectPublisher { get } +} + +// The generic type names were chosen to match the SwiftUI equivalents: +// - ObjectType from StateObject and ObservedObject +// - Subject from ObservedObject.Wrapper.subscript(dynamicMember:) +// - S from Publisher.receive(on:options:) + +/// A property wrapper used to wrap injected observable objects. +/// +/// This is similar to SwiftUI's +/// [`StateObject`](https://developer.apple.com/documentation/swiftui/stateobject), but without +/// compile-time type restrictions. The lack of compile-time restrictions means that `ObjectType` +/// may be a protocol rather than a class. +/// +/// - Important: At runtime, the wrapped value must conform to ``AnyObservableObject``. +/// +/// To pass properties of the observable object down the view hierarchy as bindings, use the +/// projected value: +/// ```swift +/// struct ExampleView: View { +/// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self) +/// +/// var body: some View { +/// TextField("username", text: $viewModel.username) +/// } +/// } +/// ``` +/// Not all injected objects need this property wrapper. See the example projects for examples each +/// way. +@propertyWrapper +public struct Store { + /// The underlying object being stored. + public let wrappedValue: ObjectType + + // See https://github.com/Tiny-Home-Consulting/Dependiject/issues/38 + fileprivate var _observableObject: ObservedObject + + @MainActor internal var observableObject: ErasedObservableObject { + return _observableObject.wrappedValue + } + + /// A projected value which has the same properties as the wrapped value, but presented as + /// bindings. + /// + /// Use this to pass bindings down the view hierarchy: + /// ```swift + /// struct ExampleView: View { + /// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self) + /// + /// var body: some View { + /// TextField("username", text: $viewModel.username) + /// } + /// } + /// ``` + public var projectedValue: Wrapper { + return Wrapper(self) + } + + /// Create a stored value on a custom scheduler. + /// + /// Use this init to schedule updates on a specific scheduler other than `DispatchQueue.main`. + public init( + wrappedValue: ObjectType, + on scheduler: S, + schedulerOptions: S.SchedulerOptions? = nil + ) { + self.wrappedValue = wrappedValue + + if let observable = wrappedValue as? AnyObservableObject { + let objectWillChange = observable.objectWillChange + .receive(on: scheduler, options: schedulerOptions) + .eraseToAnyPublisher() + self._observableObject = .init(initialValue: .init(objectWillChange: objectWillChange)) + } else { + assertionFailure( + "Only use the Store property wrapper with objects conforming to AnyObservableObject." + ) + self._observableObject = .init(initialValue: .empty()) + } + } + + /// Create a stored value which publishes on the main thread. + /// + /// To control when updates are published, see ``init(wrappedValue:on:schedulerOptions:)``. + public init(wrappedValue: ObjectType) { + self.init(wrappedValue: wrappedValue, on: DispatchQueue.main) + } + + /// An equivalent to SwiftUI's + /// [`ObservedObject.Wrapper`](https://developer.apple.com/documentation/swiftui/observedobject/wrapper) + /// type. + @dynamicMemberLookup + public struct Wrapper { + private var store: Store + + internal init(_ store: Store) { + self.store = store + } + + /// Returns a binding to the resulting value of a given key path. + public subscript( + dynamicMember keyPath: ReferenceWritableKeyPath + ) -> Binding { + return Binding { + self.store.wrappedValue[keyPath: keyPath] + } set: { + self.store.wrappedValue[keyPath: keyPath] = $0 + } + } + } +} + +extension Store: DynamicProperty { + public nonisolated mutating func update() { + _observableObject.update() + } +} diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 6e30bcf..677125f 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -16,7 +16,7 @@ public class DebridManager: ObservableObject { @Published var allDebrid: AllDebrid = .init() @Published var premiumize: Premiumize = .init() - lazy var debridSources: [any DebridSource] = [realDebrid, allDebrid, premiumize] + lazy var debridSources: [DebridSource] = [realDebrid, allDebrid, premiumize] // UI Variables @Published var showWebView: Bool = false @@ -26,6 +26,12 @@ public class DebridManager: ObservableObject { debridSources.contains { $0.isLoggedIn } } + @Published var selectedDebridId: DebridInfo? + + func debridSourceFromName(_ name: String? = nil) -> DebridSource? { + debridSources.first { $0.id.name == name ?? selectedDebridId?.name } + } + // Service agnostic variables @Published var enabledDebrids: Set = [] { didSet { @@ -106,12 +112,16 @@ public class DebridManager: ObservableObject { // If a UserDefaults integer isn't set, it's usually 0 let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService") - selectedDebridType = DebridType(rawValue: rawPreferredService) + let legacyPreferredService = DebridType(rawValue: rawPreferredService) + let preferredDebridSource = self.debridSourceFromName(legacyPreferredService?.toString()) + selectedDebridId = preferredDebridSource?.id // If a user has one logged in service, automatically set the preferred service to that one + /* if enabledDebrids.count == 1 { selectedDebridType = enabledDebrids.first } + */ } // TODO: Remove this after v0.6.0 @@ -255,38 +265,13 @@ public class DebridManager: ObservableObject { return .none } - switch selectedDebridType { - case .realDebrid: - guard let realDebridMatch = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { - return .none - } + let selectedSource = debridSourceFromName() - if realDebridMatch.files.count > 1 { - return .partial - } else { - return .full - } - case .allDebrid: - guard let allDebridMatch = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { - return .none - } - - if allDebridMatch.files.count > 1 { - return .partial - } else { - return .full - } - case .premiumize: - guard let premiumizeMatch = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { - return .none - } - - if premiumizeMatch.files.count > 1 { - return .partial - } else { - return .full - } - case .none: + if let selectedSource, + let match = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) + { + return match.files.count > 1 ? .partial : .full + } else { return .none } } @@ -297,8 +282,8 @@ public class DebridManager: ObservableObject { return false } - switch selectedDebridType { - case .realDebrid: + switch selectedDebridId?.name { + case .some("RealDebrid"): if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedRealDebridItem = realDebridItem return true @@ -306,7 +291,7 @@ public class DebridManager: ObservableObject { logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)") return false } - case .allDebrid: + case .some("AllDebrid"): if let allDebridItem = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedAllDebridItem = allDebridItem return true @@ -314,7 +299,7 @@ public class DebridManager: ObservableObject { logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)") return false } - case .premiumize: + case .some("Premiumize"): if let premiumizeItem = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedPremiumizeItem = premiumizeItem return true @@ -322,7 +307,7 @@ public class DebridManager: ObservableObject { logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)") return false } - case .none: + default: return false } } diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 13c65a0..afdde3d 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -114,7 +114,7 @@ class ScrapingViewModel: ObservableObject { var failedSourceNames: [String] = [] for await (requestResult, sourceName) in group { if let requestResult { - if await !debridManager.hasEnabledDebrids { + if await debridManager.hasEnabledDebrids { await debridManager.populateDebridIA(requestResult.magnets) } diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift index e3fd2b0..ce47955 100644 --- a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift +++ b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift @@ -8,38 +8,40 @@ import SwiftUI struct DebridLabelView: View { - @EnvironmentObject var debridManager: DebridManager + @Store var debridSource: DebridSource @State var cloudLinks: [String] = [] + @State var tagColor: Color = .red var magnet: Magnet? var body: some View { - if let selectedDebridType = debridManager.selectedDebridType { - Tag( - name: selectedDebridType.toString(abbreviated: true), - color: getTagColor(), - horizontalPadding: 5, - verticalPadding: 3 - ) + Tag( + name: debridSource.id.abbreviation, + color: tagColor, + horizontalPadding: 5, + verticalPadding: 3 + ) + .onAppear { + tagColor = getTagColor() + } + .onChange(of: debridSource.IAValues) { _ in + tagColor = getTagColor() } } func getTagColor() -> Color { if let magnet, cloudLinks.isEmpty { - switch debridManager.matchMagnetHash(magnet) { - case .full: - return Color.green - case .partial: - return Color.orange - case .none: - return Color.red + guard let match = debridSource.IAValues.first(where: { magnet.hash == $0.magnet.hash }) else { + return .red } + + return match.files.count > 1 ? .orange : .green } else if cloudLinks.count == 1 { - return Color.green + return .green } else if cloudLinks.count > 1 { - return Color.orange + return .orange } else { - return Color.red + return .red } } } diff --git a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift index 9a98c72..1ecb106 100644 --- a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift +++ b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift @@ -15,23 +15,23 @@ struct SelectedDebridFilterView: View { var body: some View { Menu { Button { - debridManager.selectedDebridType = nil + debridManager.selectedDebridId = nil } label: { Text("None") - if debridManager.selectedDebridType == nil { + if debridManager.selectedDebridId == nil { Image(systemName: "checkmark") } } - ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in - if debridManager.enabledDebrids.contains(debridType) { + ForEach(debridManager.debridSources, id: \.id) { debridSource in + if debridSource.isLoggedIn { Button { - debridManager.selectedDebridType = debridType + debridManager.selectedDebridId = debridSource.id } label: { - Text(debridType.toString()) + Text(debridSource.id.name) - if debridManager.selectedDebridType == debridType { + if debridManager.selectedDebridId == debridSource.id { Image(systemName: "checkmark") } } @@ -40,6 +40,6 @@ struct SelectedDebridFilterView: View { } label: { label } - .id(debridManager.selectedDebridType) + .id(debridManager.selectedDebridId) } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index 0f342b5..ee72b05 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -102,7 +102,7 @@ struct AllDebridCloudView: View { HStack { Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() - DebridLabelView(cloudLinks: cloudTorrent.links) + //DebridLabelView(cloudLinks: cloudTorrent.links) } .font(.caption) } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index af74379..b05e764 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -103,7 +103,7 @@ struct RealDebridCloudView: View { HStack { Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() - DebridLabelView(cloudLinks: cloudTorrent.links) + //DebridLabelView(cloudLinks: cloudTorrent.links) } .font(.caption) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift index 8797a1a..b66fe14 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift @@ -53,7 +53,7 @@ struct SearchFilterHeaderView: View { SelectedDebridFilterView { FilterLabelView( - name: debridManager.selectedDebridType?.toString(), + name: debridManager.selectedDebridId?.name, fallbackName: "Debrid" ) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift index d42371d..547e8a3 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift @@ -30,7 +30,9 @@ struct SearchResultInfoView: View { Text(size) } - DebridLabelView(magnet: result.magnet) + if let debridSource = debridManager.debridSourceFromName() { + DebridLabelView(debridSource: debridSource, magnet: result.magnet) + } } .font(.caption) } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift index cebd792..8d13af5 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift @@ -10,7 +10,7 @@ import SwiftUI struct SettingsDebridInfoView: View { @EnvironmentObject var debridManager: DebridManager - let debridType: DebridType + @Store var debridSource: DebridSource @State private var apiKeyTempText: String = "" @@ -18,9 +18,9 @@ struct SettingsDebridInfoView: View { List { Section(header: InlineHeader("Description")) { VStack(alignment: .leading, spacing: 10) { - Text("\(debridType.toString()) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.") + Text("\(debridSource.id.name) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.") - Link("Website", destination: URL(string: debridType.website()) ?? URL(string: "https://kingbri.dev/ferrite")!) + Link("Website", destination: URL(string: debridSource.id.website) ?? URL(string: "https://kingbri.dev/ferrite")!) } } @@ -30,21 +30,21 @@ struct SettingsDebridInfoView: View { ) { Button { Task { - if debridManager.enabledDebrids.contains(debridType) { - await debridManager.logoutDebrid(debridType: debridType) - } else if !debridManager.authProcessing(debridType) { - await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) + if debridSource.isLoggedIn { + //await debridManager.logoutDebrid(debridType: debridType) + } else if !debridSource.authProcessing { + //await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) } - apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" } } label: { Text( - debridManager.enabledDebrids.contains(debridType) + debridSource.isLoggedIn ? "Logout" - : (debridManager.authProcessing(debridType) ? "Processing" : "Login") + : (debridSource.authProcessing ? "Processing" : "Login") ) - .foregroundColor(debridManager.enabledDebrids.contains(debridType) ? .red : .blue) + .foregroundColor(debridSource.isLoggedIn ? .red : .blue) } } @@ -57,22 +57,22 @@ struct SettingsDebridInfoView: View { onCommit: { Task { if !apiKeyTempText.isEmpty { - await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) - apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + //await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) + //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" } } } ) - .fieldDisabled(debridManager.enabledDebrids.contains(debridType)) + .fieldDisabled(debridSource.isLoggedIn) } .onAppear { Task { - apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" } } } .listStyle(.insetGrouped) - .navigationTitle(debridType.toString()) + .navigationTitle(debridSource.id.name) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index fd5a87a..35d5f51 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -53,7 +53,7 @@ struct LibraryView: View { EmptyInstructionView(title: "No History", message: "Start watching to build history") } case .debridCloud: - if debridManager.selectedDebridType == nil { + if debridManager.selectedDebridId == nil { EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") } } @@ -69,7 +69,7 @@ struct LibraryView: View { switch navModel.libraryPickerSelection { case .bookmarks, .debridCloud: SelectedDebridFilterView { - Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid") + Text(debridManager.selectedDebridId?.abbreviation ?? "Debrid") } .transaction { $0.animation = .none diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 8914583..fc23cf4 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -46,14 +46,14 @@ struct SettingsView: View { NavView { Form { Section(header: InlineHeader("Debrid services")) { - ForEach(DebridType.allCases, id: \.self) { debridType in + ForEach(debridManager.debridSources, id: \.id) { (debridSource: DebridSource) in NavigationLink { - SettingsDebridInfoView(debridType: debridType) + SettingsDebridInfoView(debridSource: debridSource) } label: { HStack { - Text(debridType.toString()) + Text(debridSource.id.name) Spacer() - Text(debridManager.enabledDebrids.contains(debridType) ? "Enabled" : "Disabled") + Text(debridSource.isLoggedIn ? "Enabled" : "Disabled") .foregroundColor(.secondary) } } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 7d2d3f4..13c402b 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -23,8 +23,8 @@ struct BatchChoiceView: View { var body: some View { NavView { List { - switch debridManager.selectedDebridType { - case .realDebrid: + switch debridManager.selectedDebridId?.name { + case .some("RealDebrid"): ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { Button(file.name) { @@ -34,7 +34,7 @@ struct BatchChoiceView: View { } } } - case .allDebrid: + case .some("AllDebrid"): ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { Button(file.name) { @@ -44,7 +44,7 @@ struct BatchChoiceView: View { } } } - case .premiumize: + case .some("Premiumize"): ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { Button(file.name) { @@ -54,7 +54,7 @@ struct BatchChoiceView: View { } } } - case .none: + default: EmptyView() } } -- 2.45.2 From 4e6cfee608bdd139c9fa4b2010ce4f3d2c41a6a8 Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 5 Jun 2024 13:05:26 -0400 Subject: [PATCH 14/27] Debrid: Remove ID storage Storing an ID reference is redundant. Store a class reference instead. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 12 ++++++------ Ferrite/API/PremiumizeWrapper.swift | 11 +++++------ Ferrite/API/RealDebridWrapper.swift | 14 +++++++------- Ferrite/Models/DebridModels.swift | 6 ------ Ferrite/Protocols/Debrid.swift | 5 ++++- Ferrite/ViewModels/DebridManager.swift | 15 +++++++-------- .../ComponentViews/Debrid/DebridLabelView.swift | 2 +- .../Filters/SelectedDebridFilterView.swift | 12 ++++++------ .../SearchResult/SearchFilterHeaderView.swift | 2 +- .../SearchResult/SearchResultInfoView.swift | 2 +- .../Settings/SettingsDebridInfoView.swift | 6 +++--- Ferrite/Views/LibraryView.swift | 4 ++-- Ferrite/Views/SettingsView.swift | 2 +- Ferrite/Views/SheetViews/BatchChoiceView.swift | 2 +- 14 files changed, 45 insertions(+), 50 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 0be235f..eaa1269 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -9,9 +9,9 @@ import Foundation // TODO: Fix errors public class AllDebrid: PollingDebridSource, ObservableObject { - public let id = DebridInfo( - name: "AllDebrid", abbreviation: "AD", website: "https://alldebrid.com" - ) + public let id = "AllDebrid" + public let abbreviation = "AD" + public let website = "https://alldebrid.com" public var authTask: Task? public var authProcessing: Bool = false @@ -178,7 +178,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { return DebridIA( magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), - source: self.id.name, + source: self.id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -292,7 +292,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { cloudTorrents = rawResponse.magnets.map { magnetResponse in DebridCloudTorrent( torrentId: String(magnetResponse.id), - source: self.id.name, + source: self.id, fileName: magnetResponse.filename, status: magnetResponse.status, hash: magnetResponse.hash, @@ -325,7 +325,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { // The link is also the ID cloudDownloads = rawResponse.links.map { link in DebridCloudDownload( - downloadId: link.link, source: self.id.name, fileName: link.filename, link: link.link + downloadId: link.link, source: self.id, fileName: link.filename, link: link.link ) } diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 939386f..2d26a20 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -8,10 +8,9 @@ import Foundation public class Premiumize: OAuthDebridSource, ObservableObject { - public let id = DebridInfo( - name: "Premiumize", abbreviation: "PM", website: "https://premiumize.me" - ) - + public let id = "Premiumize" + public let abbreviation = "PM" + public let website = "https://premiumize.me" @Published public var authProcessing: Bool = false public var isLoggedIn: Bool { getToken() != nil @@ -195,7 +194,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { return DebridIA( magnet: magnet, - source: id.name, + source: id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -300,7 +299,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { // The "link" is the ID for Premiumize cloudDownloads = rawResponse.files.map { file in - DebridCloudDownload(downloadId: file.id, source: self.id.name, fileName: file.name, link: file.id) + DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id) } return cloudDownloads diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 7204cee..85a1fcb 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -8,9 +8,9 @@ import Foundation public class RealDebrid: PollingDebridSource, ObservableObject { - public let id = DebridInfo( - name: "RealDebrid", abbreviation: "RD", website: "https://real-debrid.com" - ) + public let id = "RealDebrid" + public let abbreviation = "RD" + public let website = "https://real-debrid.com" public var authTask: Task? @Published public var authProcessing: Bool = false @@ -287,7 +287,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: id.name, + source: id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -296,7 +296,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: id.name, + source: id, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: [] ) @@ -427,7 +427,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { cloudTorrents = rawResponse.map { response in DebridCloudTorrent( torrentId: response.id, - source: self.id.name, + source: self.id, fileName: response.filename, status: response.status, hash: response.hash, @@ -453,7 +453,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data) cloudDownloads = rawResponse.map { response in - DebridCloudDownload(downloadId: response.id, source: self.id.name, fileName: response.filename, link: response.download) + DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download) } return cloudDownloads diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 0a4316d..3eb4e5f 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -7,12 +7,6 @@ import Foundation -public struct DebridInfo: Hashable, Sendable { - let name: String - let abbreviation: String - let website: String -} - public struct DebridIA: Hashable, Sendable { let magnet: Magnet let source: String diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 3a842cb..fb7ba3d 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -9,7 +9,10 @@ import Foundation public protocol DebridSource: AnyObservableObject { // ID of the service - var id: DebridInfo { get } + //var id: DebridInfo { get } + var id: String { get } + var abbreviation: String { get } + var website: String { get } // Auth variables var authProcessing: Bool { get set } diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 677125f..6a712bb 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -26,11 +26,13 @@ public class DebridManager: ObservableObject { debridSources.contains { $0.isLoggedIn } } - @Published var selectedDebridId: DebridInfo? + @Published var selectedDebridSource: DebridSource? + /* func debridSourceFromName(_ name: String? = nil) -> DebridSource? { debridSources.first { $0.id.name == name ?? selectedDebridId?.name } } + */ // Service agnostic variables @Published var enabledDebrids: Set = [] { @@ -113,8 +115,7 @@ public class DebridManager: ObservableObject { // If a UserDefaults integer isn't set, it's usually 0 let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService") let legacyPreferredService = DebridType(rawValue: rawPreferredService) - let preferredDebridSource = self.debridSourceFromName(legacyPreferredService?.toString()) - selectedDebridId = preferredDebridSource?.id + selectedDebridSource = self.debridSources.first { $0.id == legacyPreferredService?.toString() } // If a user has one logged in service, automatically set the preferred service to that one /* @@ -265,10 +266,8 @@ public class DebridManager: ObservableObject { return .none } - let selectedSource = debridSourceFromName() - - if let selectedSource, - let match = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) + if let selectedDebridSource, + let match = selectedDebridSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) { return match.files.count > 1 ? .partial : .full } else { @@ -282,7 +281,7 @@ public class DebridManager: ObservableObject { return false } - switch selectedDebridId?.name { + switch selectedDebridSource?.id { case .some("RealDebrid"): if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedRealDebridItem = realDebridItem diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift index ce47955..84e2d5e 100644 --- a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift +++ b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift @@ -16,7 +16,7 @@ struct DebridLabelView: View { var body: some View { Tag( - name: debridSource.id.abbreviation, + name: debridSource.abbreviation, color: tagColor, horizontalPadding: 5, verticalPadding: 3 diff --git a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift index 1ecb106..c6bc9c3 100644 --- a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift +++ b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift @@ -15,11 +15,11 @@ struct SelectedDebridFilterView: View { var body: some View { Menu { Button { - debridManager.selectedDebridId = nil + debridManager.selectedDebridSource = nil } label: { Text("None") - if debridManager.selectedDebridId == nil { + if debridManager.selectedDebridSource == nil { Image(systemName: "checkmark") } } @@ -27,11 +27,11 @@ struct SelectedDebridFilterView: View { ForEach(debridManager.debridSources, id: \.id) { debridSource in if debridSource.isLoggedIn { Button { - debridManager.selectedDebridId = debridSource.id + debridManager.selectedDebridSource = debridSource } label: { - Text(debridSource.id.name) + Text(debridSource.id) - if debridManager.selectedDebridId == debridSource.id { + if debridManager.selectedDebridSource?.id == debridSource.id { Image(systemName: "checkmark") } } @@ -40,6 +40,6 @@ struct SelectedDebridFilterView: View { } label: { label } - .id(debridManager.selectedDebridId) + .id(debridManager.selectedDebridSource?.id) } } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift index b66fe14..1ba7df0 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift @@ -53,7 +53,7 @@ struct SearchFilterHeaderView: View { SelectedDebridFilterView { FilterLabelView( - name: debridManager.selectedDebridId?.name, + name: debridManager.selectedDebridSource?.id, fallbackName: "Debrid" ) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift index 547e8a3..4bb2528 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift @@ -30,7 +30,7 @@ struct SearchResultInfoView: View { Text(size) } - if let debridSource = debridManager.debridSourceFromName() { + if let debridSource = debridManager.selectedDebridSource { DebridLabelView(debridSource: debridSource, magnet: result.magnet) } } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift index 8d13af5..6207b87 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift @@ -18,9 +18,9 @@ struct SettingsDebridInfoView: View { List { Section(header: InlineHeader("Description")) { VStack(alignment: .leading, spacing: 10) { - Text("\(debridSource.id.name) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.") + Text("\(debridSource.id) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.") - Link("Website", destination: URL(string: debridSource.id.website) ?? URL(string: "https://kingbri.dev/ferrite")!) + Link("Website", destination: URL(string: debridSource.website) ?? URL(string: "https://kingbri.dev/ferrite")!) } } @@ -72,7 +72,7 @@ struct SettingsDebridInfoView: View { } } .listStyle(.insetGrouped) - .navigationTitle(debridSource.id.name) + .navigationTitle(debridSource.id) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 35d5f51..ac1260f 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -53,7 +53,7 @@ struct LibraryView: View { EmptyInstructionView(title: "No History", message: "Start watching to build history") } case .debridCloud: - if debridManager.selectedDebridId == nil { + if debridManager.selectedDebridSource == nil { EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") } } @@ -69,7 +69,7 @@ struct LibraryView: View { switch navModel.libraryPickerSelection { case .bookmarks, .debridCloud: SelectedDebridFilterView { - Text(debridManager.selectedDebridId?.abbreviation ?? "Debrid") + Text(debridManager.selectedDebridSource?.abbreviation ?? "Debrid") } .transaction { $0.animation = .none diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index fc23cf4..4171c96 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -51,7 +51,7 @@ struct SettingsView: View { SettingsDebridInfoView(debridSource: debridSource) } label: { HStack { - Text(debridSource.id.name) + Text(debridSource.id) Spacer() Text(debridSource.isLoggedIn ? "Enabled" : "Disabled") .foregroundColor(.secondary) diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 13c402b..be0f38d 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -23,7 +23,7 @@ struct BatchChoiceView: View { var body: some View { NavView { List { - switch debridManager.selectedDebridId?.name { + switch debridManager.selectedDebridSource?.id { case .some("RealDebrid"): ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { -- 2.45.2 From 13988b3c6cbb7f623fb9c982c82d2d34bd8469b7 Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 5 Jun 2024 22:16:40 -0400 Subject: [PATCH 15/27] Debrid: Refactor IA and download functions Use the common protocol to handle these. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 28 ++- Ferrite/API/PremiumizeWrapper.swift | 28 ++- Ferrite/API/RealDebridWrapper.swift | 29 ++- Ferrite/Protocols/Debrid.swift | 1 + Ferrite/ViewModels/DebridManager.swift | 194 +++++------------- .../Views/SheetViews/BatchChoiceView.swift | 35 +--- 6 files changed, 126 insertions(+), 189 deletions(-) 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) -- 2.45.2 From 3cb8a979b1dd42d583f3b454e2515b7fea900469 Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 5 Jun 2024 22:50:40 -0400 Subject: [PATCH 16/27] Debrid: Swap to common DebridError Removes the redundant error types. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 30 +++++++++++------------ Ferrite/API/PremiumizeWrapper.swift | 32 ++++++++++++------------ Ferrite/API/RealDebridWrapper.swift | 34 +++++++++++++------------- Ferrite/Models/AllDebridModels.swift | 14 ----------- Ferrite/Models/DebridModels.swift | 11 +++++++++ Ferrite/Models/PremiumizeModels.swift | 14 ----------- Ferrite/Models/RealDebridModels.swift | 14 ----------- Ferrite/ViewModels/DebridManager.swift | 20 +++++++-------- 8 files changed, 69 insertions(+), 100 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 5b44fac..cf533f9 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -42,7 +42,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { // Validate the URL before doing anything else let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data guard let userUrl = URL(string: rawResponse.userURL) else { - throw ADError.AuthQuery(description: "The login URL is invalid") + throw DebridError.AuthQuery(description: "The login URL is invalid") } // Spawn the polling task separately @@ -53,7 +53,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { return userUrl } catch { print("Couldn't get pin information!") - throw ADError.AuthQuery(description: error.localizedDescription) + throw DebridError.AuthQuery(description: error.localizedDescription) } } @@ -73,7 +73,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { while count < 12 { if Task.isCancelled { - throw ADError.AuthQuery(description: "Token request cancelled.") + throw DebridError.AuthQuery(description: "Token request cancelled.") } let (data, _) = try await URLSession.shared.data(for: request) @@ -92,7 +92,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { } } - throw ADError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") + throw DebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") } if case let .failure(error) = await authTask?.result { @@ -123,7 +123,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { // 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 = getToken() else { - throw ADError.InvalidToken + throw DebridError.InvalidToken } request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") @@ -131,23 +131,23 @@ public class AllDebrid: PollingDebridSource, ObservableObject { let (data, response) = try await URLSession.shared.data(for: request) guard let response = response as? HTTPURLResponse else { - throw ADError.FailedRequest(description: "No HTTP response given") + throw DebridError.FailedRequest(description: "No HTTP response given") } if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { logout() - throw ADError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.") + throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.") } else { - throw ADError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") + throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") } } // Builds a URL for further requests private func buildRequestURL(urlString: String, queryItems: [URLQueryItem] = []) throws -> URL { guard var components = URLComponents(string: urlString) else { - throw ADError.InvalidUrl + throw DebridError.InvalidUrl } components.queryItems = [ @@ -157,7 +157,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { if let url = components.url { return url } else { - throw ADError.InvalidUrl + throw DebridError.InvalidUrl } } @@ -234,7 +234,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { // Adds a magnet link to the user's AD account public func addMagnet(magnet: Magnet) async throws -> Int { guard let magnetLink = magnet.link else { - throw ADError.FailedRequest(description: "The magnet link is invalid") + throw DebridError.FailedRequest(description: "The magnet link is invalid") } var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload")) @@ -254,7 +254,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { if let magnet = rawResponse.magnets[safe: 0] { return magnet.id } else { - throw ADError.InvalidResponse + throw DebridError.InvalidResponse } } @@ -271,7 +271,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { if let linkWrapper = rawResponse.magnets[safe: 0]?.links[safe: selectedIndex ?? -1] { return linkWrapper.link } else { - throw ADError.EmptyTorrents + throw DebridError.EmptyTorrents } } @@ -306,7 +306,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data if rawResponse.magnets.isEmpty { - throw ADError.EmptyData + throw DebridError.EmptyData } cloudTorrents = rawResponse.magnets.map { magnetResponse in @@ -339,7 +339,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data if rawResponse.links.isEmpty { - throw ADError.EmptyData + throw DebridError.EmptyData } // The link is also the ID diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 1530ba8..fbbf6aa 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -40,7 +40,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { if let url = urlComponents.url { return url } else { - throw PMError.InvalidUrl + throw DebridError.InvalidUrl } } @@ -48,14 +48,14 @@ public class Premiumize: OAuthDebridSource, ObservableObject { let callbackComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) guard let callbackFragment = callbackComponents?.fragment else { - throw PMError.InvalidResponse + throw DebridError.InvalidResponse } var fragmentComponents = URLComponents() fragmentComponents.query = callbackFragment guard let accessToken = fragmentComponents.queryItems?.first(where: { $0.name == "access_token" })?.value else { - throw PMError.InvalidToken + throw DebridError.InvalidToken } FerriteKeychain.shared.set(accessToken, forKey: "Premiumize.AccessToken") @@ -84,7 +84,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { // 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 = getToken() else { - throw PMError.InvalidToken + throw DebridError.InvalidToken } // Use the API query parameter if a manual API key is present @@ -93,7 +93,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { let requestUrl = request.url, var components = URLComponents(url: requestUrl, resolvingAgainstBaseURL: false) else { - throw PMError.InvalidUrl + throw DebridError.InvalidUrl } let apiTokenItem = URLQueryItem(name: "apikey", value: token) @@ -112,16 +112,16 @@ public class Premiumize: OAuthDebridSource, ObservableObject { let (data, response) = try await URLSession.shared.data(for: request) guard let response = response as? HTTPURLResponse else { - throw PMError.FailedRequest(description: "No HTTP response given") + throw DebridError.FailedRequest(description: "No HTTP response given") } if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { logout() - throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.") + throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.") } else { - throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") + throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") } } @@ -184,7 +184,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { // Grabs DDL links func fetchDDL(magnet: Magnet) async throws -> DebridIA { if magnet.hash == nil { - throw PMError.EmptyData + throw DebridError.EmptyData } var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/directdl")!) @@ -215,7 +215,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { files: files ) } else { - throw PMError.EmptyData + throw DebridError.EmptyData } } @@ -245,7 +245,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { var urlComponents = URLComponents(string: "\(baseApiUrl)/cache/check")! urlComponents.queryItems = magnets.map { URLQueryItem(name: "items[]", value: $0.hash) } guard let url = urlComponents.url else { - throw PMError.InvalidUrl + throw DebridError.InvalidUrl } var request = URLRequest(url: url) @@ -254,7 +254,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data) if rawResponse.response.isEmpty { - throw PMError.EmptyData + throw DebridError.EmptyData } else { let availableMagnets = magnets.enumerated().compactMap { index, magnet in if rawResponse.response[safe: index] == true { @@ -280,13 +280,13 @@ public class Premiumize: OAuthDebridSource, ObservableObject { } else if let premiumizeItem = ia, let firstFile = premiumizeItem.files[safe: 0], let streamUrlString = firstFile.streamUrlString { return streamUrlString } else { - throw PMError.FailedRequest(description: "Could not fetch your file from the Premiumize API") + throw DebridError.FailedRequest(description: "Could not fetch your file from the Premiumize API") } } func createTransfer(magnet: Magnet) async throws { guard let magnetLink = magnet.link else { - throw PMError.FailedRequest(description: "The magnet link is invalid") + throw DebridError.FailedRequest(description: "The magnet link is invalid") } var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/create")!) @@ -310,7 +310,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { let rawResponse = try jsonDecoder.decode(AllItemsResponse.self, from: data) if rawResponse.files.isEmpty { - throw PMError.EmptyData + throw DebridError.EmptyData } // The "link" is the ID for Premiumize @@ -325,7 +325,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { var urlComponents = URLComponents(string: "\(baseApiUrl)/item/details")! urlComponents.queryItems = [URLQueryItem(name: "id", value: itemID)] guard let url = urlComponents.url else { - throw PMError.InvalidUrl + throw DebridError.InvalidUrl } var request = URLRequest(url: url) diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index f306a15..798bb68 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -7,7 +7,7 @@ import Foundation -public class RealDebrid: PollingDebridSource, ObservableObject { +public class RealDebrid: PollingDebridSource, ObservableObject { public let id = "RealDebrid" public let abbreviation = "RD" public let website = "https://real-debrid.com" @@ -52,7 +52,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { ] guard let url = urlComponents.url else { - throw RDError.InvalidUrl + throw DebridError.InvalidUrl } let request = URLRequest(url: url) @@ -62,7 +62,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { // Validate the URL before doing anything else let rawResponse = try jsonDecoder.decode(DeviceCodeResponse.self, from: data) guard let directVerificationUrl = URL(string: rawResponse.directVerificationURL) else { - throw RDError.AuthQuery(description: "The verification URL is invalid") + throw DebridError.AuthQuery(description: "The verification URL is invalid") } // Spawn the polling task separately @@ -73,7 +73,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { return directVerificationUrl } catch { print("Couldn't get the new client creds!") - throw RDError.AuthQuery(description: error.localizedDescription) + throw DebridError.AuthQuery(description: error.localizedDescription) } } @@ -86,7 +86,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { ] guard let url = urlComponents.url else { - throw RDError.InvalidUrl + throw DebridError.InvalidUrl } let request = URLRequest(url: url) @@ -96,7 +96,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { while count < 12 { if Task.isCancelled { - throw RDError.AuthQuery(description: "Token request cancelled.") + throw DebridError.AuthQuery(description: "Token request cancelled.") } let (data, _) = try await URLSession.shared.data(for: request) @@ -118,17 +118,17 @@ public class RealDebrid: PollingDebridSource, ObservableObject { } } - throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") + throw DebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") } // Fetch all tokens for the user and store in FerriteKeychain.shared public func getApiTokens(deviceCode: String) async throws { guard let clientId = UserDefaults.standard.string(forKey: "RealDebrid.ClientId") else { - throw RDError.EmptyData + throw DebridError.EmptyData } guard let clientSecret = FerriteKeychain.shared.get("RealDebrid.ClientSecret") else { - throw RDError.EmptyData + throw DebridError.EmptyData } var request = URLRequest(url: URL(string: "\(baseAuthUrl)/token")!) @@ -208,7 +208,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { // 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 getToken() else { - throw RDError.InvalidToken + throw DebridError.InvalidToken } request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") @@ -216,16 +216,16 @@ public class RealDebrid: PollingDebridSource, ObservableObject { let (data, response) = try await URLSession.shared.data(for: request) guard let response = response as? HTTPURLResponse else { - throw RDError.FailedRequest(description: "No HTTP response given") + throw DebridError.FailedRequest(description: "No HTTP response given") } if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { await logout() - throw RDError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.") + throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.") } else { - throw RDError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") + throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") } } @@ -345,7 +345,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { return downloadLink } catch { - if case RealDebrid.RDError.EmptyTorrents = error, !selectedMagnetId.isEmpty { + if case DebridError.EmptyTorrents = error, !selectedMagnetId.isEmpty { try? await deleteTorrent(torrentId: selectedMagnetId) } @@ -357,7 +357,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { // Adds a magnet link to the user's RD account public func addMagnet(magnet: Magnet) async throws -> String { guard let magnetLink = magnet.link else { - throw RDError.FailedRequest(description: "The magnet link is invalid") + throw DebridError.FailedRequest(description: "The magnet link is invalid") } var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/addMagnet")!) @@ -408,9 +408,9 @@ public class RealDebrid: PollingDebridSource, ObservableObject { if let torrentLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" { return torrentLink } else if rawResponse.status == "downloading" || rawResponse.status == "queued" { - throw RDError.EmptyTorrents + throw DebridError.EmptyTorrents } else { - throw RDError.EmptyData + throw DebridError.EmptyData } } diff --git a/Ferrite/Models/AllDebridModels.swift b/Ferrite/Models/AllDebridModels.swift index 02b07b1..3ec1d0e 100644 --- a/Ferrite/Models/AllDebridModels.swift +++ b/Ferrite/Models/AllDebridModels.swift @@ -8,20 +8,6 @@ import Foundation public extension AllDebrid { - // MARK: - Errors - - // TODO: Hybridize debrid errors in one structure - enum ADError: Error { - case InvalidUrl - case InvalidPostBody - case InvalidResponse - case InvalidToken - case EmptyData - case EmptyTorrents - case FailedRequest(description: String) - case AuthQuery(description: String) - } - // MARK: - Generic AllDebrid response // Uses a generic parametr for whatever underlying response is present diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 3eb4e5f..9bce883 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -43,3 +43,14 @@ public struct DebridCloudTorrent: Hashable, Sendable { let hash: String let links: [String] } + +public enum DebridError: Error { + case InvalidUrl + case InvalidPostBody + case InvalidResponse + case InvalidToken + case EmptyData + case EmptyTorrents + case FailedRequest(description: String) + case AuthQuery(description: String) +} diff --git a/Ferrite/Models/PremiumizeModels.swift b/Ferrite/Models/PremiumizeModels.swift index 4572526..6ec9606 100644 --- a/Ferrite/Models/PremiumizeModels.swift +++ b/Ferrite/Models/PremiumizeModels.swift @@ -8,20 +8,6 @@ import Foundation public extension Premiumize { - // MARK: - Errors - - // TODO: Hybridize debrid errors in one structure - enum PMError: Error { - case InvalidUrl - case InvalidPostBody - case InvalidResponse - case InvalidToken - case EmptyData - case EmptyTorrents - case FailedRequest(description: String) - case AuthQuery(description: String) - } - // MARK: - CacheCheckResponse struct CacheCheckResponse: Codable { diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index ba455b1..d72ad40 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -9,20 +9,6 @@ import Foundation public extension RealDebrid { - // MARK: - Errors - - // TODO: Hybridize debrid errors in one structure - enum RDError: Error { - case InvalidUrl - case InvalidPostBody - case InvalidResponse - case InvalidToken - case EmptyData - case EmptyTorrents - case FailedRequest(description: String) - case AuthQuery(description: String) - } - // MARK: - device code endpoint struct DeviceCodeResponse: Codable, Sendable { diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 0b714f0..b811d0f 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -342,7 +342,7 @@ public class DebridManager: ObservableObject { try await realDebrid.authTask?.value return true } else { - throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid") + throw DebridError.AuthQuery(description: "The verification URL was invalid") } } catch { await sendDebridError(error, prefix: "RealDebrid authentication error") @@ -361,7 +361,7 @@ public class DebridManager: ObservableObject { try await allDebrid.authTask?.value return true } else { - throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid") + throw DebridError.AuthQuery(description: "The PIN URL was invalid") } } catch { await sendDebridError(error, prefix: "AllDebrid authentication error") @@ -388,14 +388,14 @@ public class DebridManager: ObservableObject { public func handleCallback(url: URL?, error: Error?) async { do { if let error { - throw Premiumize.PMError.AuthQuery(description: "OAuth callback Error: \(error)") + throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)") } if let callbackUrl = url { try premiumize.handleAuthCallback(url: callbackUrl) completeDebridAuth(.premiumize, success: true) } else { - throw Premiumize.PMError.AuthQuery(description: "The callback URL was invalid") + throw DebridError.AuthQuery(description: "The callback URL was invalid") } } catch { await sendDebridError(error, prefix: "Premiumize authentication error (callback)") @@ -476,7 +476,7 @@ public class DebridManager: ObservableObject { // Update the UI downloadUrl = downloadLink } else { - throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") + throw DebridError.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 @@ -491,7 +491,7 @@ public class DebridManager: ObservableObject { 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") + throw DebridError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") } let downloadLink = try await realDebrid.getDownloadLink( @@ -505,7 +505,7 @@ public class DebridManager: ObservableObject { await fetchRdCloud(bypassTTL: true) } catch { switch error { - case RealDebrid.RDError.EmptyTorrents: + case DebridError.EmptyTorrents: showDeleteAlert.toggle() default: await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled") @@ -565,7 +565,7 @@ public class DebridManager: ObservableObject { await fetchRdCloud(bypassTTL: true) } else { - throw RealDebrid.RDError.FailedRequest(description: "No torrent ID was provided") + throw DebridError.FailedRequest(description: "No torrent ID was provided") } } catch { await sendDebridError(error, prefix: "RealDebrid torrent delete error", presentError: presentError) @@ -582,7 +582,7 @@ public class DebridManager: ObservableObject { // Update UI downloadUrl = downloadLink } else { - throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API") + throw DebridError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API") } // Fetch one more time to add updated data into the AD cloud cache @@ -641,7 +641,7 @@ public class DebridManager: ObservableObject { downloadUrl = downloadLink } else { - throw Premiumize.PMError.FailedRequest(description: "Could not fetch your file from Premiumize's cache or API") + throw DebridError.FailedRequest(description: "Could not fetch your file from Premiumize's cache or API") } // Fetch one more time to add updated data into the PM cloud cache -- 2.45.2 From 554b72857becc0187015b88fb55ae62e4dd6ad18 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 6 Jun 2024 10:44:44 -0400 Subject: [PATCH 17/27] Debrid: Fix cache alert Change the returned error to one that's unique to caching. Also make deleteTorrents optional to delete the first torrent if necessary since that's always being cached. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 6 ++++- Ferrite/API/PremiumizeWrapper.swift | 2 +- Ferrite/API/RealDebridWrapper.swift | 23 +++++++++++++++---- Ferrite/Models/DebridModels.swift | 1 + Ferrite/Protocols/Debrid.swift | 2 +- Ferrite/ViewModels/DebridManager.swift | 10 ++++++-- .../SearchResult/SearchResultButtonView.swift | 5 ++-- 7 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index cf533f9..7778aa2 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -323,7 +323,11 @@ public class AllDebrid: PollingDebridSource, ObservableObject { return cloudTorrents } - public func deleteTorrent(torrentId: String) async throws { + public func deleteTorrent(torrentId: String?) async throws { + guard let torrentId else { + throw DebridError.FailedRequest(description: "The torrentID \(String(describing: torrentId)) is invalid") + } + let queryItems = [ URLQueryItem(name: "id", value: torrentId) ] diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index fbbf6aa..468f406 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -359,5 +359,5 @@ public class Premiumize: OAuthDebridSource, ObservableObject { [] } - public func deleteTorrent(torrentId: String) async throws {} + public func deleteTorrent(torrentId: String?) async throws {} } diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 798bb68..daf42b0 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -408,9 +408,9 @@ public class RealDebrid: PollingDebridSource, ObservableObject { if let torrentLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" { return torrentLink } else if rawResponse.status == "downloading" || rawResponse.status == "queued" { - throw DebridError.EmptyTorrents + throw DebridError.IsCaching } else { - throw DebridError.EmptyData + throw DebridError.EmptyTorrents } } @@ -454,8 +454,23 @@ public class RealDebrid: PollingDebridSource, ObservableObject { } // Deletes a torrent download from RD - public func deleteTorrent(torrentId: String) async throws { - var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(torrentId)")!) + public func deleteTorrent(torrentId: String?) async throws { + let deleteId: String + + if let torrentId { + deleteId = torrentId + } else { + // Refresh the torrent cloud + // The first file is the currently caching one + let _ = try await getUserTorrents() + guard let firstTorrent = cloudTorrents[safe: -1] else { + throw DebridError.EmptyTorrents + } + + deleteId = firstTorrent.torrentId + } + + var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(deleteId)")!) request.httpMethod = "DELETE" try await performRequest(request: &request, requestName: #function) diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 9bce883..31ea356 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -51,6 +51,7 @@ public enum DebridError: Error { case InvalidToken case EmptyData case EmptyTorrents + case IsCaching case FailedRequest(description: String) case AuthQuery(description: String) } diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 2012492..3c0570c 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -45,7 +45,7 @@ public protocol DebridSource: AnyObservableObject { // User torrent functions func getUserTorrents() async throws -> [DebridCloudTorrent] - func deleteTorrent(torrentId: String) async throws + func deleteTorrent(torrentId: String?) async throws } public protocol PollingDebridSource: DebridSource { diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index b811d0f..06e50e8 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -483,8 +483,14 @@ public class DebridManager: ObservableObject { // TODO: Add common fetch cloud method //await fetchRdCloud(bypassTTL: true) } catch { - // TODO: Fix error types and unify errors - print("Error \(error)") + switch error { + case DebridError.IsCaching: + showDeleteAlert.toggle() + default: + await sendDebridError(error, prefix: "\(debridSource.id) download error", cancelString: "Download cancelled") + } + + logManager?.hideIndeterminateToast() } } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index efe438b..fa2d24e 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -127,13 +127,14 @@ struct SearchResultButtonView: View { .alert("Caching file", isPresented: $debridManager.showDeleteAlert) { Button("Yes", role: .destructive) { Task { - await debridManager.deleteRdTorrent() + try? await debridManager.selectedDebridSource?.deleteTorrent(torrentId: nil) } } Button("Cancel", role: .cancel) {} } message: { Text( - "RealDebrid is currently caching this file. Would you like to delete it? \n\n" + + "\(String(describing: debridManager.selectedDebridSource?.id)) is currently caching this file. " + + "Would you like to delete it? \n\n" + "Progress can be checked on the RealDebrid website." ) } -- 2.45.2 From 9fe9241ca3abf7344c2805567bc164fe816faccc Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 6 Jun 2024 10:46:18 -0400 Subject: [PATCH 18/27] Debrid: Remove redundant logout functions Logout is now handled in the debrid class itself. Signed-off-by: kingbri --- Ferrite/ViewModels/DebridManager.swift | 39 ------------------- .../Settings/SettingsDebridInfoView.swift | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 06e50e8..970431e 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -404,45 +404,6 @@ public class DebridManager: ObservableObject { } } - // MARK: - Logout UI linked functions - - // Common function to delegate what debrid service to logout of - public func logoutDebrid(debridType: DebridType) async { - switch debridType { - case .realDebrid: - await logoutRd() - case .allDebrid: - logoutAd() - case .premiumize: - logoutPm() - } - - // Automatically resets the preferred debrid service if it was set to the logged out service - if selectedDebridType == debridType { - selectedDebridType = nil - } - } - - private func logoutRd() async { - await realDebrid.logout() - enabledDebrids.remove(.realDebrid) - } - - private func logoutAd() { - allDebrid.logout() - enabledDebrids.remove(.allDebrid) - - logManager?.info( - "AllDebrid: Logged out, API key needs to be removed", - description: "Please manually delete the AllDebrid API key" - ) - } - - private func logoutPm() { - premiumize.logout() - enabledDebrids.remove(.premiumize) - } - // MARK: - Debrid fetch UI linked functions // Common function to delegate what debrid service to fetch from diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift index 6207b87..47d81c9 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift @@ -31,7 +31,7 @@ struct SettingsDebridInfoView: View { Button { Task { if debridSource.isLoggedIn { - //await debridManager.logoutDebrid(debridType: debridType) + await debridSource.logout() } else if !debridSource.authProcessing { //await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) } -- 2.45.2 From cb9231d3e7478a2b0264270557f7df0e4e116b55 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 6 Jun 2024 10:48:23 -0400 Subject: [PATCH 19/27] Debrid: Remove separated download functions No longer needed due to the common type. Signed-off-by: kingbri --- Ferrite/ViewModels/DebridManager.swift | 75 -------------------------- 1 file changed, 75 deletions(-) diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 970431e..df57145 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -455,37 +455,6 @@ public class DebridManager: ObservableObject { } } - func fetchRdDownload(magnet: Magnet?, cloudInfo: String?) async { - do { - guard let magnet else { - throw DebridError.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 { - switch error { - case DebridError.EmptyTorrents: - showDeleteAlert.toggle() - default: - await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled") - - if let torrentId = selectedRealDebridID { - try? await realDebrid.deleteTorrent(torrentId: torrentId) - } - } - - logManager?.hideIndeterminateToast() - } - } - public func fetchDebridCloud(bypassTTL: Bool = false) async { switch selectedDebridType { case .realDebrid: @@ -539,26 +508,6 @@ public class DebridManager: ObservableObject { } } - func fetchAdDownload(magnet: Magnet?, cloudInfo: String?) async { - do { - if let magnet { - let downloadLink = try await allDebrid.getDownloadLink( - magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile - ) - - // Update UI - downloadUrl = downloadLink - } else { - throw DebridError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API") - } - - // Fetch one more time to add updated data into the AD cloud cache - await fetchAdCloud(bypassTTL: true) - } catch { - await sendDebridError(error, prefix: "AllDebrid download error", cancelString: "Download cancelled") - } - } - // Refreshes torrents and downloads from a RD user's account public func fetchAdCloud(bypassTTL: Bool = false) async { if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL { @@ -594,30 +543,6 @@ public class DebridManager: ObservableObject { } } - func fetchPmDownload(magnet: Magnet?, cloudInfo: String? = nil) async { - do { - if let cloudInfo { - downloadUrl = try await premiumize.checkUserDownloads(link: cloudInfo) ?? "" - return - } - - if let magnet { - let downloadLink = try await premiumize.getDownloadLink( - magnet: magnet, ia: selectedPremiumizeItem, iaFile: selectedPremiumizeFile - ) - - downloadUrl = downloadLink - } else { - throw DebridError.FailedRequest(description: "Could not fetch your file from Premiumize's cache or API") - } - - // Fetch one more time to add updated data into the PM cloud cache - await fetchPmCloud(bypassTTL: true) - } catch { - await sendDebridError(error, prefix: "Premiumize download error", cancelString: "Download or transfer cancelled") - } - } - // Refreshes items and fetches from a PM user account public func fetchPmCloud(bypassTTL: Bool = false) async { if bypassTTL || Date().timeIntervalSince1970 > premiumizeCloudTTL { -- 2.45.2 From 01fce90d6fbf8e5260cdc7925c8034f5c9c406c9 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 6 Jun 2024 11:51:43 -0400 Subject: [PATCH 20/27] Debrid: Migrate preferred service setter PreferredService is now the debrid ID. Signed-off-by: kingbri --- Ferrite/ViewModels/DebridManager.swift | 63 ++++++++++++-------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index df57145..aaac8da 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -26,7 +26,11 @@ public class DebridManager: ObservableObject { debridSources.contains { $0.isLoggedIn } } - @Published var selectedDebridSource: DebridSource? + @Published var selectedDebridSource: DebridSource? { + didSet { + UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService") + } + } var selectedDebridItem: DebridIA? var selectedDebridFile: DebridIAFile? @@ -102,45 +106,36 @@ public class DebridManager: ObservableObject { var premiumizeCloudTTL: Double = 0.0 init() { - if let rawDebridList = UserDefaults.standard.string(forKey: "Debrid.EnabledArray"), - let serializedDebridList = Set(rawValue: rawDebridList) - { - enabledDebrids = serializedDebridList - } - // If a UserDefaults integer isn't set, it's usually 0 - let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService") - let legacyPreferredService = DebridType(rawValue: rawPreferredService) - selectedDebridSource = self.debridSources.first { $0.id == legacyPreferredService?.toString() } + // Set the preferred service. Contains migration logic for earlier versions + if let rawPreferredService = UserDefaults.standard.string(forKey: "Debrid.PreferredService") { + let debridServiceId: String? - // If a user has one logged in service, automatically set the preferred service to that one - /* - if enabledDebrids.count == 1 { - selectedDebridType = enabledDebrids.first + if let preferredServiceInt = Int(rawPreferredService) { + debridServiceId = migratePreferredService(preferredServiceInt) + } else { + print(rawPreferredService) + debridServiceId = rawPreferredService + } + + // Only set the debrid source if it's logged in + // Otherwise remove the key + let tempDebridSource = self.debridSources.first { $0.id == debridServiceId } + if (tempDebridSource?.isLoggedIn ?? false) { + selectedDebridSource = tempDebridSource + } else { + UserDefaults.standard.removeObject(forKey: "Debrid.PreferredService") + } } - */ } - // TODO: Remove this after v0.6.0 - // Login cleanup function that's automatically run to switch to the new login system - public func cleanupOldLogins() async { - let realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled") - if realDebridEnabled { - enabledDebrids.insert(.realDebrid) - UserDefaults.standard.set(false, forKey: "RealDebrid.Enabled") - } + // TODO: Remove after v0.8.0 + // Function to migrate the preferred service to the new string ID format + public func migratePreferredService(_ idInt: Int) -> String? { + // Undo the EnabledDebrids key + UserDefaults.standard.removeObject(forKey: "Debrid.EnabledArray") - let allDebridEnabled = UserDefaults.standard.bool(forKey: "AllDebrid.Enabled") - if allDebridEnabled { - enabledDebrids.insert(.allDebrid) - UserDefaults.standard.set(false, forKey: "AllDebrid.Enabled") - } - - let premiumizeEnabled = UserDefaults.standard.bool(forKey: "Premiumize.Enabled") - if premiumizeEnabled { - enabledDebrids.insert(.premiumize) - UserDefaults.standard.set(false, forKey: "Premiumize.Enabled") - } + return DebridType(rawValue: idInt)?.toString() } // Wrapper function to match error descriptions -- 2.45.2 From f9b2959ae23a4e1e341c8289066fa48b31df09b5 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 6 Jun 2024 11:54:31 -0400 Subject: [PATCH 21/27] Debrid: Remove more redundant vars the IA vars are no longer needed since that's unified. Signed-off-by: kingbri --- Ferrite/ViewModels/DebridManager.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index aaac8da..c1adfec 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -74,10 +74,6 @@ public class DebridManager: ObservableObject { @Published var showDeleteAlert: Bool = false - var selectedRealDebridItem: DebridIA? - var selectedRealDebridFile: DebridIAFile? - var selectedRealDebridID: String? - // TODO: Maybe make these generic? // RealDebrid cloud variables @Published var realDebridCloudTorrents: [DebridCloudTorrent] = [] @@ -87,9 +83,6 @@ public class DebridManager: ObservableObject { // AllDebrid auth variables var allDebridAuthProcessing: Bool = false - var selectedAllDebridItem: DebridIA? - var selectedAllDebridFile: DebridIAFile? - // AllDebrid cloud variables @Published var allDebridCloudMagnets: [DebridCloudTorrent] = [] @Published var allDebridCloudLinks: [DebridCloudDownload] = [] @@ -98,9 +91,6 @@ public class DebridManager: ObservableObject { // Premiumize auth variables var premiumizeAuthProcessing: Bool = false - var selectedPremiumizeItem: DebridIA? - var selectedPremiumizeFile: DebridIAFile? - // Premiumize cloud variables @Published var premiumizeCloudItems: [DebridCloudDownload] = [] var premiumizeCloudTTL: Double = 0.0 -- 2.45.2 From 447d8b5bd03fb55c161b79b4d897dc49417d6983 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 6 Jun 2024 18:10:54 -0400 Subject: [PATCH 22/27] Debrid: Unify cloud views Cloud torrents and downloads are unified with the new protocol. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 20 +-- Ferrite/API/AllDebridWrapper.swift | 8 +- Ferrite/API/PremiumizeWrapper.swift | 8 +- Ferrite/API/RealDebridWrapper.swift | 8 +- Ferrite/Protocols/Debrid.swift | 4 +- Ferrite/ViewModels/DebridManager.swift | 153 ++++-------------- .../Library/Cloud/CloudDownloadView.swift | 57 +++++++ ...CloudView.swift => CloudTorrentView.swift} | 71 +++----- .../Library/Cloud/PremiumizeCloudView.swift | 60 ------- .../Library/Cloud/RealDebridCloudView.swift | 126 --------------- .../Library/DebridCloudView.swift | 17 +- Ferrite/Views/LibraryView.swift | 8 +- 12 files changed, 141 insertions(+), 399 deletions(-) create mode 100644 Ferrite/Views/ComponentViews/Library/Cloud/CloudDownloadView.swift rename Ferrite/Views/ComponentViews/Library/Cloud/{AllDebridCloudView.swift => CloudTorrentView.swift} (57%) delete mode 100644 Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift delete mode 100644 Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index f957fd7..3c2c2bd 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -20,7 +20,6 @@ 0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */; }; 0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */; }; 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; }; - 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; }; 0C2B028F29E9E61E00DCF127 /* SortFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */; }; 0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; }; 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; }; @@ -54,7 +53,6 @@ 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; }; 0C5708EB29B8F89300BE07F9 /* SettingsLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */; }; 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; }; - 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; 0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */; }; @@ -130,12 +128,13 @@ 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; - 0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; }; 0CB0115B29D36D9E009AFEDE /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */; }; 0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0AB5E29BD2A200015422C /* KodiServerView.swift */; }; 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; + 0CB725322C123E6F0047FC0B /* CloudDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */; }; + 0CB725342C123E760047FC0B /* CloudTorrentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB725332C123E760047FC0B /* CloudTorrentView.swift */; }; 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; @@ -174,7 +173,6 @@ 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableWrapper.swift; sourceTree = ""; }; 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = ""; }; - 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = ""; }; 0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortFilterView.swift; sourceTree = ""; }; 0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = ""; }; @@ -207,7 +205,6 @@ 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; 0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLogView.swift; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = ""; }; - 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = ""; }; 0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiWrapper.swift; sourceTree = ""; }; 0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKodiView.swift; sourceTree = ""; }; 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiModels.swift; sourceTree = ""; }; @@ -279,12 +276,13 @@ 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = ""; }; 0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 0CAF9318296399190050812A /* PremiumizeCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeCloudView.swift; sourceTree = ""; }; 0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = ""; }; 0CB0AB5E29BD2A200015422C /* KodiServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiServerView.swift; sourceTree = ""; }; 0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = ""; }; 0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = ""; }; 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = ""; }; + 0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudDownloadView.swift; sourceTree = ""; }; + 0CB725332C123E760047FC0B /* CloudTorrentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTorrentView.swift; sourceTree = ""; }; 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableInteraction.swift; sourceTree = ""; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; @@ -413,9 +411,8 @@ 0C2886D52960C4F800D6FC16 /* Cloud */ = { isa = PBXGroup; children = ( - 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */, - 0CAF9318296399190050812A /* PremiumizeCloudView.swift */, - 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */, + 0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */, + 0CB725332C123E760047FC0B /* CloudTorrentView.swift */, ); path = Cloud; sourceTree = ""; @@ -856,7 +853,6 @@ 0C5005522992B6750064606A /* PluginTagsView.swift in Sources */, 0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */, 0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */, - 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */, 0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, @@ -875,10 +871,10 @@ 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, 0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */, 0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */, - 0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */, 0C84FCE529E4B43200B0DFE4 /* SelectedDebridFilterView.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */, + 0CB725342C123E760047FC0B /* CloudTorrentView.swift in Sources */, 0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */, 0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */, 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */, @@ -905,7 +901,6 @@ 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */, 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */, 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, - 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */, @@ -959,6 +954,7 @@ 0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */, 0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */, 0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */, + 0CB725322C123E6F0047FC0B /* CloudDownloadView.swift in Sources */, 0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */, 0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */, 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */, diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 7778aa2..cc0abb2 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -299,7 +299,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { // MARK: - Cloud methods // Referred to as "User magnets" in AllDebrid's API - public func getUserTorrents() async throws -> [DebridCloudTorrent] { + public func getUserTorrents() async throws { var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status")) let data = try await performRequest(request: &request, requestName: #function) @@ -319,8 +319,6 @@ public class AllDebrid: PollingDebridSource, ObservableObject { links: magnetResponse.links.map(\.link) ) } - - return cloudTorrents } public func deleteTorrent(torrentId: String?) async throws { @@ -336,7 +334,7 @@ public class AllDebrid: PollingDebridSource, ObservableObject { try await performRequest(request: &request, requestName: #function) } - public func getUserDownloads() async throws -> [DebridCloudDownload] { + public func getUserDownloads() async throws { var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links")) let data = try await performRequest(request: &request, requestName: #function) @@ -352,8 +350,6 @@ public class AllDebrid: PollingDebridSource, ObservableObject { downloadId: link.link, source: self.id, fileName: link.filename, link: link.link ) } - - return cloudDownloads } // Not used diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 468f406..0909fb3 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -303,7 +303,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { // MARK: - Cloud methods - public func getUserDownloads() async throws -> [DebridCloudDownload] { + public func getUserDownloads() async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!) let data = try await performRequest(request: &request, requestName: #function) @@ -317,8 +317,6 @@ public class Premiumize: OAuthDebridSource, ObservableObject { cloudDownloads = rawResponse.files.map { file in DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id) } - - return cloudDownloads } func itemDetails(itemID: String) async throws -> ItemDetailsResponse { @@ -355,9 +353,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { } // No user torrents for Premiumize - public func getUserTorrents() async throws -> [DebridCloudTorrent] { - [] - } + public func getUserTorrents() async throws {} public func deleteTorrent(torrentId: String?) async throws {} } diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index daf42b0..ea6aab7 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -434,7 +434,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { // MARK: - Cloud methods // Gets the user's torrent library - public func getUserTorrents() async throws -> [DebridCloudTorrent] { + public func getUserTorrents() async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!) let data = try await performRequest(request: &request, requestName: #function) @@ -449,8 +449,6 @@ public class RealDebrid: PollingDebridSource, ObservableObject { links: response.links ) } - - return cloudTorrents } // Deletes a torrent download from RD @@ -477,7 +475,7 @@ public class RealDebrid: PollingDebridSource, ObservableObject { } // Gets the user's downloads - public func getUserDownloads() async throws -> [DebridCloudDownload] { + public func getUserDownloads() async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads")!) let data = try await performRequest(request: &request, requestName: #function) @@ -485,8 +483,6 @@ public class RealDebrid: PollingDebridSource, ObservableObject { cloudDownloads = rawResponse.map { response in DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download) } - - return cloudDownloads } // Not used diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 3c0570c..eef1a33 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -39,12 +39,12 @@ public protocol DebridSource: AnyObservableObject { var cloudTTL: Double { get set } // User downloads functions - func getUserDownloads() async throws -> [DebridCloudDownload] + func getUserDownloads() async throws func checkUserDownloads(link: String) async throws -> String? func deleteDownload(downloadId: String) async throws // User torrent functions - func getUserTorrents() async throws -> [DebridCloudTorrent] + func getUserTorrents() async throws func deleteTorrent(torrentId: String?) async throws } diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index c1adfec..ede4ffd 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -74,27 +74,12 @@ public class DebridManager: ObservableObject { @Published var showDeleteAlert: Bool = false - // TODO: Maybe make these generic? - // RealDebrid cloud variables - @Published var realDebridCloudTorrents: [DebridCloudTorrent] = [] - @Published var realDebridCloudDownloads: [DebridCloudDownload] = [] - var realDebridCloudTTL: Double = 0.0 - // AllDebrid auth variables var allDebridAuthProcessing: Bool = false - // AllDebrid cloud variables - @Published var allDebridCloudMagnets: [DebridCloudTorrent] = [] - @Published var allDebridCloudLinks: [DebridCloudDownload] = [] - var allDebridCloudTTL: Double = 0.0 - // Premiumize auth variables var premiumizeAuthProcessing: Bool = false - // Premiumize cloud variables - @Published var premiumizeCloudItems: [DebridCloudDownload] = [] - var premiumizeCloudTTL: Double = 0.0 - init() { // Set the preferred service. Contains migration logic for earlier versions @@ -422,12 +407,11 @@ public class DebridManager: ObservableObject { // Update the UI downloadUrl = downloadLink } else { - throw DebridError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") + throw DebridError.FailedRequest(description: "Could not fetch your file from \(debridSource.id)'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) + await fetchDebridCloud(bypassTTL: true) } catch { switch error { case DebridError.IsCaching: @@ -440,122 +424,55 @@ public class DebridManager: ObservableObject { } } + // Wrapper to handle cloud fetching public func fetchDebridCloud(bypassTTL: Bool = false) async { - switch selectedDebridType { - case .realDebrid: - await fetchRdCloud(bypassTTL: bypassTTL) - case .allDebrid: - await fetchAdCloud(bypassTTL: bypassTTL) - case .premiumize: - await fetchPmCloud(bypassTTL: bypassTTL) - case .none: + guard let selectedSource = selectedDebridSource else { return } - } - // Refreshes torrents and downloads from a RD user's account - public func fetchRdCloud(bypassTTL: Bool = false) async { - if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL { + if bypassTTL || Date().timeIntervalSince1970 > selectedSource.cloudTTL { do { - realDebridCloudTorrents = try await realDebrid.getUserTorrents() - realDebridCloudDownloads = try await realDebrid.getUserDownloads() + // Populates the inner downloads and torrent arrays + try await selectedSource.getUserDownloads() + try await selectedSource.getUserTorrents() - // 5 minutes - realDebridCloudTTL = Date().timeIntervalSince1970 + 300 - } catch { - await sendDebridError(error, prefix: "RealDebrid cloud fetch error") - } - } - } - - func deleteRdDownload(downloadID: String) async { - do { - try await realDebrid.deleteDownload(downloadId: downloadID) - - // Bypass TTL to get current RD values - await fetchRdCloud(bypassTTL: true) - } catch { - await sendDebridError(error, prefix: "RealDebrid download delete error") - } - } - - func deleteRdTorrent(torrentID: String? = nil, presentError: Bool = true) async { - do { - if let torrentID { - try await realDebrid.deleteTorrent(torrentId: torrentID) - - await fetchRdCloud(bypassTTL: true) - } else { - throw DebridError.FailedRequest(description: "No torrent ID was provided") - } - } catch { - await sendDebridError(error, prefix: "RealDebrid torrent delete error", presentError: presentError) - } - } - - // Refreshes torrents and downloads from a RD user's account - public func fetchAdCloud(bypassTTL: Bool = false) async { - if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL { - do { - allDebridCloudMagnets = try await allDebrid.getUserTorrents() - allDebridCloudLinks = try await allDebrid.getUserDownloads() - - // 5 minutes - allDebridCloudTTL = Date().timeIntervalSince1970 + 300 - } catch { - await sendDebridError(error, prefix: "AlLDebrid cloud fetch error") - } - } - } - - func deleteAdLink(link: String) async { - do { - try await allDebrid.deleteDownload(downloadId: link) - - await fetchAdCloud(bypassTTL: true) - } catch { - await sendDebridError(error, prefix: "AllDebrid link delete error") - } - } - - func deleteAdMagnet(magnetId: String) async { - do { - try await allDebrid.deleteTorrent(torrentId: magnetId) - - await fetchAdCloud(bypassTTL: true) - } catch { - await sendDebridError(error, prefix: "AllDebrid magnet delete error") - } - } - - // Refreshes items and fetches from a PM user account - public func fetchPmCloud(bypassTTL: Bool = false) async { - if bypassTTL || Date().timeIntervalSince1970 > premiumizeCloudTTL { - do { - let userItems = try await premiumize.getUserDownloads() - withAnimation { - premiumizeCloudItems = userItems - } - - // 5 minutes - premiumizeCloudTTL = Date().timeIntervalSince1970 + 300 + // Update the TTL to 5 minutes from now + selectedSource.cloudTTL = Date().timeIntervalSince1970 + 300 } catch { let error = error as NSError if error.code != -999 { - await sendDebridError(error, prefix: "Premiumize cloud fetch error") + await sendDebridError(error, prefix: "\(selectedSource.id) cloud fetch error") } + } } } - public func deletePmItem(id: String) async { - do { - try await premiumize.deleteDownload(downloadId: id) + public func deleteCloudDownload(_ download: DebridCloudDownload) async { + guard let selectedSource = selectedDebridSource else { + return + } - // Bypass TTL to get current RD values - await fetchPmCloud(bypassTTL: true) + do { + try await selectedSource.deleteDownload(downloadId: download.downloadId) + + await fetchDebridCloud(bypassTTL: true) } catch { - await sendDebridError(error, prefix: "Premiumize cloud delete error") + await sendDebridError(error, prefix: "\(selectedSource.id) download delete error") + } + } + + public func deleteCloudTorrent(_ torrent: DebridCloudTorrent) async { + guard let selectedSource = selectedDebridSource else { + return + } + + do { + try await selectedSource.deleteTorrent(torrentId: torrent.torrentId) + + await fetchDebridCloud(bypassTTL: true) + } catch { + await sendDebridError(error, prefix: "\(selectedSource.id) torrent delete error") } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/CloudDownloadView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/CloudDownloadView.swift new file mode 100644 index 0000000..9f2b488 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Library/Cloud/CloudDownloadView.swift @@ -0,0 +1,57 @@ +// +// CloudDownloadView.swift +// Ferrite +// +// Created by Brian Dashore on 6/6/24. +// + +import SwiftUI + +struct CloudDownloadView: View { + @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var pluginManager: PluginManager + + @Store var debridSource: DebridSource + + @Binding var searchText: String + + var body: some View { + DisclosureGroup("Downloads") { + ForEach(debridSource.cloudDownloads.filter { + searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) + }, id: \.self) { cloudDownload in + Button(cloudDownload.fileName) { + navModel.resultFromCloud = true + navModel.selectedTitle = cloudDownload.fileName + debridManager.downloadUrl = cloudDownload.link + + PersistenceController.shared.createHistory( + HistoryEntryJson( + name: cloudDownload.fileName, + url: cloudDownload.link, + source: debridSource.id + ), + performSave: true + ) + + pluginManager.runDefaultAction( + urlString: debridManager.downloadUrl, + navModel: navModel + ) + } + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + .tint(.primary) + } + .onDelete { offsets in + for index in offsets { + if let cloudDownload = debridSource.cloudDownloads[safe: index] { + Task { + await debridManager.deleteCloudDownload(cloudDownload) + } + } + } + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/CloudTorrentView.swift similarity index 57% rename from Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift rename to Ferrite/Views/ComponentViews/Library/Cloud/CloudTorrentView.swift index ee72b05..0c86880 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/CloudTorrentView.swift @@ -1,71 +1,36 @@ // -// AllDebridCloudView.swift +// CloudTorrentView.swift // Ferrite // -// Created by Brian Dashore on 1/5/23. +// Created by Brian Dashore on 6/6/24. // import SwiftUI -struct AllDebridCloudView: View { - @EnvironmentObject var debridManager: DebridManager +struct CloudTorrentView: View { @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var pluginManager: PluginManager + @Store var debridSource: DebridSource + @Binding var searchText: String var body: some View { - DisclosureGroup("Links") { - ForEach(debridManager.allDebridCloudLinks.filter { - searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) - }, id: \.self) { cloudDownload in - Button(cloudDownload.fileName) { - navModel.resultFromCloud = true - navModel.selectedTitle = cloudDownload.fileName - debridManager.downloadUrl = cloudDownload.link - - PersistenceController.shared.createHistory( - HistoryEntryJson( - name: cloudDownload.fileName, - url: cloudDownload.link, - source: DebridType.allDebrid.toString() - ), - performSave: true - ) - - pluginManager.runDefaultAction( - urlString: debridManager.downloadUrl, - navModel: navModel - ) - } - .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) - .tint(.primary) - } - .onDelete { offsets in - for index in offsets { - if let cloudDownload = debridManager.allDebridCloudLinks[safe: index] { - Task { - await debridManager.deleteAdLink(link: cloudDownload.downloadId) - } - } - } - } - } - - DisclosureGroup("Magnets") { - ForEach(debridManager.allDebridCloudMagnets.filter { + DisclosureGroup("Torrents") { + ForEach(debridSource.cloudTorrents.filter { searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) }, id: \.self) { cloudTorrent in Button { - if cloudTorrent.status == "Ready", !cloudTorrent.links.isEmpty { + if cloudTorrent.status == "downloaded", !cloudTorrent.links.isEmpty { navModel.resultFromCloud = true navModel.selectedTitle = cloudTorrent.fileName - + var historyInfo = HistoryEntryJson( name: cloudTorrent.fileName, - source: DebridType.allDebrid.toString() + source: debridSource.id ) - + Task { let magnet = Magnet(hash: cloudTorrent.hash, link: nil) await debridManager.populateDebridIA([magnet]) @@ -74,11 +39,11 @@ struct AllDebridCloudView: View { if cloudTorrent.links.count == 1 { await debridManager.fetchDebridDownload(magnet: magnet) - + if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) - + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel @@ -98,11 +63,11 @@ struct AllDebridCloudView: View { .font(.callout) .fixedSize(horizontal: false, vertical: true) .lineLimit(4) - + HStack { Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() - //DebridLabelView(cloudLinks: cloudTorrent.links) + DebridLabelView(debridSource: debridSource, cloudLinks: cloudTorrent.links) } .font(.caption) } @@ -112,9 +77,9 @@ struct AllDebridCloudView: View { } .onDelete { offsets in for index in offsets { - if let cloudTorrent = debridManager.allDebridCloudMagnets[safe: index] { + if let cloudTorrent = debridSource.cloudTorrents[safe: index] { Task { - await debridManager.deleteAdMagnet(magnetId: cloudTorrent.torrentId) + await debridManager.deleteCloudTorrent(cloudTorrent) } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift deleted file mode 100644 index e9acb9a..0000000 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// PremiumizeCloudView.swift -// Ferrite -// -// Created by Brian Dashore on 1/2/23. -// - -import SwiftUI - -struct PremiumizeCloudView: View { - @EnvironmentObject var debridManager: DebridManager - @EnvironmentObject var navModel: NavigationViewModel - @EnvironmentObject var pluginManager: PluginManager - - @Binding var searchText: String - - var body: some View { - DisclosureGroup("Items") { - ForEach(debridManager.premiumizeCloudItems.filter { - searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) - }, id: \.self) { cloudDownload in - Button(cloudDownload.fileName) { - Task { - navModel.resultFromCloud = true - navModel.selectedTitle = cloudDownload.fileName - - await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: cloudDownload.downloadId) - - if !debridManager.downloadUrl.isEmpty { - PersistenceController.shared.createHistory( - HistoryEntryJson( - name: cloudDownload.fileName, - url: cloudDownload.link, - source: DebridType.premiumize.toString() - ), - performSave: true - ) - - pluginManager.runDefaultAction( - urlString: debridManager.downloadUrl, - navModel: navModel - ) - } - } - } - .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) - .tint(.primary) - } - .onDelete { offsets in - for index in offsets { - if let cloudDownload = debridManager.premiumizeCloudItems[safe: index] { - Task { - await debridManager.deletePmItem(id: cloudDownload.downloadId) - } - } - } - } - } - } -} diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift deleted file mode 100644 index b05e764..0000000 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// RealDebridCloudView.swift -// Ferrite -// -// Created by Brian Dashore on 12/31/22. -// - -import SwiftUI - -struct RealDebridCloudView: View { - @EnvironmentObject var navModel: NavigationViewModel - @EnvironmentObject var debridManager: DebridManager - @EnvironmentObject var pluginManager: PluginManager - - @Binding var searchText: String - - var body: some View { - Group { - DisclosureGroup("Downloads") { - ForEach(debridManager.realDebrid.cloudDownloads.filter { - searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) - }, id: \.self) { cloudDownload in - Button(cloudDownload.fileName) { - navModel.resultFromCloud = true - navModel.selectedTitle = cloudDownload.fileName - debridManager.downloadUrl = cloudDownload.link - - PersistenceController.shared.createHistory( - HistoryEntryJson( - name: cloudDownload.fileName, - url: cloudDownload.link, - source: DebridType.realDebrid.toString() - ), - performSave: true - ) - - pluginManager.runDefaultAction( - urlString: debridManager.downloadUrl, - navModel: navModel - ) - } - .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) - .tint(.primary) - } - .onDelete { offsets in - for index in offsets { - if let cloudDownload = debridManager.realDebridCloudDownloads[safe: index] { - Task { - await debridManager.deleteRdDownload(downloadID: cloudDownload.downloadId) - } - } - } - } - } - - DisclosureGroup("Torrents") { - ForEach(debridManager.realDebrid.cloudTorrents.filter { - searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased()) - }, id: \.self) { cloudTorrent in - Button { - if cloudTorrent.status == "downloaded", !cloudTorrent.links.isEmpty { - navModel.resultFromCloud = true - navModel.selectedTitle = cloudTorrent.fileName - - var historyInfo = HistoryEntryJson( - name: cloudTorrent.fileName, - source: DebridType.realDebrid.toString() - ) - - Task { - let magnet = Magnet(hash: cloudTorrent.hash, link: nil) - await debridManager.populateDebridIA([magnet]) - if debridManager.selectDebridResult(magnet: magnet) { - // Is this a batch? - - if cloudTorrent.links.count == 1 { - await debridManager.fetchDebridDownload(magnet: magnet) - - if !debridManager.downloadUrl.isEmpty { - historyInfo.url = debridManager.downloadUrl - PersistenceController.shared.createHistory(historyInfo, performSave: true) - - pluginManager.runDefaultAction( - urlString: debridManager.downloadUrl, - navModel: navModel - ) - } - } else { - navModel.selectedMagnet = magnet - navModel.selectedHistoryInfo = historyInfo - navModel.currentChoiceSheet = .batch - } - } - } - } - } label: { - VStack(alignment: .leading, spacing: 10) { - Text(cloudTorrent.fileName) - .font(.callout) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(4) - - HStack { - Text(cloudTorrent.status.capitalizingFirstLetter()) - Spacer() - //DebridLabelView(cloudLinks: cloudTorrent.links) - } - .font(.caption) - } - } - .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) - .tint(.primary) - } - .onDelete { offsets in - for index in offsets { - if let cloudTorrent = debridManager.realDebridCloudTorrents[safe: index] { - Task { - await debridManager.deleteRdTorrent(torrentID: cloudTorrent.torrentId) - } - } - } - } - } - } - } -} diff --git a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift index b9e271f..a52120a 100644 --- a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift @@ -10,19 +10,18 @@ import SwiftUI struct DebridCloudView: View { @EnvironmentObject var debridManager: DebridManager + @Store var debridSource: DebridSource + @Binding var searchText: String var body: some View { List { - switch debridManager.selectedDebridType { - case .realDebrid: - RealDebridCloudView(searchText: $searchText) - case .premiumize: - PremiumizeCloudView(searchText: $searchText) - case .allDebrid: - AllDebridCloudView(searchText: $searchText) - case .none: - EmptyView() + if !debridSource.cloudDownloads.isEmpty { + CloudDownloadView(debridSource: debridSource, searchText: $searchText) + } + + if !debridSource.cloudTorrents.isEmpty { + CloudTorrentView(debridSource: debridSource, searchText: $searchText) } } .listStyle(.plain) diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index ac1260f..347bc04 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -38,7 +38,13 @@ struct LibraryView: View { case .history: HistoryView(allHistoryEntries: allHistoryEntries, searchText: $searchText) case .debridCloud: - DebridCloudView(searchText: $searchText) + if let selectedDebridSource = debridManager.selectedDebridSource { + DebridCloudView(debridSource: selectedDebridSource, searchText: $searchText) + } else { + // Placeholder view that takes up the entire parent view + Color.clear + .frame(maxWidth: .infinity) + } } } .overlay { -- 2.45.2 From 2c25bd98dbe697a6729072a0f6b18c4de02fe10d Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 8 Jun 2024 01:09:18 -0400 Subject: [PATCH 23/27] Debrid: Migrate auth to protocol Unify authentication to the new protocol. Also remove logout on invalid requests. This became annoying and didn't update the UI properly. Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 13 +- Ferrite/API/PremiumizeWrapper.swift | 15 +- Ferrite/API/RealDebridWrapper.swift | 15 +- Ferrite/Protocols/Debrid.swift | 5 +- Ferrite/ViewModels/DebridManager.swift | 220 ++++++------------ .../Library/DebridCloudView.swift | 2 +- .../Settings/SettingsDebridInfoView.swift | 12 +- Ferrite/Views/SettingsView.swift | 2 +- 8 files changed, 118 insertions(+), 166 deletions(-) diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index cc0abb2..eaa9133 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -19,6 +19,14 @@ public class AllDebrid: PollingDebridSource, ObservableObject { getToken() != nil } + public var manualToken: String? { + if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") { + return getToken() + } else { + return nil + } + } + @Published public var IAValues: [DebridIA] = [] @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] @@ -101,11 +109,9 @@ public class AllDebrid: PollingDebridSource, ObservableObject { } // Adds a manual API key instead of web auth - public func setApiKey(_ key: String) -> Bool { + public func setApiKey(_ key: String) { FerriteKeychain.shared.set(key, forKey: "AllDebrid.ApiKey") UserDefaults.standard.set(true, forKey: "AllDebrid.UseManualKey") - - return FerriteKeychain.shared.get("AllDebrid.ApiKey") == key } public func getToken() -> String? { @@ -137,7 +143,6 @@ public class AllDebrid: PollingDebridSource, ObservableObject { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - logout() throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.") } else { throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 0909fb3..b45f8f2 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -13,7 +13,15 @@ public class Premiumize: OAuthDebridSource, ObservableObject { public let website = "https://premiumize.me" @Published public var authProcessing: Bool = false public var isLoggedIn: Bool { - getToken() != nil + return getToken() != nil + } + + public var manualToken: String? { + if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") { + return getToken() + } else { + return nil + } } @Published public var IAValues: [DebridIA] = [] @@ -62,11 +70,9 @@ public class Premiumize: OAuthDebridSource, ObservableObject { } // Adds a manual API key instead of web auth - public func setApiKey(_ key: String) -> Bool { + public func setApiKey(_ key: String) { FerriteKeychain.shared.set(key, forKey: "Premiumize.AccessToken") UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey") - - return FerriteKeychain.shared.get("Premiumize.AccessToken") == key } public func getToken() -> String? { @@ -118,7 +124,6 @@ public class Premiumize: OAuthDebridSource, ObservableObject { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - logout() throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.") } else { throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index ea6aab7..cb1dd13 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -15,11 +15,19 @@ public class RealDebrid: PollingDebridSource, ObservableObject { @Published public var authProcessing: Bool = false - // Directly checked because the request fetch uses async + // Check the manual token since getTokens() is async public var isLoggedIn: Bool { FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil } + public var manualToken: String? { + if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") { + return FerriteKeychain.shared.get("RealDebrid.AccessToken") + } else { + return nil + } + } + @Published public var IAValues: [DebridIA] = [] @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] @@ -175,14 +183,12 @@ public class RealDebrid: PollingDebridSource, ObservableObject { // Adds a manual API key instead of web auth // Clear out existing refresh tokens and timestamps - public func setApiKey(_ key: String) -> Bool { + public func setApiKey(_ key: String) { FerriteKeychain.shared.set(key, forKey: "RealDebrid.AccessToken") FerriteKeychain.shared.delete("RealDebrid.RefreshToken") FerriteKeychain.shared.delete("RealDebrid.AccessTokenStamp") UserDefaults.standard.set(true, forKey: "RealDebrid.UseManualKey") - - return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key } // Deletes tokens from device and RD's servers @@ -222,7 +228,6 @@ public class RealDebrid: PollingDebridSource, ObservableObject { if response.statusCode >= 200, response.statusCode <= 299 { return data } else if response.statusCode == 401 { - await logout() throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.") } else { throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index eef1a33..75a84d6 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -18,8 +18,11 @@ public protocol DebridSource: AnyObservableObject { var authProcessing: Bool { get set } var isLoggedIn: Bool { get } + // Manual API key + var manualToken: String? { get } + // Common authentication functions - func setApiKey(_ key: String) -> Bool + func setApiKey(_ key: String) func logout() async // Instant availability variables diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index ede4ffd..3675c26 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -26,6 +26,10 @@ public class DebridManager: ObservableObject { debridSources.contains { $0.isLoggedIn } } + var enabledDebridCount: Int { + debridSources.filter{ $0.isLoggedIn }.count + } + @Published var selectedDebridSource: DebridSource? { didSet { UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService") @@ -34,18 +38,8 @@ public class DebridManager: ObservableObject { var selectedDebridItem: DebridIA? var selectedDebridFile: DebridIAFile? - // Service agnostic variables - @Published var enabledDebrids: Set = [] { - didSet { - UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray") - } - } - - @Published var selectedDebridType: DebridType? { - didSet { - UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService") - } - } + // TODO: Figure out a way to remove this var + var selectedOAuthDebridSource: OAuthDebridSource? @Published var filteredIAStatus: Set = [] @@ -53,22 +47,6 @@ public class DebridManager: ObservableObject { var downloadUrl: String = "" var authUrl: URL? - // Is the current debrid type processing an auth request - func authProcessing(_ passedDebridType: DebridType?) -> Bool { - guard let debridType = passedDebridType ?? selectedDebridType else { - return false - } - - switch debridType { - case .realDebrid: - return realDebridAuthProcessing - case .allDebrid: - return allDebridAuthProcessing - case .premiumize: - return premiumizeAuthProcessing - } - } - // RealDebrid auth variables var realDebridAuthProcessing: Bool = false @@ -89,7 +67,6 @@ public class DebridManager: ObservableObject { if let preferredServiceInt = Int(rawPreferredService) { debridServiceId = migratePreferredService(preferredServiceInt) } else { - print(rawPreferredService) debridServiceId = rawPreferredService } @@ -207,73 +184,62 @@ public class DebridManager: ObservableObject { // MARK: - Authentication UI linked functions // Common function to delegate what debrid service to authenticate with - public func authenticateDebrid(debridType: DebridType, apiKey: String?) async { - switch debridType { - case .realDebrid: - let success = apiKey == nil ? await authenticateRd() : realDebrid.setApiKey(apiKey!) - completeDebridAuth(debridType, success: success) - case .allDebrid: - // Async can't work with nil mapping method - let success = apiKey == nil ? await authenticateAd() : allDebrid.setApiKey(apiKey!) - completeDebridAuth(debridType, success: success) - case .premiumize: - if let apiKey { - let success = premiumize.setApiKey(apiKey) - completeDebridAuth(debridType, success: success) - } else { - await authenticatePm() + public func authenticateDebrid(_ debridSource: some DebridSource, apiKey: String?) async { + defer { + // Don't cancel processing if using OAuth + if !(debridSource is OAuthDebridSource) { + debridSource.authProcessing = false } - } - } - // Callback to finish debrid auth since functions can be split - func completeDebridAuth(_ debridType: DebridType, success: Bool) { - if success { - enabledDebrids.insert(debridType) - if enabledDebrids.count == 1 { - selectedDebridType = enabledDebrids.first + if enabledDebridCount == 1 { + selectedDebridSource = debridSource } } - switch debridType { - case .realDebrid: - realDebridAuthProcessing = false - case .allDebrid: - allDebridAuthProcessing = false - case .premiumize: - premiumizeAuthProcessing = false + // Set an API key if manually provided + if let apiKey { + debridSource.setApiKey(apiKey) + return + } + + // Processing has started + debridSource.authProcessing = true + + if let pollingSource = debridSource as? PollingDebridSource { + do { + let authUrl = try await pollingSource.getAuthUrl() + + if validateAuthUrl(authUrl) { + try await pollingSource.authTask?.value + } else { + throw DebridError.AuthQuery(description: "The authentication URL was invalid") + } + } catch { + await sendDebridError(error, prefix: "\(debridSource.id) authentication error") + + pollingSource.authTask?.cancel() + } + } else if let oauthSource = debridSource as? OAuthDebridSource { + do { + let tempAuthUrl = try oauthSource.getAuthUrl() + selectedOAuthDebridSource = oauthSource + + validateAuthUrl(tempAuthUrl, useAuthSession: true) + } catch { + await sendDebridError(error, prefix: "\(debridSource.id) authentication error") + } + } else { + logManager?.error( + "DebridManager: Auth: Could not figure out the authentication type for \(debridSource.id). Is this configured properly?" + ) + + return } } // Get a truncated manual API key if it's being used - func getManualAuthKey(_ passedDebridType: DebridType?) async -> String? { - guard let debridType = passedDebridType ?? selectedDebridType else { - return nil - } - - let debridToken: String? - switch debridType { - case .realDebrid: - if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") { - debridToken = FerriteKeychain.shared.get("RealDebrid.AccessToken") - } else { - debridToken = nil - } - case .allDebrid: - if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") { - debridToken = FerriteKeychain.shared.get("AllDebrid.ApiKey") - } else { - debridToken = nil - } - case .premiumize: - if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") { - debridToken = FerriteKeychain.shared.get("Premiumize.AccessToken") - } else { - debridToken = nil - } - } - - if let debridToken { + func getManualAuthKey(_ debridSource: some DebridSource) async -> String? { + if let debridToken = debridSource.manualToken { let splitString = debridToken.suffix(4) if debridToken.count > 4 { @@ -303,74 +269,42 @@ public class DebridManager: ObservableObject { return true } - private func authenticateRd() async -> Bool { - do { - realDebridAuthProcessing = true - let authUrl = try await realDebrid.getAuthUrl() - - if validateAuthUrl(authUrl) { - try await realDebrid.authTask?.value - return true - } else { - throw DebridError.AuthQuery(description: "The verification URL was invalid") - } - } catch { - await sendDebridError(error, prefix: "RealDebrid authentication error") - - realDebrid.authTask?.cancel() - return false - } - } - - private func authenticateAd() async -> Bool { - do { - allDebridAuthProcessing = true - let authUrl = try await allDebrid.getAuthUrl() - - if validateAuthUrl(authUrl) { - try await allDebrid.authTask?.value - return true - } else { - throw DebridError.AuthQuery(description: "The PIN URL was invalid") - } - } catch { - await sendDebridError(error, prefix: "AllDebrid authentication error") - - allDebrid.authTask?.cancel() - return false - } - } - - private func authenticatePm() async { - do { - premiumizeAuthProcessing = true - let tempAuthUrl = try premiumize.getAuthUrl() - - validateAuthUrl(tempAuthUrl, useAuthSession: true) - } catch { - await sendDebridError(error, prefix: "Premiumize authentication error") - - completeDebridAuth(.premiumize, success: false) - } - } - // Currently handles Premiumize callback - public func handleCallback(url: URL?, error: Error?) async { + public func handleAuthCallback(url: URL?, error: Error?) async { + defer { + if enabledDebridCount == 1 { + selectedDebridSource = selectedOAuthDebridSource + } + + selectedOAuthDebridSource?.authProcessing = false + } + do { + guard let oauthDebridSource = selectedOAuthDebridSource else { + throw DebridError.AuthQuery(description: "OAuth source couldn't be found for callback. Aborting.") + } + if let error { throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)") - } + } if let callbackUrl = url { - try premiumize.handleAuthCallback(url: callbackUrl) - completeDebridAuth(.premiumize, success: true) + try oauthDebridSource.handleAuthCallback(url: callbackUrl) } else { throw DebridError.AuthQuery(description: "The callback URL was invalid") } } catch { await sendDebridError(error, prefix: "Premiumize authentication error (callback)") + } + } - completeDebridAuth(.premiumize, success: false) + // MARK: - Logout UI functions + + public func logout(_ debridSource: some DebridSource) async { + await debridSource.logout() + + if selectedDebridSource?.id == debridSource.id { + selectedDebridSource = nil } } diff --git a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift index a52120a..9a612a8 100644 --- a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift @@ -31,7 +31,7 @@ struct DebridCloudView: View { .refreshable { await debridManager.fetchDebridCloud(bypassTTL: true) } - .onChange(of: debridManager.selectedDebridType) { newType in + .onChange(of: debridManager.selectedDebridSource?.id) { newType in if newType != nil { Task { await debridManager.fetchDebridCloud() diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift index 47d81c9..001cb3a 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift @@ -31,12 +31,12 @@ struct SettingsDebridInfoView: View { Button { Task { if debridSource.isLoggedIn { - await debridSource.logout() + await debridManager.logout(debridSource) } else if !debridSource.authProcessing { - //await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) + await debridManager.authenticateDebrid(debridSource, apiKey: nil) } - //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? "" } } label: { Text( @@ -57,8 +57,8 @@ struct SettingsDebridInfoView: View { onCommit: { Task { if !apiKeyTempText.isEmpty { - //await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) - //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + await debridManager.authenticateDebrid(debridSource, apiKey: apiKeyTempText) + apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? "" } } } @@ -67,7 +67,7 @@ struct SettingsDebridInfoView: View { } .onAppear { Task { - //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? "" } } } diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 4171c96..3c8ffcd 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -227,7 +227,7 @@ struct SettingsView: View { callbackURLScheme: "ferrite" ) { callbackURL, error in Task { - await debridManager.handleCallback(url: callbackURL, error: error) + await debridManager.handleAuthCallback(url: callbackURL, error: error) } } .prefersEphemeralWebBrowserSession(useEphemeralAuth) -- 2.45.2 From 75291c439654eec1fe8f529a8855b283e6c45d09 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 8 Jun 2024 12:27:36 -0400 Subject: [PATCH 24/27] Tree: Format Signed-off-by: kingbri --- Ferrite/API/PremiumizeWrapper.swift | 2 +- Ferrite/API/RealDebridWrapper.swift | 2 +- Ferrite/Protocols/Debrid.swift | 2 +- Ferrite/Utils/Store.swift | 39 +++++++++---------- Ferrite/ViewModels/DebridManager.swift | 11 +++--- .../Library/Cloud/CloudTorrentView.swift | 10 ++--- 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index b45f8f2..bc14311 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -13,7 +13,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject { public let website = "https://premiumize.me" @Published public var authProcessing: Bool = false public var isLoggedIn: Bool { - return getToken() != nil + getToken() != nil } public var manualToken: String? { diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index cb1dd13..fdeccab 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -7,7 +7,7 @@ import Foundation -public class RealDebrid: PollingDebridSource, ObservableObject { +public class RealDebrid: PollingDebridSource, ObservableObject { public let id = "RealDebrid" public let abbreviation = "RD" public let website = "https://real-debrid.com" diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 75a84d6..fb24d7d 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -9,7 +9,7 @@ import Foundation public protocol DebridSource: AnyObservableObject { // ID of the service - //var id: DebridInfo { get } + // var id: DebridInfo { get } var id: String { get } var abbreviation: String { get } var website: String { get } diff --git a/Ferrite/Utils/Store.swift b/Ferrite/Utils/Store.swift index a663fe6..34cd3b2 100644 --- a/Ferrite/Utils/Store.swift +++ b/Ferrite/Utils/Store.swift @@ -12,12 +12,12 @@ // TODO: Replace with Observable when minVersion >= iOS 17 // -import SwiftUI import Combine +import SwiftUI class ErasedObservableObject: ObservableObject { let objectWillChange: AnyPublisher - + init(objectWillChange: AnyPublisher) { self.objectWillChange = objectWillChange } @@ -62,14 +62,14 @@ public protocol AnyObservableObject: AnyObject { public struct Store { /// The underlying object being stored. public let wrappedValue: ObjectType - + // See https://github.com/Tiny-Home-Consulting/Dependiject/issues/38 fileprivate var _observableObject: ObservedObject @MainActor internal var observableObject: ErasedObservableObject { - return _observableObject.wrappedValue + _observableObject.wrappedValue } - + /// A projected value which has the same properties as the wrapped value, but presented as /// bindings. /// @@ -84,55 +84,54 @@ public struct Store { /// } /// ``` public var projectedValue: Wrapper { - return Wrapper(self) + Wrapper(self) } - + /// Create a stored value on a custom scheduler. /// /// Use this init to schedule updates on a specific scheduler other than `DispatchQueue.main`. - public init( - wrappedValue: ObjectType, - on scheduler: S, - schedulerOptions: S.SchedulerOptions? = nil - ) { + public init(wrappedValue: ObjectType, + on scheduler: S, + schedulerOptions: S.SchedulerOptions? = nil) + { self.wrappedValue = wrappedValue - + if let observable = wrappedValue as? AnyObservableObject { let objectWillChange = observable.objectWillChange .receive(on: scheduler, options: schedulerOptions) .eraseToAnyPublisher() - self._observableObject = .init(initialValue: .init(objectWillChange: objectWillChange)) + _observableObject = .init(initialValue: .init(objectWillChange: objectWillChange)) } else { assertionFailure( "Only use the Store property wrapper with objects conforming to AnyObservableObject." ) - self._observableObject = .init(initialValue: .empty()) + _observableObject = .init(initialValue: .empty()) } } - + /// Create a stored value which publishes on the main thread. /// /// To control when updates are published, see ``init(wrappedValue:on:schedulerOptions:)``. public init(wrappedValue: ObjectType) { self.init(wrappedValue: wrappedValue, on: DispatchQueue.main) } - + /// An equivalent to SwiftUI's /// [`ObservedObject.Wrapper`](https://developer.apple.com/documentation/swiftui/observedobject/wrapper) /// type. @dynamicMemberLookup public struct Wrapper { private var store: Store - + internal init(_ store: Store) { self.store = store } - + /// Returns a binding to the resulting value of a given key path. public subscript( dynamicMember keyPath: ReferenceWritableKeyPath ) -> Binding { - return Binding { + Binding { self.store.wrappedValue[keyPath: keyPath] } set: { self.store.wrappedValue[keyPath: keyPath] = $0 diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 3675c26..a113407 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -27,7 +27,7 @@ public class DebridManager: ObservableObject { } var enabledDebridCount: Int { - debridSources.filter{ $0.isLoggedIn }.count + debridSources.filter(\.isLoggedIn).count } @Published var selectedDebridSource: DebridSource? { @@ -35,6 +35,7 @@ public class DebridManager: ObservableObject { UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService") } } + var selectedDebridItem: DebridIA? var selectedDebridFile: DebridIAFile? @@ -59,7 +60,6 @@ public class DebridManager: ObservableObject { var premiumizeAuthProcessing: Bool = false init() { - // Set the preferred service. Contains migration logic for earlier versions if let rawPreferredService = UserDefaults.standard.string(forKey: "Debrid.PreferredService") { let debridServiceId: String? @@ -72,8 +72,8 @@ public class DebridManager: ObservableObject { // Only set the debrid source if it's logged in // Otherwise remove the key - let tempDebridSource = self.debridSources.first { $0.id == debridServiceId } - if (tempDebridSource?.isLoggedIn ?? false) { + let tempDebridSource = debridSources.first { $0.id == debridServiceId } + if tempDebridSource?.isLoggedIn ?? false { selectedDebridSource = tempDebridSource } else { UserDefaults.standard.removeObject(forKey: "Debrid.PreferredService") @@ -286,7 +286,7 @@ public class DebridManager: ObservableObject { if let error { throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)") - } + } if let callbackUrl = url { try oauthDebridSource.handleAuthCallback(url: callbackUrl) @@ -377,7 +377,6 @@ public class DebridManager: ObservableObject { if error.code != -999 { await sendDebridError(error, prefix: "\(selectedSource.id) cloud fetch error") } - } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/CloudTorrentView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/CloudTorrentView.swift index 0c86880..cdc950f 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/CloudTorrentView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/CloudTorrentView.swift @@ -25,12 +25,12 @@ struct CloudTorrentView: View { if cloudTorrent.status == "downloaded", !cloudTorrent.links.isEmpty { navModel.resultFromCloud = true navModel.selectedTitle = cloudTorrent.fileName - + var historyInfo = HistoryEntryJson( name: cloudTorrent.fileName, source: debridSource.id ) - + Task { let magnet = Magnet(hash: cloudTorrent.hash, link: nil) await debridManager.populateDebridIA([magnet]) @@ -39,11 +39,11 @@ struct CloudTorrentView: View { if cloudTorrent.links.count == 1 { await debridManager.fetchDebridDownload(magnet: magnet) - + if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) - + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel @@ -63,7 +63,7 @@ struct CloudTorrentView: View { .font(.callout) .fixedSize(horizontal: false, vertical: true) .lineLimit(4) - + HStack { Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() -- 2.45.2 From 9b936778b0a5eb7b8ae2832ce8b93fa416ad20c6 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 8 Jun 2024 12:28:19 -0400 Subject: [PATCH 25/27] Logging: Improve generic error message Point the user to settings logs rather than giving no extra information. It would be a good idea to give the type of error in the future. Signed-off-by: kingbri --- Ferrite/ViewModels/LoggingManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ferrite/ViewModels/LoggingManager.swift b/Ferrite/ViewModels/LoggingManager.swift index 3e84b14..86c7334 100644 --- a/Ferrite/ViewModels/LoggingManager.swift +++ b/Ferrite/ViewModels/LoggingManager.swift @@ -121,7 +121,7 @@ class LoggingManager: ObservableObject { if let description { toastDescription = description } else if showErrorToasts { - toastDescription = "An error was logged" + toastDescription = "An error was logged. Please look at logs in Settings." } } -- 2.45.2 From 26b9bdd702ae0cb98636ab2ae2f8d2394c59ad67 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sun, 9 Jun 2024 20:28:02 -0400 Subject: [PATCH 26/27] Premiumize: Fix service-specific errors This parameter should be optional and errors if it isn't. Signed-off-by: kingbri --- Ferrite/API/PremiumizeWrapper.swift | 5 +++-- Ferrite/Models/PremiumizeModels.swift | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index bc14311..4f1803e 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -203,9 +203,10 @@ public class Premiumize: OAuthDebridSource, ObservableObject { let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode(DDLResponse.self, from: data) + let content = rawResponse.content ?? [] - if !rawResponse.content.isEmpty { - let files = rawResponse.content.map { file in + if !content.isEmpty { + let files = content.map { file in DebridIAFile( fileId: 0, name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path, diff --git a/Ferrite/Models/PremiumizeModels.swift b/Ferrite/Models/PremiumizeModels.swift index 6ec9606..d160c11 100644 --- a/Ferrite/Models/PremiumizeModels.swift +++ b/Ferrite/Models/PremiumizeModels.swift @@ -19,7 +19,7 @@ public extension Premiumize { struct DDLResponse: Codable { let status: String - let content: [DDLData] + let content: [DDLData]? let location: String let filename: String let filesize: Int -- 2.45.2 From ea4a4350bae8369f4176b26e3cf0d818618ecce7 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sun, 9 Jun 2024 20:28:54 -0400 Subject: [PATCH 27/27] Debrid: Fix UI updates for IA Hook to the published variable to push updates. Signed-off-by: kingbri --- Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift | 8 +------- .../ComponentViews/Filters/SelectedDebridFilterView.swift | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift index 84e2d5e..f402ec1 100644 --- a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift +++ b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift @@ -17,16 +17,10 @@ struct DebridLabelView: View { var body: some View { Tag( name: debridSource.abbreviation, - color: tagColor, + color: getTagColor(), horizontalPadding: 5, verticalPadding: 3 ) - .onAppear { - tagColor = getTagColor() - } - .onChange(of: debridSource.IAValues) { _ in - tagColor = getTagColor() - } } func getTagColor() -> Color { diff --git a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift index c6bc9c3..bec93ec 100644 --- a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift +++ b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift @@ -40,6 +40,5 @@ struct SelectedDebridFilterView: View { } label: { label } - .id(debridManager.selectedDebridSource?.id) } } -- 2.45.2