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:
kingbri 2022-12-05 16:01:26 -05:00
parent 2322d3af67
commit 17867db40c
18 changed files with 577 additions and 68 deletions

View file

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

View file

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

View 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
}
}
}

View file

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

View file

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

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

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

View file

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

View 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
}
}

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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