Debrid: Various updates to API and settings
Debrid services can change their APIs at any time which negatively impacts user experiences on Ferrite. Add the following: - Ability for a user to add a manually generated API key only showing the last 4 characters for security purposes. - Make ephemeral auth sessions toggle-able. ASWebAuthenticationView does not automatically clear on toggle change. - Add the savedLinks endpoint for AllDebrid so users can access their downloads and magnets. - Add a links section to AD's cloud view. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
d8db3e0cc8
commit
375de6f46e
16 changed files with 374 additions and 82 deletions
|
|
@ -142,6 +142,7 @@
|
|||
0CC2CA7429E24F63000A8585 /* ExpandedSearchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2CA7329E24F63000A8585 /* ExpandedSearchable.swift */; };
|
||||
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */; };
|
||||
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */; };
|
||||
0CD0265729FEFBF900A83D25 /* FerriteKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */; };
|
||||
0CD4030A29DA01B6008D9F03 /* PluginInfoMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */; };
|
||||
0CD4030C29DA0222008D9F03 /* PluginInfoAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */; };
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
||||
|
|
@ -288,6 +289,7 @@
|
|||
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FerriteKeychain.swift; sourceTree = "<group>"; };
|
||||
0CD4030929DA01B6008D9F03 /* PluginInfoMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoMetaView.swift; sourceTree = "<group>"; };
|
||||
0CD4030B29DA0222008D9F03 /* PluginInfoAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoAboutView.swift; sourceTree = "<group>"; };
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -449,6 +451,7 @@
|
|||
children = (
|
||||
0C44E2A728D4DDDC007711AE /* Application.swift */,
|
||||
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */,
|
||||
0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -852,6 +855,7 @@
|
|||
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */,
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
|
||||
0CD0265729FEFBF900A83D25 /* FerriteKeychain.swift in Sources */,
|
||||
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */,
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||
0C50B7D0299DF63C00A9FA3C /* UIDevice.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@
|
|||
//
|
||||
|
||||
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"
|
||||
|
|
@ -60,7 +58,7 @@ public class AllDebrid {
|
|||
|
||||
// If there's an API key from the response, end the task successfully
|
||||
if let apiKeyResponse = rawResponse {
|
||||
keychain.set(apiKeyResponse.apikey, forKey: "AllDebrid.ApiKey")
|
||||
FerriteKeychain.shared.set(apiKeyResponse.apikey, forKey: "AllDebrid.ApiKey")
|
||||
|
||||
return
|
||||
} else {
|
||||
|
|
@ -77,14 +75,27 @@ public class AllDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
// Adds a manual API key instead of web auth
|
||||
public func setApiKey(_ key: String) -> Bool {
|
||||
FerriteKeychain.shared.set(key, forKey: "AllDebrid.ApiKey")
|
||||
UserDefaults.standard.set(true, forKey: "AllDebrid.UseManualKey")
|
||||
|
||||
return FerriteKeychain.shared.get("AllDebrid.ApiKey") == key
|
||||
}
|
||||
|
||||
public func getToken() -> String? {
|
||||
return FerriteKeychain.shared.get("AllDebrid.ApiKey")
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
keychain.delete("AllDebrid.ApiKey")
|
||||
FerriteKeychain.shared.delete("AllDebrid.ApiKey")
|
||||
UserDefaults.standard.removeObject(forKey: "AllDebrid.UseManualKey")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
guard let token = getToken() else {
|
||||
throw ADError.InvalidToken
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +212,37 @@ public class AllDebrid {
|
|||
return rawResponse.link
|
||||
}
|
||||
|
||||
public func saveLink(link: String) async throws {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "links[]", value: link)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links/save", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
public func savedLinks() async throws -> [SavedLink] {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links"))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<SavedLinksResponse>.self, from: data).data
|
||||
|
||||
if rawResponse.links.isEmpty {
|
||||
throw ADError.EmptyData
|
||||
} else {
|
||||
return rawResponse.links
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteLink(link: String) async throws {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "link", value: link)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links/delete", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -6,11 +6,9 @@
|
|||
//
|
||||
|
||||
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"
|
||||
|
|
@ -45,17 +43,30 @@ public class Premiumize {
|
|||
throw PMError.InvalidToken
|
||||
}
|
||||
|
||||
keychain.set(accessToken, forKey: "Premiumize.AccessToken")
|
||||
FerriteKeychain.shared.set(accessToken, forKey: "Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// Adds a manual API key instead of web auth
|
||||
public func setApiKey(_ key: String) -> Bool {
|
||||
FerriteKeychain.shared.set(key, forKey: "Premiumize.AccessToken")
|
||||
UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey")
|
||||
|
||||
return FerriteKeychain.shared.get("Premiumize.AccessToken") == key
|
||||
}
|
||||
|
||||
public func getToken() -> String? {
|
||||
return FerriteKeychain.shared.get("Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
keychain.delete("Premiumize.AccessToken")
|
||||
FerriteKeychain.shared.delete("Premiumize.AccessToken")
|
||||
UserDefaults.standard.removeObject(forKey: "Premiumize.UseManualKey")
|
||||
}
|
||||
|
||||
// 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 {
|
||||
guard let token = getToken() else {
|
||||
throw PMError.InvalidToken
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,9 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainSwift
|
||||
|
||||
public class RealDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let keychain = KeychainSwift()
|
||||
|
||||
let baseAuthUrl = "https://api.real-debrid.com/oauth/v2"
|
||||
let baseApiUrl = "https://api.real-debrid.com/rest/1.0"
|
||||
|
|
@ -83,7 +81,7 @@ public class RealDebrid {
|
|||
// If there's a client ID from the response, end the task successfully
|
||||
if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret {
|
||||
await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId")
|
||||
keychain.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
|
||||
try await getTokens(deviceCode: deviceCode)
|
||||
|
||||
|
|
@ -102,13 +100,13 @@ public class RealDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch all tokens for the user and store in keychain
|
||||
// Fetch all tokens for the user and store in FerriteKeychain.shared
|
||||
public func getTokens(deviceCode: String) async throws {
|
||||
guard let clientId = UserDefaults.standard.string(forKey: "RealDebrid.ClientId") else {
|
||||
throw RDError.EmptyData
|
||||
}
|
||||
|
||||
guard let clientSecret = keychain.get("RealDebrid.ClientSecret") else {
|
||||
guard let clientSecret = FerriteKeychain.shared.get("RealDebrid.ClientSecret") else {
|
||||
throw RDError.EmptyData
|
||||
}
|
||||
|
||||
|
|
@ -130,8 +128,8 @@ public class RealDebrid {
|
|||
|
||||
let rawResponse = try jsonDecoder.decode(TokenResponse.self, from: data)
|
||||
|
||||
keychain.set(rawResponse.accessToken, forKey: "RealDebrid.AccessToken")
|
||||
keychain.set(rawResponse.refreshToken, forKey: "RealDebrid.RefreshToken")
|
||||
FerriteKeychain.shared.set(rawResponse.accessToken, forKey: "RealDebrid.AccessToken")
|
||||
FerriteKeychain.shared.set(rawResponse.refreshToken, forKey: "RealDebrid.RefreshToken")
|
||||
|
||||
let accessTimestamp = Date().timeIntervalSince1970 + Double(rawResponse.expiresIn)
|
||||
await setUserDefaultsValue(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp")
|
||||
|
|
@ -142,7 +140,7 @@ public class RealDebrid {
|
|||
|
||||
if Date().timeIntervalSince1970 > accessTokenStamp {
|
||||
do {
|
||||
if let refreshToken = keychain.get("RealDebrid.RefreshToken") {
|
||||
if let refreshToken = FerriteKeychain.shared.get("RealDebrid.RefreshToken") {
|
||||
try await getTokens(deviceCode: refreshToken)
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -151,22 +149,35 @@ public class RealDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
return keychain.get("RealDebrid.AccessToken")
|
||||
return FerriteKeychain.shared.get("RealDebrid.AccessToken")
|
||||
}
|
||||
|
||||
// Adds a manual API key instead of web auth
|
||||
// Clear out existing refresh tokens and timestamps
|
||||
public func setApiKey(_ key: String) -> Bool {
|
||||
FerriteKeychain.shared.set(key, forKey: "RealDebrid.AccessToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.AccessTokenStamp")
|
||||
|
||||
UserDefaults.standard.set(true, forKey: "RealDebrid.UseManualKey")
|
||||
|
||||
return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key
|
||||
}
|
||||
|
||||
public func deleteTokens() async throws {
|
||||
keychain.delete("RealDebrid.RefreshToken")
|
||||
keychain.delete("RealDebrid.ClientSecret")
|
||||
FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.ClientSecret")
|
||||
await removeUserDefaultsValue(forKey: "RealDebrid.ClientId")
|
||||
await removeUserDefaultsValue(forKey: "RealDebrid.AccessTokenStamp")
|
||||
|
||||
// Run the request, doesn't matter if it fails
|
||||
if let token = keychain.get("RealDebrid.AccessToken") {
|
||||
if let token = FerriteKeychain.shared.get("RealDebrid.AccessToken") {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/disable_access_token")!)
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
_ = try? await URLSession.shared.data(for: request)
|
||||
|
||||
keychain.delete("RealDebrid.AccessToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.AccessToken")
|
||||
await removeUserDefaultsValue(forKey: "RealDebrid.UseManualKey")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@ import Introspect
|
|||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
// Modifies properties of a view. Works the same way as a ViewModifier
|
||||
// From: https://github.com/SwiftUIX/SwiftUIX/blob/master/Sources/Intermodular/Extensions/SwiftUI/View%2B%2B.swift#L10
|
||||
public func modifyViewProp(_ body: (inout Self) -> Void) -> Self {
|
||||
var result = self
|
||||
body(&result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: Modifiers
|
||||
|
||||
func conditionalContextMenu(id: some Hashable,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,19 @@ public extension AllDebrid {
|
|||
let link: String
|
||||
}
|
||||
|
||||
// MARK: - SavedLinksResponse
|
||||
|
||||
struct SavedLinksResponse: Codable {
|
||||
let links: [SavedLink]
|
||||
}
|
||||
|
||||
struct SavedLink: Codable, Hashable {
|
||||
let link: String
|
||||
let date: Int
|
||||
let filename: String
|
||||
let size: Int
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailabilityResponse
|
||||
|
||||
struct InstantAvailabilityResponse: Codable {
|
||||
|
|
|
|||
13
Ferrite/Utils/FerriteKeychain.swift
Normal file
13
Ferrite/Utils/FerriteKeychain.swift
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// FerriteKeychain.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 4/30/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainSwift
|
||||
|
||||
class FerriteKeychain {
|
||||
static let shared = KeychainSwift()
|
||||
}
|
||||
|
|
@ -39,8 +39,24 @@ public class DebridManager: ObservableObject {
|
|||
var downloadUrl: String = ""
|
||||
var authUrl: URL?
|
||||
|
||||
// Is the current debrid type processing an auth request
|
||||
func authProcessing(_ passedDebridType: DebridType?) -> Bool {
|
||||
guard let debridType = passedDebridType ?? selectedDebridType else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
return realDebridAuthProcessing
|
||||
case .allDebrid:
|
||||
return allDebridAuthProcessing
|
||||
case .premiumize:
|
||||
return premiumizeAuthProcessing
|
||||
}
|
||||
}
|
||||
|
||||
// RealDebrid auth variables
|
||||
@Published var realDebridAuthProcessing: Bool = false
|
||||
var realDebridAuthProcessing: Bool = false
|
||||
|
||||
// RealDebrid fetch variables
|
||||
@Published var realDebridIAValues: [RealDebrid.IA] = []
|
||||
|
|
@ -58,7 +74,7 @@ public class DebridManager: ObservableObject {
|
|||
var realDebridCloudTTL: Double = 0.0
|
||||
|
||||
// AllDebrid auth variables
|
||||
@Published var allDebridAuthProcessing: Bool = false
|
||||
var allDebridAuthProcessing: Bool = false
|
||||
|
||||
// AllDebrid fetch variables
|
||||
@Published var allDebridIAValues: [AllDebrid.IA] = []
|
||||
|
|
@ -68,10 +84,11 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
// AllDebrid cloud variables
|
||||
@Published var allDebridCloudMagnets: [AllDebrid.MagnetStatusData] = []
|
||||
@Published var allDebridCloudLinks: [AllDebrid.SavedLink] = []
|
||||
var allDebridCloudTTL: Double = 0.0
|
||||
|
||||
// Premiumize auth variables
|
||||
@Published var premiumizeAuthProcessing: Bool = false
|
||||
var premiumizeAuthProcessing: Bool = false
|
||||
|
||||
// Premiumize fetch variables
|
||||
@Published var premiumizeIAValues: [Premiumize.IA] = []
|
||||
|
|
@ -325,34 +342,32 @@ public class DebridManager: ObservableObject {
|
|||
// MARK: - Authentication UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to authenticate with
|
||||
public func authenticateDebrid(debridType: DebridType) async {
|
||||
public func authenticateDebrid(debridType: DebridType, apiKey: String?) async {
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
let success = await authenticateRd()
|
||||
let success = apiKey == nil ? await authenticateRd() : realDebrid.setApiKey(apiKey!)
|
||||
completeDebridAuth(debridType, success: success)
|
||||
case .allDebrid:
|
||||
let success = await authenticateAd()
|
||||
// Async can't work with nil mapping method
|
||||
let success = apiKey == nil ? await authenticateAd() : allDebrid.setApiKey(apiKey!)
|
||||
completeDebridAuth(debridType, success: success)
|
||||
case .premiumize:
|
||||
await authenticatePm()
|
||||
}
|
||||
}
|
||||
|
||||
public func getAuthProcessingBool(debridType: DebridType) -> Bool {
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
return realDebridAuthProcessing
|
||||
case .allDebrid:
|
||||
return allDebridAuthProcessing
|
||||
case .premiumize:
|
||||
return premiumizeAuthProcessing
|
||||
if let apiKey {
|
||||
let success = premiumize.setApiKey(apiKey)
|
||||
completeDebridAuth(debridType, success: success)
|
||||
} else {
|
||||
await authenticatePm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback to finish debrid auth since functions can be split
|
||||
func completeDebridAuth(_ debridType: DebridType, success: Bool = true) {
|
||||
if enabledDebrids.count == 1, success {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
func completeDebridAuth(_ debridType: DebridType, success: Bool) {
|
||||
if success {
|
||||
enabledDebrids.insert(debridType)
|
||||
if enabledDebrids.count == 1 {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
}
|
||||
}
|
||||
|
||||
switch debridType {
|
||||
|
|
@ -365,6 +380,47 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// Get a truncated manual API key if it's being used
|
||||
func getManualAuthKey(_ passedDebridType: DebridType?) async -> String? {
|
||||
guard let debridType = passedDebridType ?? selectedDebridType else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let debridToken: String?
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") {
|
||||
debridToken = FerriteKeychain.shared.get("RealDebrid.AccessToken")
|
||||
} else {
|
||||
debridToken = nil
|
||||
}
|
||||
case .allDebrid:
|
||||
if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") {
|
||||
debridToken = FerriteKeychain.shared.get("AllDebrid.ApiKey")
|
||||
} else {
|
||||
debridToken = nil
|
||||
}
|
||||
case .premiumize:
|
||||
if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") {
|
||||
debridToken = FerriteKeychain.shared.get("Premiumize.AccessToken")
|
||||
} else {
|
||||
debridToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
if let debridToken {
|
||||
let splitString = debridToken.suffix(4)
|
||||
|
||||
if debridToken.count > 4 {
|
||||
return String(repeating: "*", count: debridToken.count - 4) + splitString
|
||||
} else {
|
||||
return String(splitString)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
@ -389,12 +445,10 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
if validateAuthUrl(URL(string: verificationResponse.directVerificationURL)) {
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
enabledDebrids.insert(.realDebrid)
|
||||
return true
|
||||
} else {
|
||||
throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid")
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid authentication error")
|
||||
|
||||
|
|
@ -410,12 +464,10 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
if validateAuthUrl(URL(string: pinResponse.userURL)) {
|
||||
try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin)
|
||||
enabledDebrids.insert(.allDebrid)
|
||||
return true
|
||||
} else {
|
||||
throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid")
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid authentication error")
|
||||
|
||||
|
|
@ -446,8 +498,7 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
if let callbackUrl = url {
|
||||
try premiumize.handleAuthCallback(url: callbackUrl)
|
||||
enabledDebrids.insert(.premiumize)
|
||||
completeDebridAuth(.premiumize)
|
||||
completeDebridAuth(.premiumize, success: true)
|
||||
} else {
|
||||
throw Premiumize.PMError.AuthQuery(description: "The callback URL was invalid")
|
||||
}
|
||||
|
|
@ -528,19 +579,6 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func fetchDebridCloud() async {
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
await fetchRdCloud()
|
||||
case .allDebrid:
|
||||
await fetchAdCloud()
|
||||
case .premiumize:
|
||||
await fetchPmCloud()
|
||||
case .none:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRdDownload(magnet: Magnet?, existingLink: String?) async {
|
||||
// If an existing link is passed in args, set it to that. Otherwise, find one from RD cloud.
|
||||
let torrentLink: String?
|
||||
|
|
@ -609,6 +647,19 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func fetchDebridCloud(bypassTTL: Bool = false) async {
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
await fetchRdCloud(bypassTTL: bypassTTL)
|
||||
case .allDebrid:
|
||||
await fetchAdCloud(bypassTTL: bypassTTL)
|
||||
case .premiumize:
|
||||
await fetchPmCloud(bypassTTL: bypassTTL)
|
||||
case .none:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshes torrents and downloads from a RD user's account
|
||||
public func fetchRdCloud(bypassTTL: Bool = false) async {
|
||||
if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL {
|
||||
|
|
@ -664,6 +715,7 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Integrate with AD saved links
|
||||
func fetchAdDownload(magnet: Magnet?, existingLockedLink: String?) async {
|
||||
// If an existing link is passed in args, set it to that. Otherwise, find one from AD cloud.
|
||||
let lockedLink: String?
|
||||
|
|
@ -678,8 +730,10 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
|
||||
do {
|
||||
if let lockedLink {
|
||||
downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink)
|
||||
if let lockedLink,
|
||||
let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink)
|
||||
{
|
||||
downloadUrl = unlockedLink
|
||||
} else if let magnet {
|
||||
let magnetID = try await allDebrid.addMagnet(magnet: magnet)
|
||||
let lockedLink = try await allDebrid.fetchMagnetStatus(
|
||||
|
|
@ -687,6 +741,7 @@ public class DebridManager: ObservableObject {
|
|||
selectedIndex: selectedAllDebridFile?.id ?? 0
|
||||
)
|
||||
|
||||
try await allDebrid.saveLink(link: lockedLink)
|
||||
downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink)
|
||||
} else {
|
||||
throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API")
|
||||
|
|
@ -699,11 +754,28 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func checkAdUserLinks(lockedLink: String) async -> String? {
|
||||
do {
|
||||
let existingLinks = allDebridCloudLinks.first { $0.link == lockedLink }
|
||||
if let existingLink = existingLinks?.link {
|
||||
return existingLink
|
||||
} else {
|
||||
try await allDebrid.saveLink(link: lockedLink)
|
||||
return try await allDebrid.unlockLink(lockedLink: lockedLink)
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid download check error")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshes torrents and downloads from a RD user's account
|
||||
public func fetchAdCloud(bypassTTL: Bool = false) async {
|
||||
if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL {
|
||||
do {
|
||||
allDebridCloudMagnets = try await allDebrid.userMagnets()
|
||||
allDebridCloudLinks = try await allDebrid.savedLinks()
|
||||
|
||||
// 5 minutes
|
||||
allDebridCloudTTL = Date().timeIntervalSince1970 + 300
|
||||
|
|
@ -713,13 +785,23 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func deleteAdLink(link: String) async {
|
||||
do {
|
||||
try await allDebrid.deleteLink(link: link)
|
||||
|
||||
await fetchAdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid link delete error")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAdMagnet(magnetId: Int) async {
|
||||
do {
|
||||
try await allDebrid.deleteMagnet(magnetId: magnetId)
|
||||
|
||||
await fetchAdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid delete error")
|
||||
await sendDebridError(error, prefix: "AllDebrid magnet delete error")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,22 +14,34 @@ struct HybridSecureField: View {
|
|||
}
|
||||
|
||||
@Binding var text: String
|
||||
var onCommit: () -> Void = {}
|
||||
|
||||
@State private var showPassword = false
|
||||
@FocusState private var focusedField: Field?
|
||||
private var isFieldDisabled: Bool = false
|
||||
|
||||
init(text: Binding<String>, onCommit: (() -> Void)? = nil, showPassword: Bool = false) {
|
||||
self._text = text
|
||||
if let onCommit {
|
||||
self.onCommit = onCommit
|
||||
}
|
||||
self.showPassword = showPassword
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Group {
|
||||
if showPassword {
|
||||
TextField("Password", text: $text)
|
||||
TextField("Password", text: $text, onCommit: onCommit)
|
||||
.focused($focusedField, equals: .plain)
|
||||
} else {
|
||||
SecureField("Password", text: $text)
|
||||
SecureField("Password", text: $text, onCommit: onCommit)
|
||||
.focused($focusedField, equals: .secure)
|
||||
}
|
||||
}
|
||||
.autocorrectionDisabled(true)
|
||||
.autocapitalization(.none)
|
||||
.disabledAppearance(isFieldDisabled)
|
||||
|
||||
Button {
|
||||
showPassword.toggle()
|
||||
|
|
@ -42,3 +54,9 @@ struct HybridSecureField: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HybridSecureField {
|
||||
public func fieldDisabled(_ isFieldDisabled: Bool) -> Self {
|
||||
modifyViewProp({ $0.isFieldDisabled = isFieldDisabled })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,43 @@ struct AllDebridCloudView: View {
|
|||
@Binding var searchText: String
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Links") {
|
||||
ForEach(debridManager.allDebridCloudLinks.filter {
|
||||
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.self) { downloadResponse in
|
||||
Button(downloadResponse.filename) {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = downloadResponse.filename
|
||||
debridManager.downloadUrl = downloadResponse.link
|
||||
|
||||
PersistenceController.shared.createHistory(
|
||||
HistoryEntryJson(
|
||||
name: downloadResponse.filename,
|
||||
url: downloadResponse.link,
|
||||
source: DebridType.allDebrid.toString()
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let savedLink = debridManager.allDebridCloudLinks[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteAdLink(link: savedLink.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisclosureGroup("Magnets") {
|
||||
ForEach(debridManager.allDebridCloudMagnets.filter {
|
||||
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
|
||||
|
|
@ -69,8 +106,8 @@ struct AllDebridCloudView: View {
|
|||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.black)
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.9, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ struct PremiumizeCloudView: View {
|
|||
}
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.black)
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ struct RealDebridCloudView: View {
|
|||
navModel: navModel
|
||||
)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ struct DebridCloudView: View {
|
|||
await debridManager.fetchDebridCloud()
|
||||
}
|
||||
.refreshable {
|
||||
await debridManager.fetchDebridCloud()
|
||||
await debridManager.fetchDebridCloud(bypassTTL: true)
|
||||
}
|
||||
.onChange(of: debridManager.selectedDebridType) { newType in
|
||||
if newType != nil {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ struct SettingsDebridInfoView: View {
|
|||
|
||||
let debridType: DebridType
|
||||
|
||||
@State private var apiKeyTempText: String = ""
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: InlineHeader("Description")) {
|
||||
|
|
@ -30,19 +32,44 @@ struct SettingsDebridInfoView: View {
|
|||
Task {
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
await debridManager.logoutDebrid(debridType: debridType)
|
||||
} else if !debridManager.getAuthProcessingBool(debridType: debridType) {
|
||||
await debridManager.authenticateDebrid(debridType: debridType)
|
||||
} else if !debridManager.authProcessing(debridType) {
|
||||
await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil)
|
||||
}
|
||||
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
|
||||
}
|
||||
} label: {
|
||||
Text(
|
||||
debridManager.enabledDebrids.contains(debridType)
|
||||
? "Logout"
|
||||
: (debridManager.getAuthProcessingBool(debridType: debridType) ? "Processing" : "Login")
|
||||
: (debridManager.authProcessing(debridType) ? "Processing" : "Login")
|
||||
)
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(debridType) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
|
||||
Section(
|
||||
header: InlineHeader("API key"),
|
||||
footer: Text("Add a permanent API key here. Only use this if web authentication does not work!")
|
||||
) {
|
||||
HybridSecureField(
|
||||
text: $apiKeyTempText,
|
||||
onCommit: {
|
||||
Task {
|
||||
if !apiKeyTempText.isEmpty {
|
||||
await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText)
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.fieldDisabled(debridManager.enabledDebrids.contains(debridType))
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(debridType.toString())
|
||||
|
|
|
|||
|
|
@ -9,19 +9,23 @@ import SwiftUI
|
|||
import WebKit
|
||||
|
||||
struct WebView: UIViewRepresentable {
|
||||
@AppStorage("Behavior.UseEphemeralAuth") var useEphemeralAuth: Bool = true
|
||||
var url: URL
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
// Make the WebView ephemeral
|
||||
// Make the WebView ephemeral depending on the ephemeral auth setting
|
||||
let config = WKWebViewConfiguration()
|
||||
config.websiteDataStore = WKWebsiteDataStore.nonPersistent()
|
||||
|
||||
config.websiteDataStore = useEphemeralAuth ? .nonPersistent() : .default()
|
||||
|
||||
let webView = WKWebView(frame: .zero, configuration: config)
|
||||
let _ = webView.load(URLRequest(url: url))
|
||||
return webView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: WKWebView, context: Context) {}
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
webView.configuration.websiteDataStore = useEphemeralAuth ? .nonPersistent() : .default()
|
||||
}
|
||||
}
|
||||
|
||||
struct WebView_Previews: PreviewProvider {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import BetterSafariView
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
|
@ -24,6 +25,7 @@ struct SettingsView: View {
|
|||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText = false
|
||||
@AppStorage("Behavior.UseEphemeralAuth") var useEphemeralAuth = true
|
||||
@AppStorage("Behavior.DisableRequestTimeout") var disableRequestTimeout = false
|
||||
@AppStorage("Behavior.RequestTimeoutSecs") var requestTimeoutSecs: Double = 15
|
||||
|
||||
|
|
@ -73,7 +75,10 @@ struct SettingsView: View {
|
|||
|
||||
Section(
|
||||
header: InlineHeader("Behavior"),
|
||||
footer: Text("Only disable search timeout if results are slow to fetch")
|
||||
footer: VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Temporarily disable ephemeral auth if you cannot log into a service")
|
||||
Text("Only disable search timeout if results are slow to fetch")
|
||||
}
|
||||
) {
|
||||
Toggle(isOn: $autocorrectSearch) {
|
||||
Text("Autocorrect search")
|
||||
|
|
@ -83,6 +88,21 @@ struct SettingsView: View {
|
|||
Text("Random searchbar text")
|
||||
}
|
||||
|
||||
Toggle(isOn: $useEphemeralAuth) {
|
||||
Text("Ephemeral authentication")
|
||||
}
|
||||
.onChange(of: useEphemeralAuth) { changed in
|
||||
// Does not work with ASWebAuthenticationSession
|
||||
if changed {
|
||||
Task {
|
||||
let dataRecords = await WKWebsiteDataStore.default().dataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes())
|
||||
|
||||
await WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: dataRecords)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Change this to enable search timeout instead
|
||||
Toggle(isOn: $disableRequestTimeout) {
|
||||
Text("Disable search timeout")
|
||||
}
|
||||
|
|
@ -210,7 +230,7 @@ struct SettingsView: View {
|
|||
await debridManager.handleCallback(url: callbackURL, error: error)
|
||||
}
|
||||
}
|
||||
.prefersEphemeralWebBrowserSession(true)
|
||||
.prefersEphemeralWebBrowserSession(useEphemeralAuth)
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
|
|
|
|||
Loading…
Reference in a new issue