Debrid: Add Premiumize support and cleanup
Premiumize is another debrid provider. Add support in addition to other debrid services. Add a unified Magnet type that encloses both the link and hash when needed for certain services. A universal ASAuthenticationSession has been added to make implicit authentication easier for services that support it. Clean up declarations of certain variables that were mismanaged during the debrid decentralization process. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
2322d3af67
commit
17867db40c
18 changed files with 577 additions and 68 deletions
|
|
@ -22,8 +22,10 @@
|
|||
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 */; };
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */; };
|
||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7F293542F300486D65 /* PremiumizeModels.swift */; };
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */; };
|
||||
0C42B5982932F6DD008057A0 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Array.swift */; };
|
||||
0C42B5982932F6DD008057A0 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Set.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 */; };
|
||||
|
|
@ -105,7 +107,9 @@
|
|||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; };
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; };
|
||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
|
||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; };
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -125,8 +129,10 @@
|
|||
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>"; };
|
||||
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeWrapper.swift; sourceTree = "<group>"; };
|
||||
0C422E7F293542F300486D65 /* PremiumizeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeModels.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>"; };
|
||||
0C42B5972932F6DD008057A0 /* Set.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Set.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>"; };
|
||||
|
|
@ -203,6 +209,7 @@
|
|||
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
|
||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
|
||||
0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
|
||||
0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
|
@ -219,6 +226,7 @@
|
|||
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */,
|
||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
|
||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -287,6 +295,7 @@
|
|||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||
0C422E7F293542F300486D65 /* PremiumizeModels.swift */,
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */,
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||
|
|
@ -401,7 +410,6 @@
|
|||
0CA148C8288903F000DE2211 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C42B5972932F6DD008057A0 /* Array.swift */,
|
||||
0CA148C9288903F000DE2211 /* Collection.swift */,
|
||||
0CA148CA288903F000DE2211 /* Data.swift */,
|
||||
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
|
||||
|
|
@ -410,7 +418,9 @@
|
|||
0CA148CB288903F000DE2211 /* Task.swift */,
|
||||
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
||||
0C7ED14228D65518009E29AD /* FileManager.swift */,
|
||||
0C42B5972932F6DD008057A0 /* Set.swift */,
|
||||
0C7C128528DAA3CD00381CD1 /* URL.swift */,
|
||||
0CD72E16293D9928001A7EA4 /* Array.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -460,6 +470,7 @@
|
|||
children = (
|
||||
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */,
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
|
||||
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */,
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||
);
|
||||
path = API;
|
||||
|
|
@ -526,6 +537,7 @@
|
|||
0C7376EF28A97D1400D60918 /* SwiftUIX */,
|
||||
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
|
||||
0CB6516728C5A5EC00DCA721 /* Introspect */,
|
||||
0CDDDE042935235E006810B1 /* BetterSafariView */,
|
||||
);
|
||||
productName = Torrenter;
|
||||
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
|
||||
|
|
@ -563,6 +575,7 @@
|
|||
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
|
||||
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */,
|
||||
);
|
||||
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -621,6 +634,7 @@
|
|||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
|
|
@ -631,6 +645,7 @@
|
|||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */,
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */,
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */,
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */,
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
||||
|
|
@ -654,7 +669,7 @@
|
|||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */,
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */,
|
||||
0C42B5982932F6DD008057A0 /* Array.swift in Sources */,
|
||||
0C42B5982932F6DD008057A0 /* Set.swift in Sources */,
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
|
|
@ -666,6 +681,7 @@
|
|||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
|
||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||
|
|
@ -951,6 +967,14 @@
|
|||
kind = branch;
|
||||
};
|
||||
};
|
||||
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/stleamist/BetterSafariView";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
|
@ -989,6 +1013,11 @@
|
|||
package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
||||
productName = Introspect;
|
||||
};
|
||||
0CDDDE042935235E006810B1 /* BetterSafariView */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */;
|
||||
productName = BetterSafariView;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public class AllDebrid {
|
|||
authTask = Task {
|
||||
var count = 0
|
||||
|
||||
while count < 20 {
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw ADError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
|
@ -177,8 +177,8 @@ public class AllDebrid {
|
|||
return rawResponse.link
|
||||
}
|
||||
|
||||
public func instantAvailability(hashes: [String]) async throws -> [IA] {
|
||||
let queryItems = hashes.map { URLQueryItem(name: "magnets[]", value: $0) }
|
||||
public func instantAvailability(magnets: [Magnet]) async throws -> [IA] {
|
||||
let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
|
|
|
|||
158
Ferrite/API/PremiumizeWrapper.swift
Normal file
158
Ferrite/API/PremiumizeWrapper.swift
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// PremiumizeWrapper.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainSwift
|
||||
|
||||
public class Premiumize {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let keychain = KeychainSwift()
|
||||
|
||||
let baseAuthUrl = "https://www.premiumize.me/authorize"
|
||||
let baseApiUrl = "https://www.premiumize.me/api"
|
||||
let clientId = "791565696"
|
||||
|
||||
public func buildAuthUrl() throws -> URL {
|
||||
var urlComponents = URLComponents(string: baseAuthUrl)!
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: clientId),
|
||||
URLQueryItem(name: "response_type", value: "token"),
|
||||
URLQueryItem(name: "state", value: UUID().uuidString)
|
||||
]
|
||||
|
||||
if let url = urlComponents.url {
|
||||
return url
|
||||
} else {
|
||||
throw PMError.InvalidUrl
|
||||
}
|
||||
}
|
||||
|
||||
public func handleAuthCallback(url: URL) throws {
|
||||
let callbackComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
|
||||
guard let callbackFragment = callbackComponents?.fragment else {
|
||||
throw PMError.InvalidResponse
|
||||
}
|
||||
|
||||
var fragmentComponents = URLComponents()
|
||||
fragmentComponents.query = callbackFragment
|
||||
|
||||
guard let accessToken = fragmentComponents.queryItems?.first(where: { $0.name == "access_token" })?.value else {
|
||||
throw PMError.InvalidToken
|
||||
}
|
||||
|
||||
keychain.set(accessToken, forKey: "Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
keychain.delete("Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// 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("Premiumize.AccessToken") else {
|
||||
throw PMError.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 PMError.FailedRequest(description: "No HTTP response given")
|
||||
}
|
||||
|
||||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
deleteTokens()
|
||||
throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.")
|
||||
} else {
|
||||
throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
// Parent function for initial checking of the cache
|
||||
public func checkCache(magnets: [Magnet]) async throws -> [Magnet] {
|
||||
var urlComponents = URLComponents(string: "\(baseApiUrl)/cache/check")!
|
||||
urlComponents.queryItems = magnets.map { URLQueryItem(name: "items[]", value: $0.hash) }
|
||||
guard let url = urlComponents.url else {
|
||||
throw PMError.InvalidUrl
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data)
|
||||
|
||||
if rawResponse.response.isEmpty {
|
||||
throw PMError.EmptyData
|
||||
} else {
|
||||
let availableMagnets = magnets.enumerated().compactMap { index, magnet in
|
||||
if rawResponse.response[safe: index] == true {
|
||||
return magnet
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return availableMagnets
|
||||
}
|
||||
}
|
||||
|
||||
// Function to divide and execute DDL endpoint requests in parallel
|
||||
// Calls this for 10 requests at a time to not overwhelm API servers
|
||||
public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [IA] {
|
||||
let tempIA = try await withThrowingTaskGroup(of: Premiumize.IA.self) { group in
|
||||
for magnet in magnetChunk {
|
||||
group.addTask {
|
||||
try await self.fetchDDL(magnet: magnet)
|
||||
}
|
||||
}
|
||||
|
||||
var chunkedIA: [Premiumize.IA] = []
|
||||
for try await ia in group {
|
||||
chunkedIA.append(ia)
|
||||
}
|
||||
return chunkedIA
|
||||
}
|
||||
|
||||
return tempIA
|
||||
}
|
||||
|
||||
// Grabs DDL links
|
||||
func fetchDDL(magnet: Magnet) async throws -> IA {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/directdl")!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var bodyComponents = URLComponents()
|
||||
bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnet.link)]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(DDLResponse.self, from: data)
|
||||
|
||||
if !rawResponse.content.isEmpty {
|
||||
let files = rawResponse.content.map { file in
|
||||
IAFile(
|
||||
name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path,
|
||||
streamUrlString: file.link
|
||||
)
|
||||
}
|
||||
|
||||
return IA(
|
||||
hash: magnet.hash,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
)
|
||||
} else {
|
||||
throw PMError.EmptyData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ public class RealDebrid {
|
|||
authTask = Task {
|
||||
var count = 0
|
||||
|
||||
while count < 20 {
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw RDError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
|
@ -186,9 +186,9 @@ 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 -> [IA] {
|
||||
public func instantAvailability(magnets: [Magnet]) async throws -> [IA] {
|
||||
var availableHashes: [RealDebrid.IA] = []
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!)
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.map(\.hash).joined(separator: "/"))")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,25 +2,16 @@
|
|||
// Array.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/26/22.
|
||||
// Created by Brian Dashore on 12/4/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 "[]"
|
||||
extension Array {
|
||||
// From https://www.hackingwithswift.com/example-code/language/how-to-split-an-array-into-chunks
|
||||
func chunked(into size: Int) -> [[Element]] {
|
||||
stride(from: 0, to: count, by: size).map {
|
||||
Array(self[$0 ..< Swift.min($0 + size, count)])
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
Ferrite/Extensions/Set.swift
Normal file
26
Ferrite/Extensions/Set.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,19 @@
|
|||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Ferrite</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ferrite://</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public enum IAStatus: Codable, Hashable, Sendable {
|
|||
public enum DebridType: Int, Codable, Hashable, CaseIterable {
|
||||
case realDebrid = 1
|
||||
case allDebrid = 2
|
||||
case premiumize = 3
|
||||
|
||||
func toString(abbreviated: Bool = false) -> String {
|
||||
switch self {
|
||||
|
|
@ -27,6 +28,14 @@ public enum DebridType: Int, Codable, Hashable, CaseIterable {
|
|||
return abbreviated ? "RD" : "RealDebrid"
|
||||
case .allDebrid:
|
||||
return abbreviated ? "AD" : "AllDebrid"
|
||||
case .premiumize:
|
||||
return abbreviated ? "PM" : "Premiumize"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper struct for magnet links to contain both the link and hash for easy access
|
||||
public struct Magnet: Codable, Hashable, Sendable {
|
||||
let link: String
|
||||
let hash: String
|
||||
}
|
||||
|
|
|
|||
68
Ferrite/Models/PremiumizeModels.swift
Normal file
68
Ferrite/Models/PremiumizeModels.swift
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// PremiumizeModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Premiumize {
|
||||
// MARK: - Errors
|
||||
|
||||
// TODO: Hybridize debrid errors in one structure
|
||||
enum PMError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
||||
// MARK: - CacheCheckResponse
|
||||
|
||||
struct CacheCheckResponse: Codable {
|
||||
let status: String
|
||||
let response: [Bool]
|
||||
}
|
||||
|
||||
// MARK: - DDLResponse
|
||||
|
||||
struct DDLResponse: Codable {
|
||||
let status: String
|
||||
let content: [DDLData]
|
||||
let location: String
|
||||
let filename: String
|
||||
let filesize: Int
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
struct DDLData: Codable {
|
||||
let path: String
|
||||
let size: Int
|
||||
let link: String
|
||||
let streamLink: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case path, size, link
|
||||
case streamLink = "stream_link"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailability client side structures
|
||||
|
||||
struct IA: Codable, Hashable {
|
||||
let hash: String
|
||||
let expiryTimeStamp: Double
|
||||
let files: [IAFile]
|
||||
}
|
||||
|
||||
struct IAFile: Codable, Hashable {
|
||||
let name: String
|
||||
let streamUrlString: String
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct SearchResult: Hashable, Codable, Sendable {
|
||||
public struct SearchResult: Codable, Hashable, Sendable {
|
||||
let title: String?
|
||||
let source: String
|
||||
let size: String?
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ public class DebridManager: ObservableObject {
|
|||
var toastModel: ToastViewModel?
|
||||
let realDebrid: RealDebrid = .init()
|
||||
let allDebrid: AllDebrid = .init()
|
||||
let premiumize: Premiumize = .init()
|
||||
|
||||
// UI Variables
|
||||
@Published var showWebView: Bool = false
|
||||
@Published var showAuthSession: Bool = false
|
||||
@Published var showLoadingProgress: Bool = false
|
||||
|
||||
// Service agnostic variables
|
||||
|
|
@ -34,7 +36,7 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
var currentDebridTask: Task<Void, Never>?
|
||||
var downloadUrl: String = ""
|
||||
var authUrl: String = ""
|
||||
var authUrl: URL?
|
||||
|
||||
// RealDebrid auth variables
|
||||
@Published var realDebridAuthProcessing: Bool = false
|
||||
|
|
@ -57,6 +59,15 @@ public class DebridManager: ObservableObject {
|
|||
var selectedAllDebridItem: AllDebrid.IA?
|
||||
var selectedAllDebridFile: AllDebrid.IAFile?
|
||||
|
||||
// Premiumize auth variables
|
||||
@Published var premiumizeAuthProcessing: Bool = false
|
||||
|
||||
// Premiumize fetch variables
|
||||
@Published var premiumizeIAValues: [Premiumize.IA] = []
|
||||
|
||||
var selectedPremiumizeItem: Premiumize.IA?
|
||||
var selectedPremiumizeFile: Premiumize.IAFile?
|
||||
|
||||
init() {
|
||||
if let rawDebridList = UserDefaults.standard.string(forKey: "Debrid.EnabledArray"),
|
||||
let serializedDebridList = Set<DebridType>(rawValue: rawDebridList)
|
||||
|
|
@ -88,45 +99,69 @@ public class DebridManager: ObservableObject {
|
|||
enabledDebrids.insert(.allDebrid)
|
||||
UserDefaults.standard.set(false, forKey: "AllDebrid.Enabled")
|
||||
}
|
||||
|
||||
let premiumizeEnabled = UserDefaults.standard.bool(forKey: "Premiumize.Enabled")
|
||||
if premiumizeEnabled {
|
||||
enabledDebrids.insert(.premiumize)
|
||||
UserDefaults.standard.set(false, forKey: "Premiumize.Enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Common function to populate hashes for debrid services
|
||||
public func populateDebridHashes(_ resultHashes: [String]) async {
|
||||
public func populateDebridIA(_ resultMagnets: [Magnet]) async {
|
||||
do {
|
||||
let now = Date()
|
||||
|
||||
// If a hash isn't found in the IA, update it
|
||||
// If the hash is expired, remove it and update it
|
||||
let sendHashes = resultHashes.filter { hash in
|
||||
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }), enabledDebrids.contains(.realDebrid) {
|
||||
let sendMagnets = resultMagnets.filter { magnet in
|
||||
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == magnet.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) {
|
||||
} else if let IAIndex = allDebridIAValues.firstIndex(where: { $0.hash == magnet.hash }), enabledDebrids.contains(.allDebrid) {
|
||||
if now.timeIntervalSince1970 > allDebridIAValues[IAIndex].expiryTimeStamp {
|
||||
allDebridIAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if let IAIndex = premiumizeIAValues.firstIndex(where: { $0.hash == magnet.hash }), enabledDebrids.contains(.premiumize) {
|
||||
if now.timeIntervalSince1970 > premiumizeIAValues[IAIndex].expiryTimeStamp {
|
||||
premiumizeIAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !sendHashes.isEmpty {
|
||||
if !sendMagnets.isEmpty {
|
||||
if enabledDebrids.contains(.realDebrid) {
|
||||
let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnetHashes: sendHashes)
|
||||
let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnets: sendMagnets)
|
||||
realDebridIAValues += fetchedRealDebridIA
|
||||
}
|
||||
|
||||
if enabledDebrids.contains(.allDebrid) {
|
||||
let fetchedAllDebridIA = try await allDebrid.instantAvailability(hashes: sendHashes)
|
||||
let fetchedAllDebridIA = try await allDebrid.instantAvailability(magnets: sendMagnets)
|
||||
allDebridIAValues += fetchedAllDebridIA
|
||||
}
|
||||
|
||||
if enabledDebrids.contains(.premiumize) {
|
||||
let availableMagnets = try await premiumize.checkCache(magnets: sendMagnets)
|
||||
|
||||
// Split DDL requests into chunks of 10
|
||||
for chunk in availableMagnets.chunked(into: 10) {
|
||||
let tempIA = try await premiumize.divideDDLRequests(magnetChunk: chunk)
|
||||
|
||||
premiumizeIAValues += tempIA
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
|
|
@ -166,6 +201,16 @@ public class DebridManager: ObservableObject {
|
|||
} else {
|
||||
return .full
|
||||
}
|
||||
case .premiumize:
|
||||
guard let premiumizeMatch = premiumizeIAValues.first(where: { result.magnetHash == $0.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if premiumizeMatch.files.count > 1 {
|
||||
return .partial
|
||||
} else {
|
||||
return .full
|
||||
}
|
||||
case .none:
|
||||
return .none
|
||||
}
|
||||
|
|
@ -194,6 +239,14 @@ public class DebridManager: ObservableObject {
|
|||
toastModel?.updateToastDescription("Could not find the associated AllDebrid entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .premiumize:
|
||||
if let premiumizeItem = premiumizeIAValues.first(where: { magnetHash == $0.hash }) {
|
||||
selectedPremiumizeItem = premiumizeItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not find the associated Premiumize entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .none:
|
||||
return false
|
||||
}
|
||||
|
|
@ -205,50 +258,129 @@ public class DebridManager: ObservableObject {
|
|||
public func authenticateDebrid(debridType: DebridType) async {
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
await authenticateRd()
|
||||
enabledDebrids.insert(.realDebrid)
|
||||
let success = await authenticateRd()
|
||||
completeDebridAuth(debridType, success: success)
|
||||
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
|
||||
let success = await authenticateAd()
|
||||
completeDebridAuth(debridType, success: success)
|
||||
case .premiumize:
|
||||
await authenticatePm()
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticateRd() async {
|
||||
// Callback to finish debrid auth since functions can be split
|
||||
func completeDebridAuth(_ debridType: DebridType, success: Bool = true) {
|
||||
if enabledDebrids.count == 1, success {
|
||||
print("Enabled debrids is 1!")
|
||||
selectedDebridType = enabledDebrids.first
|
||||
}
|
||||
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
realDebridAuthProcessing = false
|
||||
case .allDebrid:
|
||||
allDebridAuthProcessing = false
|
||||
case .premiumize:
|
||||
premiumizeAuthProcessing = false
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper function to validate and present an auth URL to the user
|
||||
@discardableResult func validateAuthUrl(_ url: URL?, useAuthSession: Bool = false) -> Bool {
|
||||
guard let url else {
|
||||
toastModel?.updateToastDescription("Authentication Error: Invalid URL created: \(String(describing: url))")
|
||||
return false
|
||||
}
|
||||
|
||||
authUrl = url
|
||||
if useAuthSession {
|
||||
showAuthSession.toggle()
|
||||
} else {
|
||||
showWebView.toggle()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func authenticateRd() async -> Bool {
|
||||
do {
|
||||
realDebridAuthProcessing = true
|
||||
let verificationResponse = try await realDebrid.getVerificationInfo()
|
||||
|
||||
authUrl = verificationResponse.directVerificationURL
|
||||
showWebView.toggle()
|
||||
if validateAuthUrl(URL(string: verificationResponse.directVerificationURL)) {
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
enabledDebrids.insert(.realDebrid)
|
||||
} else {
|
||||
throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid")
|
||||
}
|
||||
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
return true
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("RealDebrid authentication error: \(error)")
|
||||
realDebrid.authTask?.cancel()
|
||||
|
||||
print("RealDebrid authentication error: \(error)")
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticateAd() async {
|
||||
private func authenticateAd() async -> Bool {
|
||||
do {
|
||||
allDebridAuthProcessing = true
|
||||
let pinResponse = try await allDebrid.getPinInfo()
|
||||
|
||||
authUrl = pinResponse.userURL
|
||||
showWebView.toggle()
|
||||
if validateAuthUrl(URL(string: pinResponse.userURL)) {
|
||||
try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin)
|
||||
enabledDebrids.insert(.allDebrid)
|
||||
} else {
|
||||
throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid")
|
||||
}
|
||||
|
||||
try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin)
|
||||
return true
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("AllDebrid authentication error: \(error)")
|
||||
allDebrid.authTask?.cancel()
|
||||
|
||||
print("AllDebrid authentication error: \(error)")
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticatePm() async {
|
||||
do {
|
||||
premiumizeAuthProcessing = true
|
||||
let tempAuthUrl = try premiumize.buildAuthUrl()
|
||||
|
||||
validateAuthUrl(tempAuthUrl, useAuthSession: true)
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("Premiumize authentication error: \(error)")
|
||||
completeDebridAuth(.premiumize, success: false)
|
||||
|
||||
print("Premiumize authentication error (auth): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Currently handles Premiumize callback
|
||||
public func handleCallback(url: URL?, error: Error?) {
|
||||
do {
|
||||
if let error {
|
||||
throw Premiumize.PMError.AuthQuery(description: "OAuth callback Error: \(error)")
|
||||
}
|
||||
|
||||
if let callbackUrl = url {
|
||||
try premiumize.handleAuthCallback(url: callbackUrl)
|
||||
enabledDebrids.insert(.premiumize)
|
||||
completeDebridAuth(.premiumize)
|
||||
} else {
|
||||
throw Premiumize.PMError.AuthQuery(description: "The callback URL was invalid")
|
||||
}
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("Premiumize authentication error: \(error)")
|
||||
completeDebridAuth(.premiumize, success: false)
|
||||
|
||||
print("Premiumize authentication error (callback): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +393,8 @@ public class DebridManager: ObservableObject {
|
|||
await logoutRd()
|
||||
case .allDebrid:
|
||||
logoutAd()
|
||||
case .premiumize:
|
||||
logoutPm()
|
||||
}
|
||||
|
||||
// Automatically resets the preferred debrid service if it was set to the logged out service
|
||||
|
|
@ -273,7 +407,6 @@ public class DebridManager: ObservableObject {
|
|||
do {
|
||||
try await realDebrid.deleteTokens()
|
||||
enabledDebrids.remove(.realDebrid)
|
||||
realDebridAuthProcessing = false
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("RealDebrid logout error: \(error)")
|
||||
|
||||
|
|
@ -284,11 +417,15 @@ public class DebridManager: ObservableObject {
|
|||
private func logoutAd() {
|
||||
allDebrid.deleteTokens()
|
||||
enabledDebrids.remove(.allDebrid)
|
||||
allDebridAuthProcessing = false
|
||||
|
||||
toastModel?.updateToastDescription("Please manually delete the AllDebrid API key", newToastType: .info)
|
||||
}
|
||||
|
||||
private func logoutPm() {
|
||||
premiumize.deleteTokens()
|
||||
enabledDebrids.remove(.premiumize)
|
||||
}
|
||||
|
||||
// MARK: - Debrid fetch UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to fetch from
|
||||
|
|
@ -300,7 +437,8 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
showLoadingProgress = true
|
||||
|
||||
guard let magnetLink = searchResult.magnetLink else {
|
||||
// Premiumize doesn't need a magnet link
|
||||
guard let magnetLink = searchResult.magnetLink, selectedDebridType == .premiumize else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("Debrid error: Invalid magnet link")
|
||||
|
||||
|
|
@ -312,14 +450,14 @@ public class DebridManager: ObservableObject {
|
|||
await fetchRdDownload(magnetLink: magnetLink)
|
||||
case .allDebrid:
|
||||
await fetchAdDownload(magnetLink: magnetLink)
|
||||
case .premiumize:
|
||||
fetchPmDownload()
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRdDownload(magnetLink: String) async {
|
||||
print("Called RD Download function!")
|
||||
|
||||
do {
|
||||
var fileIds: [Int] = []
|
||||
|
||||
|
|
@ -416,4 +554,22 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPmDownload() {
|
||||
guard let premiumizeItem = selectedPremiumizeItem else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the result is invalid")
|
||||
print("Premiumize download error: Invalid selected Premiumize item")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if let premiumizeFile = selectedPremiumizeFile {
|
||||
downloadUrl = premiumizeFile.streamUrlString
|
||||
} else if let firstFile = premiumizeItem.files[safe: 0] {
|
||||
downloadUrl = firstFile.streamUrlString
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the result could not be found")
|
||||
print("Premiumize download error: Could not find the selected Premiumize file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,8 +54,14 @@ struct BookmarksView: View {
|
|||
.onAppear {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
viewTask = Task {
|
||||
let hashes = bookmarks.compactMap(\.magnetHash)
|
||||
await debridManager.populateDebridHashes(hashes)
|
||||
let magnets = bookmarks.compactMap {
|
||||
if let magnetLink = $0.magnetLink, let magnetHash = $0.magnetHash {
|
||||
return Magnet(link: magnetLink, hash: magnetHash)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ struct SearchResultInfoView: View {
|
|||
if debridManager.selectedDebridType == .allDebrid {
|
||||
DebridLabelView(result: result, debridAbbreviation: "AD")
|
||||
}
|
||||
|
||||
if debridManager.selectedDebridType == .premiumize {
|
||||
DebridLabelView(result: result, debridAbbreviation: "PM")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,9 +90,14 @@ struct ContentView: View {
|
|||
debridManager.realDebridIAValues = []
|
||||
debridManager.allDebridIAValues = []
|
||||
|
||||
await debridManager.populateDebridHashes(
|
||||
scrapingModel.searchResults.compactMap(\.magnetHash)
|
||||
)
|
||||
let magnets = scrapingModel.searchResults.compactMap {
|
||||
if let magnetLink = $0.magnetLink, let magnetHash = $0.magnetHash {
|
||||
return Magnet(link: magnetLink, hash: magnetHash)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
|
||||
navModel.showSearchProgress = false
|
||||
|
|
@ -109,6 +114,8 @@ struct ContentView: View {
|
|||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
searchController.searchBar.autocorrectionType = .no
|
||||
searchController.searchBar.autocapitalizationType = .none
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ struct WebView: UIViewRepresentable {
|
|||
var url: URL
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let webView = WKWebView()
|
||||
// Make the WebView ephemeral
|
||||
let config = WKWebViewConfiguration()
|
||||
config.websiteDataStore = WKWebsiteDataStore.nonPersistent()
|
||||
|
||||
let webView = WKWebView(frame: .zero, configuration: config)
|
||||
let _ = webView.load(URLRequest(url: url))
|
||||
return webView
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
// Created by Brian Dashore on 7/11/22.
|
||||
//
|
||||
|
||||
import BetterSafariView
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ struct SettingsView: View {
|
|||
Task {
|
||||
if debridManager.enabledDebrids.contains(.allDebrid) {
|
||||
await debridManager.logoutDebrid(debridType: .allDebrid)
|
||||
} else if !debridManager.realDebridAuthProcessing {
|
||||
} else if !debridManager.allDebridAuthProcessing {
|
||||
await debridManager.authenticateDebrid(debridType: .allDebrid)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +57,23 @@ struct SettingsView: View {
|
|||
.foregroundColor(debridManager.enabledDebrids.contains(.allDebrid) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Premiumize")
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.enabledDebrids.contains(.premiumize) {
|
||||
await debridManager.logoutDebrid(debridType: .premiumize)
|
||||
} else if !debridManager.premiumizeAuthProcessing {
|
||||
await debridManager.authenticateDebrid(debridType: .premiumize)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.enabledDebrids.contains(.premiumize) ? "Logout" : (debridManager.premiumizeAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(.premiumize) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Source management")) {
|
||||
|
|
@ -132,7 +150,16 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
.sheet(isPresented: $debridManager.showWebView) {
|
||||
LoginWebView(url: URL(string: debridManager.authUrl)!)
|
||||
LoginWebView(url: debridManager.authUrl ?? URL(string: "https://google.com")!)
|
||||
}
|
||||
.webAuthenticationSession(isPresented: $debridManager.showAuthSession) {
|
||||
WebAuthenticationSession(
|
||||
url: debridManager.authUrl ?? URL(string: "https://google.com")!,
|
||||
callbackURLScheme: "ferrite"
|
||||
) { callbackURL, error in
|
||||
debridManager.handleCallback(url: callbackURL, error: error)
|
||||
}
|
||||
.prefersEphemeralWebBrowserSession(false)
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ struct BatchChoiceView: View {
|
|||
queueCommonDownload(fileName: file.fileName)
|
||||
}
|
||||
}
|
||||
case .premiumize:
|
||||
ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.name) {
|
||||
debridManager.selectedPremiumizeFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.name)
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
|
|
@ -78,11 +86,14 @@ struct BatchChoiceView: View {
|
|||
|
||||
switch debridManager.selectedDebridType {
|
||||
case .realDebrid:
|
||||
debridManager.selectedAllDebridFile = nil
|
||||
debridManager.selectedAllDebridItem = nil
|
||||
case .allDebrid:
|
||||
debridManager.selectedRealDebridFile = nil
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
case .allDebrid:
|
||||
debridManager.selectedAllDebridFile = nil
|
||||
debridManager.selectedAllDebridItem = nil
|
||||
case .premiumize:
|
||||
debridManager.selectedPremiumizeFile = nil
|
||||
debridManager.selectedPremiumizeItem = nil
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
Section(header: "Real Debrid options") {
|
||||
Section(header: "Debrid options") {
|
||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue