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:
kingbri 2024-06-02 12:31:19 -04:00 committed by Brian Dashore
parent b1227db143
commit b8a225e141
7 changed files with 140 additions and 64 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,16 @@
//
// DebridModels.swift
// Ferrite
//
// Created by Brian Dashore on 6/2/24.
//
import Foundation
public struct DebridIAFile {
}
public struct DebridCloudFile {
}

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

View file

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