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>
203 lines
7.5 KiB
Swift
203 lines
7.5 KiB
Swift
//
|
|
// AllDebridWrapper.swift
|
|
// Ferrite
|
|
//
|
|
// Created by Brian Dashore on 11/25/22.
|
|
//
|
|
|
|
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"
|
|
|
|
var authTask: Task<Void, Error>?
|
|
|
|
// Fetches information for PIN auth
|
|
public func getPinInfo() async throws -> PinResponse {
|
|
let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get")
|
|
print("Auth URL: \(url)")
|
|
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
|
|
} catch {
|
|
print("Couldn't get pin information!")
|
|
throw ADError.AuthQuery(description: error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// Fetches API keys
|
|
public func getApiKey(checkID: String, pin: String) async throws {
|
|
let queryItems = [
|
|
URLQueryItem(name: "agent", value: appName),
|
|
URLQueryItem(name: "check", value: checkID),
|
|
URLQueryItem(name: "pin", value: pin)
|
|
]
|
|
|
|
let request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/pin/check", queryItems: queryItems))
|
|
|
|
// Timer to poll AD API for key
|
|
authTask = Task {
|
|
var count = 0
|
|
|
|
while count < 12 {
|
|
if Task.isCancelled {
|
|
throw ADError.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(ADResponse<ApiKeyResponse>.self, from: data).data
|
|
|
|
// If there's an API key from the response, end the task successfully
|
|
if let apiKeyResponse = rawResponse {
|
|
keychain.set(apiKeyResponse.apikey, forKey: "AllDebrid.ApiKey")
|
|
|
|
return
|
|
} else {
|
|
try await Task.sleep(seconds: 5)
|
|
count += 1
|
|
}
|
|
}
|
|
|
|
throw ADError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
|
}
|
|
|
|
if case let .failure(error) = await authTask?.result {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Clears tokens. No endpoint to deregister a device
|
|
public func deleteTokens() {
|
|
keychain.delete("AllDebrid.ApiKey")
|
|
}
|
|
|
|
// 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 {
|
|
throw ADError.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 ADError.FailedRequest(description: "No HTTP response given")
|
|
}
|
|
|
|
if response.statusCode >= 200, response.statusCode <= 299 {
|
|
return data
|
|
} else if response.statusCode == 401 {
|
|
deleteTokens()
|
|
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).")
|
|
}
|
|
}
|
|
|
|
// Builds a URL for further requests
|
|
private func buildRequestURL(urlString: String, queryItems: [URLQueryItem] = []) throws -> URL {
|
|
guard var components = URLComponents(string: urlString) else {
|
|
throw ADError.InvalidUrl
|
|
}
|
|
|
|
components.queryItems = [
|
|
URLQueryItem(name: "agent", value: appName)
|
|
] + queryItems
|
|
|
|
if let url = components.url {
|
|
return url
|
|
} else {
|
|
throw ADError.InvalidUrl
|
|
}
|
|
}
|
|
|
|
// Adds a magnet link to the user's AD account
|
|
public func addMagnet(magnetLink: String) async throws -> Int {
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload"))
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
|
|
|
var bodyComponents = URLComponents()
|
|
bodyComponents.queryItems = [
|
|
URLQueryItem(name: "magnets[]", value: magnetLink)
|
|
]
|
|
|
|
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
|
|
|
let data = try await performRequest(request: &request, requestName: #function)
|
|
let rawResponse = try jsonDecoder.decode(ADResponse<AddMagnetResponse>.self, from: data).data
|
|
|
|
if let magnet = rawResponse.magnets[safe: 0] {
|
|
return magnet.id
|
|
} else {
|
|
throw ADError.InvalidResponse
|
|
}
|
|
}
|
|
|
|
public func fetchMagnetStatus(magnetId: Int, selectedIndex: Int?) async throws -> String {
|
|
let queryItems = [
|
|
URLQueryItem(name: "id", value: String(magnetId))
|
|
]
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems))
|
|
|
|
let data = try await performRequest(request: &request, requestName: #function)
|
|
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
|
|
|
|
// Better to fetch no link at all than the wrong link
|
|
if let linkWrapper = rawResponse.magnets.links[safe: selectedIndex ?? -1] {
|
|
return linkWrapper.link
|
|
} else {
|
|
throw ADError.EmptyTorrents
|
|
}
|
|
}
|
|
|
|
public func unlockLink(lockedLink: String) async throws -> String {
|
|
let queryItems = [
|
|
URLQueryItem(name: "link", value: lockedLink)
|
|
]
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/link/unlock", queryItems: queryItems))
|
|
print(request)
|
|
|
|
let data = try await performRequest(request: &request, requestName: #function)
|
|
let rawResponse = try jsonDecoder.decode(ADResponse<UnlockLinkResponse>.self, from: data).data
|
|
|
|
return rawResponse.link
|
|
}
|
|
|
|
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)
|
|
let rawResponse = try jsonDecoder.decode(ADResponse<InstantAvailabilityResponse>.self, from: data).data
|
|
|
|
let filteredMagnets = rawResponse.magnets.filter { $0.instant == true && $0.files != nil }
|
|
let availableHashes = filteredMagnets.map { magnetResp in
|
|
// Force unwrap is OK here since the filter caught any nil values
|
|
let files = magnetResp.files!.enumerated().map { index, magnetFile in
|
|
IAFile(id: index, fileName: magnetFile.name)
|
|
}
|
|
|
|
return IA(
|
|
hash: magnetResp.hash,
|
|
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
|
files: files
|
|
)
|
|
}
|
|
|
|
return availableHashes
|
|
}
|
|
}
|