diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index c9689eb..999e7e2 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; }; + 0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; }; + 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; }; 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; }; 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; @@ -20,6 +22,8 @@ 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; }; 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; }; 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; }; + 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */; }; + 0C42B5982932F6DD008057A0 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Array.swift */; }; 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; }; 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; }; 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.swift */; }; @@ -28,11 +32,13 @@ 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; }; 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; }; 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; }; - 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; }; + 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; }; 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; }; + 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */; }; + 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */; }; 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; }; 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; }; 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; }; @@ -105,6 +111,8 @@ /* Begin PBXFileReference section */ 0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = ""; }; + 0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = ""; }; + 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = ""; }; 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = ""; }; 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = ""; }; @@ -117,6 +125,8 @@ 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = ""; }; 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = ""; }; + 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridChoiceView.swift; sourceTree = ""; }; + 0C42B5972932F6DD008057A0 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = ""; }; 0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = ""; }; @@ -124,9 +134,11 @@ 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; - 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; + 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; 0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = ""; }; + 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = ""; }; + 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridModels.swift; sourceTree = ""; }; 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = ""; }; 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenter.swift; sourceTree = ""; }; 0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = ""; }; @@ -213,6 +225,36 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0C0755C22934241F00ECA142 /* SheetViews */ = { + isa = PBXGroup; + children = ( + 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, + 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */, + ); + path = SheetViews; + sourceTree = ""; + }; + 0C0755C32934244500ECA142 /* ComponentViews */ = { + isa = PBXGroup; + children = ( + 0C0755C42934245800ECA142 /* Debrid */, + 0CA3B23528C265FD00616D3A /* Library */, + 0C44E2AB28D4E126007711AE /* SearchResult */, + 0CA0545C288F7CB200850554 /* Settings */, + 0C794B65289DAC9F00DD1CC8 /* Source */, + ); + path = ComponentViews; + sourceTree = ""; + }; + 0C0755C42934245800ECA142 /* Debrid */ = { + isa = PBXGroup; + children = ( + 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */, + 0C0755C5293424A200ECA142 /* DebridLabelView.swift */, + ); + path = Debrid; + sourceTree = ""; + }; 0C0D50DE288DF72D0035ECC8 /* Classes */ = { isa = PBXGroup; children = ( @@ -241,7 +283,9 @@ 0C0D50E3288DFE6E0035ECC8 /* Models */ = { isa = PBXGroup; children = ( + 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */, 0C7ED14028D61BBA009E29AD /* BackupModels.swift */, + 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */, 0C68135128BC1A7C00FAD890 /* GithubModels.swift */, 0C0167DB29293FA900B65783 /* RealDebridModels.swift */, 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, @@ -281,25 +325,25 @@ path = Buttons; sourceTree = ""; }; - 0C44E2AB28D4E126007711AE /* SearchResultViews */ = { + 0C44E2AB28D4E126007711AE /* SearchResult */ = { isa = PBXGroup; children = ( 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, - 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */, + 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */, ); - path = SearchResultViews; + path = SearchResult; sourceTree = ""; }; - 0C794B65289DAC9F00DD1CC8 /* SourceViews */ = { + 0C794B65289DAC9F00DD1CC8 /* Source */ = { isa = PBXGroup; children = ( 0C44E2AA28D4E09B007711AE /* Buttons */, 0C733286289C4C820058D1FE /* SourceSettingsView.swift */, ); - path = SourceViews; + path = Source; sourceTree = ""; }; - 0CA0545C288F7CB200850554 /* SettingsViews */ = { + 0CA0545C288F7CB200850554 /* Settings */ = { isa = PBXGroup; children = ( 0C44E2AE28D52E8A007711AE /* BackupsView.swift */, @@ -308,7 +352,7 @@ 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */, 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */, ); - path = SettingsViews; + path = Settings; sourceTree = ""; }; 0CA148BA288903F000DE2211 /* Ferrite */ = { @@ -357,6 +401,7 @@ 0CA148C8288903F000DE2211 /* Extensions */ = { isa = PBXGroup; children = ( + 0C42B5972932F6DD008057A0 /* Array.swift */, 0CA148C9288903F000DE2211 /* Collection.swift */, 0CA148CA288903F000DE2211 /* Data.swift */, 0CA429F728C5098D000D0610 /* DateFormatter.swift */, @@ -373,12 +418,10 @@ 0CA148EE2889061200DE2211 /* Views */ = { isa = PBXGroup; children = ( - 0CA3B23528C265FD00616D3A /* LibraryViews */, - 0C794B65289DAC9F00DD1CC8 /* SourceViews */, + 0C0755C32934244500ECA142 /* ComponentViews */, 0CA148F02889062700DE2211 /* RepresentableViews */, 0CA148C0288903F000DE2211 /* CommonViews */, - 0C44E2AB28D4E126007711AE /* SearchResultViews */, - 0CA0545C288F7CB200850554 /* SettingsViews */, + 0C0755C22934241F00ECA142 /* SheetViews */, 0CA148D1288903F000DE2211 /* MainView.swift */, 0CA148D4288903F000DE2211 /* ContentView.swift */, 0CA148D3288903F000DE2211 /* SearchResultsView.swift */, @@ -387,8 +430,6 @@ 0CA148BB288903F000DE2211 /* SettingsView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */, 0CA148BC288903F000DE2211 /* LoginWebView.swift */, - 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */, - 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, ); path = Views; sourceTree = ""; @@ -417,13 +458,14 @@ 0CA148F12889066000DE2211 /* API */ = { isa = PBXGroup; children = ( + 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */, 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */, 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */, ); path = API; sourceTree = ""; }; - 0CA3B23528C265FD00616D3A /* LibraryViews */ = { + 0CA3B23528C265FD00616D3A /* Library */ = { isa = PBXGroup; children = ( 0CA3B23828C2660D00616D3A /* BookmarksView.swift */, @@ -431,7 +473,7 @@ 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */, 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */, ); - path = LibraryViews; + path = Library; sourceTree = ""; }; 0CAF1C5F286F5C0D00296F86 = { @@ -562,11 +604,13 @@ 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, + 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */, 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */, 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */, + 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, @@ -588,6 +632,7 @@ 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, + 0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */, 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */, @@ -596,6 +641,8 @@ 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, + 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */, + 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */, 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, @@ -606,7 +653,8 @@ 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */, 0CA148E2288903F000DE2211 /* Data.swift in Sources */, 0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */, - 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */, + 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */, + 0C42B5982932F6DD008057A0 /* Array.swift in Sources */, 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */, 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */, diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 9d9459c..8e79f5f 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -6,3 +6,198 @@ // import Foundation +import KeychainSwift + +// TODO: Fix errors +public class AllDebrid { + let jsonDecoder = JSONDecoder() + let keychain = KeychainSwift() + + let baseApiUrl = "https://api.alldebrid.com/v4" + let appName = "Ferrite" + + var authTask: Task? + + // Fetches information for PIN auth + public func getPinInfo() async throws -> PinResponse { + let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get") + print("Auth URL: \(url)") + 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 + } catch { + print("Couldn't get pin information!") + throw ADError.AuthQuery(description: error.localizedDescription) + } + } + + // Fetches API keys + public func getApiKey(checkID: String, pin: String) async throws { + let queryItems = [ + URLQueryItem(name: "agent", value: appName), + URLQueryItem(name: "check", value: checkID), + URLQueryItem(name: "pin", value: pin) + ] + + let request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/pin/check", queryItems: queryItems)) + + // Timer to poll AD API for key + authTask = Task { + var count = 0 + + while count < 20 { + if Task.isCancelled { + throw ADError.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(ADResponse.self, from: data).data + + // If there's an API key from the response, end the task successfully + if let apiKeyResponse = rawResponse { + keychain.set(apiKeyResponse.apikey, forKey: "AllDebrid.ApiKey") + + return + } else { + try await Task.sleep(seconds: 5) + count += 1 + } + } + + throw ADError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") + } + + if case let .failure(error) = await authTask?.result { + throw error + } + } + + // Clears tokens. No endpoint to deregister a device + public func deleteTokens() { + keychain.delete("AllDebrid.ApiKey") + } + + // 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 = keychain.get("AllDebrid.ApiKey") else { + throw ADError.InvalidToken + } + + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + + 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") + } + + if response.statusCode >= 200, response.statusCode <= 299 { + return data + } else if response.statusCode == 401 { + deleteTokens() + 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).") + } + } + + // 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 + } + + components.queryItems = [ + URLQueryItem(name: "agent", value: appName) + ] + queryItems + + if let url = components.url { + return url + } else { + throw ADError.InvalidUrl + } + } + + // Adds a magnet link to the user's AD account + public func addMagnet(magnetLink: String) async throws -> Int { + var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload")) + request.httpMethod = "POST" + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + var bodyComponents = URLComponents() + bodyComponents.queryItems = [ + URLQueryItem(name: "magnets[]", value: magnetLink) + ] + + request.httpBody = bodyComponents.query?.data(using: .utf8) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data + + if let magnet = rawResponse.magnets[safe: 0] { + return magnet.id + } else { + throw ADError.InvalidResponse + } + } + + public func fetchMagnetStatus(magnetId: Int, selectedIndex: Int?) async throws -> String { + let queryItems = [ + URLQueryItem(name: "id", value: String(magnetId)) + ] + var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems)) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data + + // Better to fetch no link at all than the wrong link + if let linkWrapper = rawResponse.magnets.links[safe: selectedIndex ?? -1] { + return linkWrapper.link + } else { + throw ADError.EmptyTorrents + } + } + + public func unlockLink(lockedLink: String) async throws -> String { + let queryItems = [ + URLQueryItem(name: "link", value: lockedLink) + ] + var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/link/unlock", queryItems: queryItems)) + print(request) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data + + return rawResponse.link + } + + public func instantAvailability(hashes: [String]) async throws -> [IA] { + let queryItems = hashes.map { URLQueryItem(name: "magnets[]", value: $0) } + 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 + IAFile(id: index, fileName: magnetFile.name) + } + + return IA( + hash: magnetResp.hash, + expiryTimeStamp: Date().timeIntervalSince1970 + 300, + files: files + ) + } + + return availableHashes + } +} diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 92bd60a..c49f65d 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -161,7 +161,7 @@ public class RealDebrid { } // Wrapper request function which matches the responses and returns data - @discardableResult public func performRequest(request: inout URLRequest, requestName: String) async throws -> Data { + @discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data { guard let token = await fetchToken() else { throw RDError.InvalidToken } @@ -186,7 +186,7 @@ public class RealDebrid { // Checks if the magnet is streamable on RD // Currently does not work for batch links - public func instantAvailability(magnetHashes: [String]) async throws -> [RealDebrid.IA] { + public func instantAvailability(magnetHashes: [String]) async throws -> [IA] { var availableHashes: [RealDebrid.IA] = [] var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!) diff --git a/Ferrite/Extensions/Array.swift b/Ferrite/Extensions/Array.swift new file mode 100644 index 0000000..b4ca668 --- /dev/null +++ b/Ferrite/Extensions/Array.swift @@ -0,0 +1,26 @@ +// +// Array.swift +// Ferrite +// +// Created by Brian Dashore on 11/26/22. +// + +import Foundation + +extension Set: RawRepresentable where Element: Codable { + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode(Set.self, from: data) + else { return nil } + self = result + } + + public var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } +} diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index b47adf9..032567f 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -23,6 +23,16 @@ extension View { )) } + // From https://github.com/siteline/SwiftUI-Introspect/pull/129 + public func introspectSearchController(customize: @escaping (UISearchController) -> Void) -> some View { + introspectNavigationController { navigationController in + let navigationBar = navigationController.navigationBar + if let searchController = navigationBar.topItem?.searchController { + customize(searchController) + } + } + } + // MARK: Modifiers func conditionalContextMenu(id: some Hashable, diff --git a/Ferrite/Models/AllDebridModels.swift b/Ferrite/Models/AllDebridModels.swift new file mode 100644 index 0000000..a253027 --- /dev/null +++ b/Ferrite/Models/AllDebridModels.swift @@ -0,0 +1,157 @@ +// +// AllDebridModels.swift +// Ferrite +// +// Created by Brian Dashore on 11/25/22. +// + +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 + struct ADResponse: Codable { + let status: String + let data: ADData + } + + // MARK: - PinResponse + + struct PinResponse: Codable { + let pin, check: String + let expiresIn: Int + let userURL, baseURL, checkURL: String + + enum CodingKeys: String, CodingKey { + case pin, check + case expiresIn = "expires_in" + case userURL = "user_url" + case baseURL = "base_url" + case checkURL = "check_url" + } + } + + // MARK: - ApiKeyResponse + + struct ApiKeyResponse: Codable { + let apikey: String + let activated: Bool + let expiresIn: Int + + enum CodingKeys: String, CodingKey { + case apikey, activated + case expiresIn = "expires_in" + } + } + + // MARK: - AddMagnetResponse + + struct AddMagnetResponse: Codable { + let magnets: [AddMagnetData] + } + + // MARK: - AddMagnetData + + internal struct AddMagnetData: Codable { + let magnet, hash, name, filenameOriginal: String + let size: Int + let ready: Bool + let id: Int + + enum CodingKeys: String, CodingKey { + case magnet, hash, name + case filenameOriginal = "filename_original" + case size, ready, id + } + } + + // MARK: - MagnetStatusResponse + + struct MagnetStatusResponse: Codable { + let magnets: MagnetStatusData + } + + // MARK: - MagnetStatusData + + internal struct MagnetStatusData: Codable { + let id: Int + let filename: String + let size: Int + let hash, status: String + let statusCode, downloaded, uploaded, seeders: Int + let downloadSpeed, processingPerc, uploadSpeed, uploadDate: Int + let completionDate: Int + let links: [MagnetStatusLink] + let type: String + let notified: Bool + let version: Int + } + + // MARK: - MagnetStatusLink + + // Abridged for required parameters + internal struct MagnetStatusLink: Codable { + let link: String + let filename: String + let size: Int + } + + // MARK: - UnlockLinkResponse + + // Abridged for required parameters + struct UnlockLinkResponse: Codable { + let link: String + } + + // MARK: - InstantAvailabilityResponse + + struct InstantAvailabilityResponse: Codable { + let magnets: [InstantAvailabilityMagnet] + } + + // MARK: - IAMagnetResponse + + internal struct InstantAvailabilityMagnet: Codable { + let magnet, hash: String + let instant: Bool + let files: [InstantAvailabilityFile]? + } + + // MARK: - IAFileResponse + + internal struct InstantAvailabilityFile: Codable { + let name: String + + enum CodingKeys: String, CodingKey { + case name = "n" + } + } + + // MARK: - InstantAvailablity client side structures + + struct IA: Codable, Hashable { + let hash: String + let expiryTimeStamp: Double + var files: [IAFile] + } + + struct IAFile: Codable, Hashable { + let id: Int + let fileName: String + } +} diff --git a/Ferrite/Models/DebridManagerModels.swift b/Ferrite/Models/DebridManagerModels.swift new file mode 100644 index 0000000..1280222 --- /dev/null +++ b/Ferrite/Models/DebridManagerModels.swift @@ -0,0 +1,32 @@ +// +// DebridManagerModels.swift +// Ferrite +// +// Created by Brian Dashore on 11/27/22. +// + +import Foundation + +// MARK: - Universal IA enum (IA = InstantAvailability) + +public enum IAStatus: Codable, Hashable, Sendable { + case full + case partial + case none +} + +// MARK: - Enum for debrid differentiation. 0 is nil + +public enum DebridType: Int, Codable, Hashable, CaseIterable { + case realDebrid = 1 + case allDebrid = 2 + + func toString(abbreviated: Bool = false) -> String { + switch self { + case .realDebrid: + return abbreviated ? "RD" : "RealDebrid" + case .allDebrid: + return abbreviated ? "AD" : "AllDebrid" + } + } +} diff --git a/Ferrite/Models/GithubModels.swift b/Ferrite/Models/GithubModels.swift index 242d5e0..08e007e 100644 --- a/Ferrite/Models/GithubModels.swift +++ b/Ferrite/Models/GithubModels.swift @@ -7,8 +7,8 @@ import Foundation -extension Github { - public struct Release: Codable, Hashable, Sendable { +public extension Github { + struct Release: Codable, Hashable, Sendable { let htmlUrl: String let tagName: String diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index 114a202..3a9e1a0 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -8,9 +8,11 @@ import Foundation -extension RealDebrid { +public extension RealDebrid { // MARK: - Errors - public enum RDError: Error { + + // TODO: Hybridize debrid errors in one structure + enum RDError: Error { case InvalidUrl case InvalidPostBody case InvalidResponse @@ -23,7 +25,7 @@ extension RealDebrid { // MARK: - device code endpoint - public struct DeviceCodeResponse: Codable, Sendable { + struct DeviceCodeResponse: Codable, Sendable { let deviceCode, userCode: String let interval, expiresIn: Int let verificationURL, directVerificationURL: String @@ -40,7 +42,7 @@ extension RealDebrid { // MARK: - device credentials endpoint - public struct DeviceCredentialsResponse: Codable, Sendable { + struct DeviceCredentialsResponse: Codable, Sendable { let clientID, clientSecret: String? enum CodingKeys: String, CodingKey { @@ -51,7 +53,7 @@ extension RealDebrid { // MARK: - token endpoint - public struct TokenResponse: Codable, Sendable { + struct TokenResponse: Codable, Sendable { let accessToken: String let expiresIn: Int let refreshToken, tokenType: String @@ -67,7 +69,7 @@ extension RealDebrid { // MARK: - instantAvailability endpoint // Thanks Skitty! - public struct InstantAvailabilityResponse: Codable, Sendable { + struct InstantAvailabilityResponse: Codable, Sendable { var data: InstantAvailabilityData? public init(from decoder: Decoder) throws { @@ -79,55 +81,49 @@ extension RealDebrid { } } - struct InstantAvailabilityData: Codable, Sendable { + internal struct InstantAvailabilityData: Codable, Sendable { var rd: [[String: InstantAvailabilityInfo]] } - struct InstantAvailabilityInfo: Codable, Sendable { + internal struct InstantAvailabilityInfo: Codable, Sendable { var filename: String var filesize: Int } // MARK: - Instant Availability client side structures - public struct IA: Codable, Hashable, Sendable { + struct IA: Codable, Hashable, Sendable { let hash: String let expiryTimeStamp: Double var files: [IAFile] = [] var batches: [IABatch] = [] } - public struct IABatch: Codable, Hashable, Sendable { + struct IABatch: Codable, Hashable, Sendable { let files: [IABatchFile] } - public struct IABatchFile: Codable, Hashable, Sendable { + struct IABatchFile: Codable, Hashable, Sendable { let id: Int let fileName: String } - public struct IAFile: Codable, Hashable, Sendable { + struct IAFile: Codable, Hashable, Sendable { let name: String let batchIndex: Int let batchFileIndex: Int } - public enum IAStatus: Codable, Hashable, Sendable { - case full - case partial - case none - } - // MARK: - addMagnet endpoint - public struct AddMagnetResponse: Codable, Sendable { + struct AddMagnetResponse: Codable, Sendable { let id: String let uri: String } // MARK: - torrentInfo endpoint - struct TorrentInfoResponse: Codable, Sendable { + internal struct TorrentInfoResponse: Codable, Sendable { let id, filename, originalFilename, hash: String let bytes, originalBytes: Int let host: String @@ -148,13 +144,13 @@ extension RealDebrid { } } - struct TorrentInfoFile: Codable, Sendable { + internal struct TorrentInfoFile: Codable, Sendable { let id: Int let path: String let bytes, selected: Int } - public struct UserTorrentsResponse: Codable, Sendable { + struct UserTorrentsResponse: Codable, Sendable { let id, filename, hash: String let bytes: Int let host: String @@ -167,7 +163,7 @@ extension RealDebrid { // MARK: - unrestrictLink endpoint - struct UnrestrictLinkResponse: Codable, Sendable { + internal struct UnrestrictLinkResponse: Codable, Sendable { let id, filename: String let mimeType: String? let filesize: Int @@ -187,7 +183,7 @@ extension RealDebrid { // MARK: - User downloads list - public struct UserDownloadsResponse: Codable, Sendable { + struct UserDownloadsResponse: Codable, Sendable { let id, filename: String let mimeType: String? let filesize: Int diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 175694a..6338d02 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -13,39 +13,84 @@ public class DebridManager: ObservableObject { // Linked classes var toastModel: ToastViewModel? let realDebrid: RealDebrid = .init() + let allDebrid: AllDebrid = .init() // UI Variables @Published var showWebView: Bool = false @Published var showLoadingProgress: Bool = false // Service agnostic variables - var currentDebridTask: Task? - - // RealDebrid auth variables - @Published var realDebridEnabled: Bool = false { + @Published var enabledDebrids: Set = [] { didSet { - UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled") + UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray") } } + @Published var selectedDebridType: DebridType? { + didSet { + UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService") + } + } + + var currentDebridTask: Task? + var downloadUrl: String = "" + var authUrl: String = "" + + // RealDebrid auth variables @Published var realDebridAuthProcessing: Bool = false - var realDebridAuthUrl: String = "" // RealDebrid fetch variables @Published var realDebridIAValues: [RealDebrid.IA] = [] - var realDebridDownloadUrl: String = "" @Published var showDeleteAlert: Bool = false - // TODO: Switch to an individual item based sheet system to remove these variables var selectedRealDebridItem: RealDebrid.IA? var selectedRealDebridFile: RealDebrid.IAFile? var selectedRealDebridID: String? + // AllDebrid auth variables + @Published var allDebridAuthProcessing: Bool = false + + // AllDebrid fetch variables + @Published var allDebridIAValues: [AllDebrid.IA] = [] + + var selectedAllDebridItem: AllDebrid.IA? + var selectedAllDebridFile: AllDebrid.IAFile? + init() { - realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled") + 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") + selectedDebridType = DebridType(rawValue: rawPreferredService) + + // 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 + // 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") + } + + let allDebridEnabled = UserDefaults.standard.bool(forKey: "AllDebrid.Enabled") + if allDebridEnabled { + enabledDebrids.insert(.allDebrid) + UserDefaults.standard.set(false, forKey: "AllDebrid.Enabled") + } + } + + // Common function to populate hashes for debrid services public func populateDebridHashes(_ resultHashes: [String]) async { do { let now = Date() @@ -53,76 +98,135 @@ 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 sendHashes = resultHashes.filter { hash in - if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }) { + if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }), enabledDebrids.contains(.realDebrid) { if now.timeIntervalSince1970 > realDebridIAValues[IAIndex].expiryTimeStamp { realDebridIAValues.remove(at: IAIndex) return true } else { return false } + } else if let IAIndex = allDebridIAValues.firstIndex(where: { $0.hash == hash }), enabledDebrids.contains(.allDebrid) { + if now.timeIntervalSince1970 > allDebridIAValues[IAIndex].expiryTimeStamp { + allDebridIAValues.remove(at: IAIndex) + return true + } else { + return false + } } else { return true } } if !sendHashes.isEmpty { - let fetchedIAValues = try await realDebrid.instantAvailability(magnetHashes: sendHashes) + if enabledDebrids.contains(.realDebrid) { + let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnetHashes: sendHashes) + realDebridIAValues += fetchedRealDebridIA + } - realDebridIAValues += fetchedIAValues + if enabledDebrids.contains(.allDebrid) { + let fetchedAllDebridIA = try await allDebrid.instantAvailability(hashes: sendHashes) + allDebridIAValues += fetchedAllDebridIA + } } } catch { let error = error as NSError if error.code != -999 { - toastModel?.updateToastDescription("RealDebrid hash error: \(error)") + toastModel?.updateToastDescription("Hash population error: \(error)") } - print("RealDebrid hash error: \(error)") + print("Hash population error: \(error)") } } - public func matchSearchResult(result: SearchResult?) -> RealDebrid.IAStatus { + // Common function to match search results with a provided debrid service + public func matchSearchResult(result: SearchResult?) -> IAStatus { guard let result else { return .none } - guard let debridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { - return .none - } + switch selectedDebridType { + case .realDebrid: + guard let realDebridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { + return .none + } - if debridMatch.batches.isEmpty { - return .full - } else { - return .partial + if realDebridMatch.batches.isEmpty { + return .full + } else { + return .partial + } + case .allDebrid: + guard let allDebridMatch = allDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { + return .none + } + + if allDebridMatch.files.count > 1 { + return .partial + } else { + return .full + } + case .none: + return .none } } - public func setSelectedRdResult(result: SearchResult) -> Bool { + public func selectDebridResult(result: SearchResult) -> Bool { guard let magnetHash = result.magnetHash else { toastModel?.updateToastDescription("Could not find the torrent magnet hash") return false } - if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) { - selectedRealDebridItem = realDebridItem - return true - } else { - toastModel?.updateToastDescription("Could not find the associated RealDebrid entry for magnet hash \(magnetHash)") + switch selectedDebridType { + case .realDebrid: + if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) { + selectedRealDebridItem = realDebridItem + return true + } else { + toastModel?.updateToastDescription("Could not find the associated RealDebrid entry for magnet hash \(magnetHash)") + return false + } + case .allDebrid: + if let allDebridItem = allDebridIAValues.first(where: { magnetHash == $0.hash }) { + selectedAllDebridItem = allDebridItem + return true + } else { + toastModel?.updateToastDescription("Could not find the associated AllDebrid entry for magnet hash \(magnetHash)") + return false + } + case .none: return false } } - public func authenticateRd() async { + // MARK: - Authentication UI linked functions + + // Common function to delegate what debrid service to authenticate with + public func authenticateDebrid(debridType: DebridType) async { + switch debridType { + case .realDebrid: + await authenticateRd() + enabledDebrids.insert(.realDebrid) + case .allDebrid: + await authenticateAd() + enabledDebrids.insert(.allDebrid) + } + + // Automatically sets the preferred debrid service if only one login is provided + if enabledDebrids.count == 1 { + selectedDebridType = enabledDebrids.first + } + } + + private func authenticateRd() async { do { realDebridAuthProcessing = true let verificationResponse = try await realDebrid.getVerificationInfo() - realDebridAuthUrl = verificationResponse.directVerificationURL + authUrl = verificationResponse.directVerificationURL showWebView.toggle() try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode) - - realDebridEnabled = true } catch { toastModel?.updateToastDescription("RealDebrid authentication error: \(error)") realDebrid.authTask?.cancel() @@ -131,10 +235,44 @@ public class DebridManager: ObservableObject { } } - public func logoutRd() async { + private func authenticateAd() async { + do { + allDebridAuthProcessing = true + let pinResponse = try await allDebrid.getPinInfo() + + authUrl = pinResponse.userURL + showWebView.toggle() + + try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin) + } catch { + toastModel?.updateToastDescription("AllDebrid authentication error: \(error)") + allDebrid.authTask?.cancel() + + print("AllDebrid authentication error: \(error)") + } + } + + // 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() + } + + // Automatically resets the preferred debrid service if it was set to the logged out service + if selectedDebridType == debridType { + selectedDebridType = nil + } + } + + private func logoutRd() async { do { try await realDebrid.deleteTokens() - realDebridEnabled = false + enabledDebrids.remove(.realDebrid) realDebridAuthProcessing = false } catch { toastModel?.updateToastDescription("RealDebrid logout error: \(error)") @@ -143,7 +281,18 @@ public class DebridManager: ObservableObject { } } - public func fetchRdDownload(searchResult: SearchResult) async { + private func logoutAd() { + allDebrid.deleteTokens() + enabledDebrids.remove(.allDebrid) + allDebridAuthProcessing = false + + toastModel?.updateToastDescription("Please manually delete the AllDebrid API key", newToastType: .info) + } + + // MARK: - Debrid fetch UI linked functions + + // Common function to delegate what debrid service to fetch from + public func fetchDebridDownload(searchResult: SearchResult) async { defer { currentDebridTask = nil showLoadingProgress = false @@ -153,11 +302,24 @@ public class DebridManager: ObservableObject { guard let magnetLink = searchResult.magnetLink else { toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") - print("RealDebrid error: Invalid magnet link") + print("Debrid error: Invalid magnet link") return } + switch selectedDebridType { + case .realDebrid: + await fetchRdDownload(magnetLink: magnetLink) + case .allDebrid: + await fetchAdDownload(magnetLink: magnetLink) + case .none: + break + } + } + + func fetchRdDownload(magnetLink: String) async { + print("Called RD Download function!") + do { var fileIds: [Int] = [] @@ -178,11 +340,11 @@ public class DebridManager: ObservableObject { { let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink } if let existingLink = existingLinks[safe: 0]?.download { - realDebridDownloadUrl = existingLink + downloadUrl = existingLink } else { let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) - realDebridDownloadUrl = downloadLink + downloadUrl = downloadLink } } else { @@ -192,10 +354,13 @@ public class DebridManager: ObservableObject { 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 torrentLink = try await realDebrid.torrentInfo( + debridID: realDebridId, + selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0 + ) let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) - realDebridDownloadUrl = downloadLink + downloadUrl = downloadLink } else { toastModel?.updateToastDescription("Could not cache this torrent. Aborting.") } @@ -223,11 +388,32 @@ public class DebridManager: ObservableObject { } } - public func deleteRdTorrent() async { + func deleteRdTorrent() async { if let realDebridId = selectedRealDebridID { try? await realDebrid.deleteTorrent(debridID: realDebridId) } selectedRealDebridID = nil } + + func fetchAdDownload(magnetLink: String) async { + do { + let magnetID = try await allDebrid.addMagnet(magnetLink: magnetLink) + let lockedLink = try await allDebrid.fetchMagnetStatus( + magnetId: magnetID, + selectedIndex: selectedAllDebridFile?.id ?? 0 + ) + let unlockedLink = try await allDebrid.unlockLink(lockedLink: lockedLink) + + downloadUrl = unlockedLink + } catch { + let error = error as NSError + switch error.code { + case -999: + toastModel?.updateToastDescription("Download cancelled", newToastType: .info) + default: + toastModel?.updateToastDescription("AllDebrid download error: \(error)") + } + } + } } diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 5554516..92b8c75 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -12,8 +12,6 @@ import SwiftUI import SwiftyJSON class ScrapingViewModel: ObservableObject { - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false - // Link the toast view model for single-directional communication var toastModel: ToastViewModel? let byteCountFormatter: ByteCountFormatter = .init() diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift deleted file mode 100644 index 0a70ac0..0000000 --- a/Ferrite/Views/BatchChoiceView.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// BatchChoiceView.swift -// Ferrite -// -// Created by Brian Dashore on 7/24/22. -// - -import SwiftUI - -struct BatchChoiceView: View { - @EnvironmentObject var debridManager: DebridManager - @EnvironmentObject var scrapingModel: ScrapingViewModel - @EnvironmentObject var navModel: NavigationViewModel - - let backgroundContext = PersistenceController.shared.backgroundContext - - var body: some View { - NavView { - List { - ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in - Button(file.name) { - debridManager.selectedRealDebridFile = file - - if let searchResult = navModel.selectedSearchResult { - debridManager.currentDebridTask = Task { - await debridManager.fetchRdDownload(searchResult: searchResult) - - if !debridManager.realDebridDownloadUrl.isEmpty { - // The download may complete before this sheet dismisses - try? await Task.sleep(seconds: 1) - navModel.selectedBatchTitle = file.name - navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name) - navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) - } - - debridManager.selectedRealDebridFile = nil - debridManager.selectedRealDebridItem = nil - } - } - - navModel.currentChoiceSheet = nil - } - .backport.tint(.primary) - } - } - .listStyle(.insetGrouped) - .navigationTitle("Select a file") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - navModel.currentChoiceSheet = nil - - Task { - try? await Task.sleep(seconds: 1) - debridManager.selectedRealDebridItem = nil - } - } - } - } - } - } -} - -struct BatchChoiceView_Previews: PreviewProvider { - static var previews: some View { - BatchChoiceView() - } -} diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift new file mode 100644 index 0000000..f38ddf5 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift @@ -0,0 +1,37 @@ +// +// DebridChoiceView.swift +// Ferrite +// +// Created by Brian Dashore on 11/26/22. +// + +import SwiftUI + +struct DebridChoiceView: View { + @EnvironmentObject var debridManager: DebridManager + + var body: some View { + Menu { + Picker("", selection: $debridManager.selectedDebridType) { + Text("None") + .tag(nil as DebridType?) + + ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in + if debridManager.enabledDebrids.contains(debridType) { + Text(debridType.toString()) + .tag(DebridType?.some(debridType)) + } + } + } + } label: { + Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid") + } + .animation(.none) + } +} + +struct DebridChoiceView_Previews: PreviewProvider { + static var previews: some View { + DebridChoiceView() + } +} diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift new file mode 100644 index 0000000..6d1bdcc --- /dev/null +++ b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift @@ -0,0 +1,36 @@ +// +// DebridLabelView.swift +// Ferrite +// +// Created by Brian Dashore on 11/27/22. +// + +import SwiftUI + +struct DebridLabelView: View { + @EnvironmentObject var debridManager: DebridManager + + var result: SearchResult + + let debridAbbreviation: String + + var body: some View { + Text(debridAbbreviation) + .fontWeight(.bold) + .padding(2) + .background { + Group { + switch debridManager.matchSearchResult(result: result) { + case .full: + Color.green + case .partial: + Color.orange + case .none: + Color.red + } + } + .cornerRadius(4) + .opacity(0.5) + } + } +} diff --git a/Ferrite/Views/LibraryViews/BookmarksView.swift b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift similarity index 95% rename from Ferrite/Views/LibraryViews/BookmarksView.swift rename to Ferrite/Views/ComponentViews/Library/BookmarksView.swift index d668b62..85cf526 100644 --- a/Ferrite/Views/LibraryViews/BookmarksView.swift +++ b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift @@ -13,8 +13,6 @@ struct BookmarksView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false - let backgroundContext = PersistenceController.shared.backgroundContext var bookmarks: FetchedResults @@ -54,7 +52,7 @@ struct BookmarksView: View { } } .onAppear { - if realDebridEnabled { + if debridManager.enabledDebrids.count > 0 { viewTask = Task { let hashes = bookmarks.compactMap(\.magnetHash) await debridManager.populateDebridHashes(hashes) diff --git a/Ferrite/Views/LibraryViews/HistoryActionsView.swift b/Ferrite/Views/ComponentViews/Library/HistoryActionsView.swift similarity index 100% rename from Ferrite/Views/LibraryViews/HistoryActionsView.swift rename to Ferrite/Views/ComponentViews/Library/HistoryActionsView.swift diff --git a/Ferrite/Views/LibraryViews/HistoryButtonView.swift b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift similarity index 95% rename from Ferrite/Views/LibraryViews/HistoryButtonView.swift rename to Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift index 9399b16..c05f1fd 100644 --- a/Ferrite/Views/LibraryViews/HistoryButtonView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift @@ -22,11 +22,11 @@ struct HistoryButtonView: View { if let url = entry.url { if url.starts(with: "https://") { Task { - debridManager.realDebridDownloadUrl = url + debridManager.downloadUrl = url navModel.runDebridAction(urlString: url) if navModel.currentChoiceSheet != .magnet { - debridManager.realDebridDownloadUrl = "" + debridManager.downloadUrl = "" } } } else { diff --git a/Ferrite/Views/LibraryViews/HistoryView.swift b/Ferrite/Views/ComponentViews/Library/HistoryView.swift similarity index 100% rename from Ferrite/Views/LibraryViews/HistoryView.swift rename to Ferrite/Views/ComponentViews/Library/HistoryView.swift diff --git a/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift similarity index 88% rename from Ferrite/Views/SearchResultViews/SearchResultButtonView.swift rename to Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index 9d9d984..5943e48 100644 --- a/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -13,12 +13,11 @@ struct SearchResultButtonView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false - var result: SearchResult @State private var runOnce = false @State var existingBookmark: Bookmark? = nil + @State private var showConfirmation = false var body: some View { Button { @@ -28,22 +27,22 @@ struct SearchResultButtonView: View { switch debridManager.matchSearchResult(result: result) { case .full: - if debridManager.setSelectedRdResult(result: result) { + if debridManager.selectDebridResult(result: result) { debridManager.currentDebridTask = Task { - await debridManager.fetchRdDownload(searchResult: result) + await debridManager.fetchDebridDownload(searchResult: result) - if !debridManager.realDebridDownloadUrl.isEmpty { - navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl) - navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) + if !debridManager.downloadUrl.isEmpty { + navModel.addToHistory(name: result.title, source: result.source, url: debridManager.downloadUrl) + navModel.runDebridAction(urlString: debridManager.downloadUrl) if navModel.currentChoiceSheet != .magnet { - debridManager.realDebridDownloadUrl = "" + debridManager.downloadUrl = "" } } } } case .partial: - if debridManager.setSelectedRdResult(result: result) { + if debridManager.selectDebridResult(result: result) { navModel.currentChoiceSheet = .batch } case .none: @@ -58,7 +57,7 @@ struct SearchResultButtonView: View { .fixedSize(horizontal: false, vertical: true) .lineLimit(4) - SearchResultRDView(result: result) + SearchResultInfoView(result: result) } .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift new file mode 100644 index 0000000..10072bd --- /dev/null +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift @@ -0,0 +1,43 @@ +// +// SearchResultRDView.swift +// Ferrite +// +// Created by Brian Dashore on 7/26/22. +// + +import SwiftUI + +struct SearchResultInfoView: View { + @EnvironmentObject var debridManager: DebridManager + + var result: SearchResult + + var body: some View { + HStack { + Text(result.source) + + Spacer() + + if let seeders = result.seeders { + Text("S: \(seeders)") + } + + if let leechers = result.leechers { + Text("L: \(leechers)") + } + + if let size = result.size { + Text(size) + } + + if debridManager.selectedDebridType == .realDebrid { + DebridLabelView(result: result, debridAbbreviation: "RD") + } + + if debridManager.selectedDebridType == .allDebrid { + DebridLabelView(result: result, debridAbbreviation: "AD") + } + } + .font(.caption) + } +} diff --git a/Ferrite/Views/SettingsViews/BackupsView.swift b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift similarity index 100% rename from Ferrite/Views/SettingsViews/BackupsView.swift rename to Ferrite/Views/ComponentViews/Settings/BackupsView.swift diff --git a/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift b/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift similarity index 100% rename from Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift rename to Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsAppVersionView.swift similarity index 100% rename from Ferrite/Views/SettingsViews/SettingsAppVersionView.swift rename to Ferrite/Views/ComponentViews/Settings/SettingsAppVersionView.swift diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift similarity index 100% rename from Ferrite/Views/SettingsViews/SettingsSourceListView.swift rename to Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift similarity index 100% rename from Ferrite/Views/SettingsViews/SourceListEditorView.swift rename to Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift diff --git a/Ferrite/Views/SourceViews/Buttons/InstalledSourceButtonView.swift b/Ferrite/Views/ComponentViews/Source/Buttons/InstalledSourceButtonView.swift similarity index 100% rename from Ferrite/Views/SourceViews/Buttons/InstalledSourceButtonView.swift rename to Ferrite/Views/ComponentViews/Source/Buttons/InstalledSourceButtonView.swift diff --git a/Ferrite/Views/SourceViews/Buttons/SourceCatalogButtonView.swift b/Ferrite/Views/ComponentViews/Source/Buttons/SourceCatalogButtonView.swift similarity index 100% rename from Ferrite/Views/SourceViews/Buttons/SourceCatalogButtonView.swift rename to Ferrite/Views/ComponentViews/Source/Buttons/SourceCatalogButtonView.swift diff --git a/Ferrite/Views/SourceViews/Buttons/SourceUpdateButtonView.swift b/Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift similarity index 100% rename from Ferrite/Views/SourceViews/Buttons/SourceUpdateButtonView.swift rename to Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift diff --git a/Ferrite/Views/SourceViews/SourceSettingsView.swift b/Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift similarity index 100% rename from Ferrite/Views/SourceViews/SourceSettingsView.swift rename to Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 9a117fa..3d7c495 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -14,8 +14,6 @@ struct ContentView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var sourceManager: SourceManager - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false - @FetchRequest( entity: Source.entity(), sortDescriptors: [] @@ -64,6 +62,7 @@ struct ContentView: View { Image(systemName: "chevron.down") } .foregroundColor(.primary) + .animation(.none) Spacer() } @@ -73,6 +72,7 @@ struct ContentView: View { SearchResultsView() } .navigationTitle("Search") + .navigationBarTitleDisplayMode(navModel.isEditingSearch || navModel.isSearching ? .inline : .automatic) .navigationSearchBar { SearchBar("Search", text: $scrapingModel.searchText, @@ -86,8 +86,9 @@ struct ContentView: View { let sources = sourceManager.fetchInstalledSources() await scrapingModel.scanSources(sources: sources) - if realDebridEnabled, !scrapingModel.searchResults.isEmpty { + if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { debridManager.realDebridIAValues = [] + debridManager.allDebridIAValues = [] await debridManager.populateDebridHashes( scrapingModel.searchResults.compactMap(\.magnetHash) @@ -106,6 +107,14 @@ struct ContentView: View { scrapingModel.searchText = "" } } + .introspectSearchController { searchController in + searchController.hidesNavigationBarDuringPresentation = false + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + DebridChoiceView() + } + } } } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 39c475f..87e3e1f 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -71,7 +71,10 @@ struct LibraryView: View { HStack { EditButton() - if selectedSegment == .history { + switch selectedSegment { + case .bookmarks: + DebridChoiceView() + case .history: HistoryActionsView() } } diff --git a/Ferrite/Views/SearchResultViews/SearchResultRDView.swift b/Ferrite/Views/SearchResultViews/SearchResultRDView.swift deleted file mode 100644 index 8c51280..0000000 --- a/Ferrite/Views/SearchResultViews/SearchResultRDView.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// SearchResultRDView.swift -// Ferrite -// -// Created by Brian Dashore on 7/26/22. -// - -import SwiftUI - -struct SearchResultRDView: View { - @EnvironmentObject var debridManager: DebridManager - - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false - - var result: SearchResult - - var body: some View { - HStack { - Text(result.source) - - Spacer() - - if let seeders = result.seeders { - Text("S: \(seeders)") - } - - if let leechers = result.leechers { - Text("L: \(leechers)") - } - - if let size = result.size { - Text(size) - } - - if realDebridEnabled { - Text("RD") - .fontWeight(.bold) - .padding(2) - .background { - Group { - switch debridManager.matchSearchResult(result: result) { - case .full: - Color.green - case .partial: - Color.orange - case .none: - Color.red - } - } - .cornerRadius(4) - .opacity(0.5) - } - } - } - .font(.caption) - } -} diff --git a/Ferrite/Views/SearchResultsView.swift b/Ferrite/Views/SearchResultsView.swift index 110813e..df775a7 100644 --- a/Ferrite/Views/SearchResultsView.swift +++ b/Ferrite/Views/SearchResultsView.swift @@ -10,6 +10,7 @@ import SwiftUI struct SearchResultsView: View { @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager var body: some View { List { diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 080445f..b39b316 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -24,22 +24,36 @@ struct SettingsView: View { Form { Section(header: InlineHeader("Debrid Services")) { HStack { - Text("Real Debrid") + Text("RealDebrid") Spacer() Button { Task { - if debridManager.realDebridEnabled { - await debridManager.logoutRd() + if debridManager.enabledDebrids.contains(.realDebrid) { + await debridManager.logoutDebrid(debridType: .realDebrid) } else if !debridManager.realDebridAuthProcessing { - await debridManager.authenticateRd() + await debridManager.authenticateDebrid(debridType: .realDebrid) } } } label: { - Text(debridManager.realDebridEnabled ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login")) - .foregroundColor(debridManager.realDebridEnabled ? .red : .blue) - .onChange(of: debridManager.realDebridEnabled) { changed in - print("Debrid enabled changed to \(changed)") + Text(debridManager.enabledDebrids.contains(.realDebrid) ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login")) + .foregroundColor(debridManager.enabledDebrids.contains(.realDebrid) ? .red : .blue) + } + } + + HStack { + Text("AllDebrid") + Spacer() + Button { + Task { + if debridManager.enabledDebrids.contains(.allDebrid) { + await debridManager.logoutDebrid(debridType: .allDebrid) + } else if !debridManager.realDebridAuthProcessing { + await debridManager.authenticateDebrid(debridType: .allDebrid) } + } + } label: { + Text(debridManager.enabledDebrids.contains(.allDebrid) ? "Logout" : (debridManager.allDebridAuthProcessing ? "Processing" : "Login")) + .foregroundColor(debridManager.enabledDebrids.contains(.allDebrid) ? .red : .blue) } } } @@ -49,7 +63,7 @@ struct SettingsView: View { } Section(header: Text("Default actions")) { - if debridManager.realDebridEnabled { + if debridManager.enabledDebrids.count > 0 { NavigationLink( destination: DebridActionPickerView(), label: { @@ -118,7 +132,7 @@ struct SettingsView: View { } } .sheet(isPresented: $debridManager.showWebView) { - LoginWebView(url: URL(string: debridManager.realDebridAuthUrl)!) + LoginWebView(url: URL(string: debridManager.authUrl)!) } .navigationTitle("Settings") } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift new file mode 100644 index 0000000..9385702 --- /dev/null +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -0,0 +1,100 @@ +// +// BatchChoiceView.swift +// Ferrite +// +// Created by Brian Dashore on 7/24/22. +// + +import SwiftUI + +struct BatchChoiceView: View { + @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var scrapingModel: ScrapingViewModel + @EnvironmentObject var navModel: NavigationViewModel + + let backgroundContext = PersistenceController.shared.backgroundContext + + var body: some View { + NavView { + List { + switch debridManager.selectedDebridType { + case .realDebrid: + ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in + Button(file.name) { + debridManager.selectedRealDebridFile = file + + queueCommonDownload(fileName: file.name) + } + } + case .allDebrid: + ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in + Button(file.fileName) { + debridManager.selectedAllDebridFile = file + + queueCommonDownload(fileName: file.fileName) + } + } + case .none: + EmptyView() + } + } + .backport.tint(.primary) + .listStyle(.insetGrouped) + .inlinedList() + .navigationTitle("Select a file") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + navModel.currentChoiceSheet = nil + + Task { + try? await Task.sleep(seconds: 1) + debridManager.selectedRealDebridItem = nil + } + } + } + } + } + } + + // Common function to communicate betwen VMs and queue/display a download + func queueCommonDownload(fileName: String) { + if let searchResult = navModel.selectedSearchResult { + debridManager.currentDebridTask = Task { + await debridManager.fetchDebridDownload(searchResult: searchResult) + + if !debridManager.downloadUrl.isEmpty { + try? await Task.sleep(seconds: 1) + navModel.selectedBatchTitle = fileName + navModel.addToHistory( + name: searchResult.title, + source: searchResult.source, + url: debridManager.downloadUrl, + subName: fileName + ) + navModel.runDebridAction(urlString: debridManager.downloadUrl) + } + + switch debridManager.selectedDebridType { + case .realDebrid: + debridManager.selectedAllDebridFile = nil + debridManager.selectedAllDebridItem = nil + case .allDebrid: + debridManager.selectedRealDebridFile = nil + debridManager.selectedRealDebridItem = nil + case .none: + break + } + } + } + + navModel.currentChoiceSheet = nil + } +} + +struct BatchChoiceView_Previews: PreviewProvider { + static var previews: some View { + BatchChoiceView() + } +} diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/SheetViews/MagnetChoiceView.swift similarity index 93% rename from Ferrite/Views/MagnetChoiceView.swift rename to Ferrite/Views/SheetViews/MagnetChoiceView.swift index 1ad0c77..dd3c1d3 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/SheetViews/MagnetChoiceView.swift @@ -37,22 +37,22 @@ struct MagnetChoiceView: View { } } - if !debridManager.realDebridDownloadUrl.isEmpty { + if !debridManager.downloadUrl.isEmpty { Section(header: "Real Debrid options") { ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .outplayer) + navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer) } ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .vlc) + navModel.runDebridAction(urlString: debridManager.downloadUrl, .vlc) } ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .infuse) + navModel.runDebridAction(urlString: debridManager.downloadUrl, .infuse) } ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") { - UIPasteboard.general.string = debridManager.realDebridDownloadUrl + UIPasteboard.general.string = debridManager.downloadUrl showLinkCopyAlert.toggle() } .backport.alert( @@ -63,7 +63,7 @@ struct MagnetChoiceView: View { ) ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") { - if let url = URL(string: debridManager.realDebridDownloadUrl) { + if let url = URL(string: debridManager.downloadUrl) { navModel.activityItems = [url] navModel.showLocalActivitySheet.toggle() } @@ -108,14 +108,14 @@ struct MagnetChoiceView: View { } } .onDisappear { - debridManager.realDebridDownloadUrl = "" + debridManager.downloadUrl = "" } .navigationTitle("Link actions") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { - debridManager.realDebridDownloadUrl = "" + debridManager.downloadUrl = "" presentationMode.wrappedValue.dismiss() }