Debrid: Decentralize and add AllDebrid support

AllDebrid is another debrid provider. Add support to Ferrite in
addition to RealDebrid.

The overall debrid login backend has changed to accomodate for a more
agnostic app structure where more services can be added as needed.

Also add some cosmetic changes to search so filters can be added while
searching for a phrase.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2022-11-27 18:14:32 -05:00
parent 06d4f8e84e
commit 2322d3af67
36 changed files with 1014 additions and 252 deletions

View file

@ -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 = "<group>"; };
0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = "<group>"; };
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = "<group>"; };
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
@ -117,6 +125,8 @@
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; };
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; };
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridChoiceView.swift; sourceTree = "<group>"; };
0C42B5972932F6DD008057A0 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
@ -124,9 +134,11 @@
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = "<group>"; };
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridModels.swift; sourceTree = "<group>"; };
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = "<group>"; };
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenter.swift; sourceTree = "<group>"; };
0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = "<group>"; };
@ -213,6 +225,36 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0C0755C22934241F00ECA142 /* SheetViews */ = {
isa = PBXGroup;
children = (
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
);
path = SheetViews;
sourceTree = "<group>";
};
0C0755C32934244500ECA142 /* ComponentViews */ = {
isa = PBXGroup;
children = (
0C0755C42934245800ECA142 /* Debrid */,
0CA3B23528C265FD00616D3A /* Library */,
0C44E2AB28D4E126007711AE /* SearchResult */,
0CA0545C288F7CB200850554 /* Settings */,
0C794B65289DAC9F00DD1CC8 /* Source */,
);
path = ComponentViews;
sourceTree = "<group>";
};
0C0755C42934245800ECA142 /* Debrid */ = {
isa = PBXGroup;
children = (
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */,
0C0755C5293424A200ECA142 /* DebridLabelView.swift */,
);
path = Debrid;
sourceTree = "<group>";
};
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 = "<group>";
};
0C44E2AB28D4E126007711AE /* SearchResultViews */ = {
0C44E2AB28D4E126007711AE /* SearchResult */ = {
isa = PBXGroup;
children = (
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */,
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */,
);
path = SearchResultViews;
path = SearchResult;
sourceTree = "<group>";
};
0C794B65289DAC9F00DD1CC8 /* SourceViews */ = {
0C794B65289DAC9F00DD1CC8 /* Source */ = {
isa = PBXGroup;
children = (
0C44E2AA28D4E09B007711AE /* Buttons */,
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
);
path = SourceViews;
path = Source;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
@ -417,13 +458,14 @@
0CA148F12889066000DE2211 /* API */ = {
isa = PBXGroup;
children = (
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */,
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
);
path = API;
sourceTree = "<group>";
};
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 = "<group>";
};
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 */,

View file

@ -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<Void, Error>?
// 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<PinResponse>.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<ApiKeyResponse>.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<AddMagnetResponse>.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<MagnetStatusResponse>.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<UnlockLinkResponse>.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<InstantAvailabilityResponse>.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
}
}

View file

@ -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: "/"))")!)

View file

@ -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<Element>.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
}
}

View file

@ -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,

View file

@ -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<ADData: Codable>: 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
}
}

View file

@ -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"
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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<Void, Never>?
// RealDebrid auth variables
@Published var realDebridEnabled: Bool = false {
@Published var enabledDebrids: Set<DebridType> = [] {
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<Void, Never>?
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<DebridType>(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)")
}
}
}
}

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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<Bookmark>
@ -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)

View file

@ -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 {

View file

@ -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))
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}
}
}

View file

@ -71,7 +71,10 @@ struct LibraryView: View {
HStack {
EditButton()
if selectedSegment == .history {
switch selectedSegment {
case .bookmarks:
DebridChoiceView()
case .history:
HistoryActionsView()
}
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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")
}

View file

@ -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()
}
}

View file

@ -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()
}