Debrid: Begin using common protocols
Unifying the debrid services under a protocol will help slim down on excess redundant code and allow for easy addition of new services in the future. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
b1227db143
commit
b8a225e141
7 changed files with 140 additions and 64 deletions
|
|
@ -154,6 +154,8 @@
|
|||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; };
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; };
|
||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */; };
|
||||
0CF1ABDC2C0C04B2009F6C26 /* Debrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */; };
|
||||
0CF1ABE22C0C3D2F009F6C26 /* DebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */; };
|
||||
0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF2C0A429D1EBD400E716DD /* UIApplication.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -300,6 +302,8 @@
|
|||
0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = "<group>"; };
|
||||
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterHeaderView.swift; sourceTree = "<group>"; };
|
||||
0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPickerView.swift; sourceTree = "<group>"; };
|
||||
0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debrid.swift; sourceTree = "<group>"; };
|
||||
0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridModels.swift; sourceTree = "<group>"; };
|
||||
0CF2C0A429D1EBD400E716DD /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -399,6 +403,7 @@
|
|||
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
|
||||
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */,
|
||||
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */,
|
||||
0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -492,6 +497,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0CE1C4172981E8D700418F20 /* Plugin.swift */,
|
||||
0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -923,6 +929,7 @@
|
|||
0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */,
|
||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
||||
0CC2CA7429E24F63000A8585 /* ExpandedSearchable.swift in Sources */,
|
||||
0CF1ABE22C0C3D2F009F6C26 /* DebridModels.swift in Sources */,
|
||||
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
|
||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||
|
|
@ -942,6 +949,7 @@
|
|||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
|
||||
0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */,
|
||||
0CF1ABDC2C0C04B2009F6C26 /* Debrid.swift in Sources */,
|
||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -8,24 +8,36 @@
|
|||
import Foundation
|
||||
|
||||
// TODO: Fix errors
|
||||
public class AllDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
public class AllDebrid: PollingDebridSource {
|
||||
|
||||
public let id = "AllDebrid"
|
||||
public var authTask: Task<Void, Error>?
|
||||
|
||||
let baseApiUrl = "https://api.alldebrid.com/v4"
|
||||
let appName = "Ferrite"
|
||||
|
||||
var authTask: Task<Void, Error>?
|
||||
let jsonDecoder = JSONDecoder()
|
||||
|
||||
// Fetches information for PIN auth
|
||||
public func getPinInfo() async throws -> PinResponse {
|
||||
public func getAuthUrl() async throws -> URL {
|
||||
let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get")
|
||||
let request = URLRequest(url: url)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<PinResponse>.self, from: data).data
|
||||
|
||||
return rawResponse
|
||||
// Validate the URL before doing anything else
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<PinResponse>.self, from: data).data
|
||||
guard let userUrl = URL(string: rawResponse.userURL) else {
|
||||
throw ADError.AuthQuery(description: "The login URL is invalid")
|
||||
}
|
||||
|
||||
// Spawn the polling task separately
|
||||
authTask = Task {
|
||||
try await getApiKey(checkID: rawResponse.check, pin: rawResponse.pin)
|
||||
}
|
||||
|
||||
return userUrl
|
||||
} catch {
|
||||
print("Couldn't get pin information!")
|
||||
throw ADError.AuthQuery(description: error.localizedDescription)
|
||||
|
|
@ -88,7 +100,7 @@ public class AllDebrid {
|
|||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
public func logout() {
|
||||
FerriteKeychain.shared.delete("AllDebrid.ApiKey")
|
||||
UserDefaults.standard.removeObject(forKey: "AllDebrid.UseManualKey")
|
||||
}
|
||||
|
|
@ -110,7 +122,7 @@ public class AllDebrid {
|
|||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
deleteTokens()
|
||||
logout()
|
||||
throw ADError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.")
|
||||
} else {
|
||||
throw ADError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
|
|
|
|||
|
|
@ -7,14 +7,17 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class Premiumize {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
public class Premiumize: OAuthDebridSource {
|
||||
|
||||
public let id = "Premiumize"
|
||||
|
||||
let baseAuthUrl = "https://www.premiumize.me/authorize"
|
||||
let baseApiUrl = "https://www.premiumize.me/api"
|
||||
let clientId = "791565696"
|
||||
|
||||
public func buildAuthUrl() throws -> URL {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
|
||||
public func getAuthUrl() throws -> URL {
|
||||
var urlComponents = URLComponents(string: baseAuthUrl)!
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: clientId),
|
||||
|
|
@ -59,7 +62,7 @@ public class Premiumize {
|
|||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
public func logout() {
|
||||
FerriteKeychain.shared.delete("Premiumize.AccessToken")
|
||||
UserDefaults.standard.removeObject(forKey: "Premiumize.UseManualKey")
|
||||
}
|
||||
|
|
@ -101,7 +104,7 @@ public class Premiumize {
|
|||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
deleteTokens()
|
||||
logout()
|
||||
throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
|
||||
} else {
|
||||
throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
|
|
|
|||
|
|
@ -7,14 +7,16 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class RealDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
public class RealDebrid: PollingDebridSource {
|
||||
|
||||
public let id = "RealDebrid"
|
||||
public var authTask: Task<Void, Error>?
|
||||
|
||||
let baseAuthUrl = "https://api.real-debrid.com/oauth/v2"
|
||||
let baseApiUrl = "https://api.real-debrid.com/rest/1.0"
|
||||
let openSourceClientId = "X245A4XAIBGVM"
|
||||
|
||||
var authTask: Task<Void, Error>?
|
||||
let jsonDecoder = JSONDecoder()
|
||||
|
||||
@MainActor
|
||||
func setUserDefaultsValue(_ value: Any, forKey: String) {
|
||||
|
|
@ -27,7 +29,7 @@ public class RealDebrid {
|
|||
}
|
||||
|
||||
// Fetches the device code from RD
|
||||
public func getVerificationInfo() async throws -> DeviceCodeResponse {
|
||||
public func getAuthUrl() async throws -> URL {
|
||||
var urlComponents = URLComponents(string: "\(baseAuthUrl)/device/code")!
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: openSourceClientId),
|
||||
|
|
@ -42,8 +44,18 @@ public class RealDebrid {
|
|||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// Validate the URL before doing anything else
|
||||
let rawResponse = try jsonDecoder.decode(DeviceCodeResponse.self, from: data)
|
||||
return rawResponse
|
||||
guard let directVerificationUrl = URL(string: rawResponse.directVerificationURL) else {
|
||||
throw RDError.AuthQuery(description: "The verification URL is invalid")
|
||||
}
|
||||
|
||||
// Spawn the polling task separately
|
||||
authTask = Task {
|
||||
try await getDeviceCredentials(deviceCode: rawResponse.deviceCode)
|
||||
}
|
||||
|
||||
return directVerificationUrl
|
||||
} catch {
|
||||
print("Couldn't get the new client creds!")
|
||||
throw RDError.AuthQuery(description: error.localizedDescription)
|
||||
|
|
@ -65,39 +77,33 @@ public class RealDebrid {
|
|||
let request = URLRequest(url: url)
|
||||
|
||||
// Timer to poll RD API for credentials
|
||||
authTask = Task {
|
||||
var count = 0
|
||||
var count = 0
|
||||
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw RDError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// We don't care if this fails
|
||||
let rawResponse = try? self.jsonDecoder.decode(DeviceCredentialsResponse.self, from: data)
|
||||
|
||||
// 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")
|
||||
FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
|
||||
try await getTokens(deviceCode: deviceCode)
|
||||
|
||||
return
|
||||
} else {
|
||||
try await Task.sleep(seconds: 5)
|
||||
count += 1
|
||||
}
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw RDError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// We don't care if this fails
|
||||
let rawResponse = try? self.jsonDecoder.decode(DeviceCredentialsResponse.self, from: data)
|
||||
|
||||
// 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")
|
||||
FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
|
||||
try await getTokens(deviceCode: deviceCode)
|
||||
|
||||
return
|
||||
} else {
|
||||
try await Task.sleep(seconds: 5)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
if case let .failure(error) = await authTask?.result {
|
||||
throw error
|
||||
}
|
||||
throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
}
|
||||
|
||||
// Fetch all tokens for the user and store in FerriteKeychain.shared
|
||||
|
|
@ -163,8 +169,9 @@ public class RealDebrid {
|
|||
|
||||
return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key
|
||||
}
|
||||
|
||||
public func deleteTokens() async throws {
|
||||
|
||||
// Deletes tokens from device and RD's servers
|
||||
public func logout() async {
|
||||
FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.ClientSecret")
|
||||
await removeUserDefaultsValue(forKey: "RealDebrid.ClientId")
|
||||
|
|
@ -198,7 +205,7 @@ public class RealDebrid {
|
|||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
try await deleteTokens()
|
||||
await logout()
|
||||
throw RDError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
|
||||
} else {
|
||||
throw RDError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
|
|
|
|||
16
Ferrite/Models/DebridModels.swift
Normal file
16
Ferrite/Models/DebridModels.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// DebridModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 6/2/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct DebridIAFile {
|
||||
|
||||
}
|
||||
|
||||
public struct DebridCloudFile {
|
||||
|
||||
}
|
||||
34
Ferrite/Protocols/Debrid.swift
Normal file
34
Ferrite/Protocols/Debrid.swift
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Debrid.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 6/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DebridSource {
|
||||
// ID of the service
|
||||
var id: String { get }
|
||||
|
||||
// Common authentication functions
|
||||
func setApiKey(_ key: String) -> Bool
|
||||
func logout() async
|
||||
}
|
||||
|
||||
public protocol PollingDebridSource: DebridSource {
|
||||
// Task reference for polling
|
||||
var authTask: Task<Void, Error>? { get set }
|
||||
|
||||
// Fetches the Auth URL
|
||||
func getAuthUrl() async throws -> URL
|
||||
}
|
||||
|
||||
public protocol OAuthDebridSource: DebridSource {
|
||||
|
||||
// Fetches the auth URL
|
||||
func getAuthUrl() throws -> URL
|
||||
|
||||
// Handles an OAuth callback
|
||||
func handleAuthCallback(url: URL) throws
|
||||
}
|
||||
|
|
@ -450,10 +450,10 @@ public class DebridManager: ObservableObject {
|
|||
private func authenticateRd() async -> Bool {
|
||||
do {
|
||||
realDebridAuthProcessing = true
|
||||
let verificationResponse = try await realDebrid.getVerificationInfo()
|
||||
let authUrl = try await realDebrid.getAuthUrl()
|
||||
|
||||
if validateAuthUrl(URL(string: verificationResponse.directVerificationURL)) {
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
if validateAuthUrl(authUrl) {
|
||||
try await realDebrid.authTask?.value
|
||||
return true
|
||||
} else {
|
||||
throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid")
|
||||
|
|
@ -469,10 +469,10 @@ public class DebridManager: ObservableObject {
|
|||
private func authenticateAd() async -> Bool {
|
||||
do {
|
||||
allDebridAuthProcessing = true
|
||||
let pinResponse = try await allDebrid.getPinInfo()
|
||||
let authUrl = try await allDebrid.getAuthUrl()
|
||||
|
||||
if validateAuthUrl(URL(string: pinResponse.userURL)) {
|
||||
try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin)
|
||||
if validateAuthUrl(authUrl) {
|
||||
try await allDebrid.authTask?.value
|
||||
return true
|
||||
} else {
|
||||
throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid")
|
||||
|
|
@ -488,7 +488,7 @@ public class DebridManager: ObservableObject {
|
|||
private func authenticatePm() async {
|
||||
do {
|
||||
premiumizeAuthProcessing = true
|
||||
let tempAuthUrl = try premiumize.buildAuthUrl()
|
||||
let tempAuthUrl = try premiumize.getAuthUrl()
|
||||
|
||||
validateAuthUrl(tempAuthUrl, useAuthSession: true)
|
||||
} catch {
|
||||
|
|
@ -538,16 +538,12 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
|
||||
private func logoutRd() async {
|
||||
do {
|
||||
try await realDebrid.deleteTokens()
|
||||
enabledDebrids.remove(.realDebrid)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid logout error")
|
||||
}
|
||||
await realDebrid.logout()
|
||||
enabledDebrids.remove(.realDebrid)
|
||||
}
|
||||
|
||||
private func logoutAd() {
|
||||
allDebrid.deleteTokens()
|
||||
allDebrid.logout()
|
||||
enabledDebrids.remove(.allDebrid)
|
||||
|
||||
logManager?.info(
|
||||
|
|
@ -557,7 +553,7 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
|
||||
private func logoutPm() {
|
||||
premiumize.deleteTokens()
|
||||
premiumize.logout()
|
||||
enabledDebrids.remove(.premiumize)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue