Ferrite-backup/Ferrite/API/PremiumizeWrapper.swift
kingbri 78f2aff25b Debrid: Fix OffCloud single files and cloud population
Populate cloud lists when the app is launched to begin maintainence
of a synced list. In addition, fix the errors when OffCloud tried
fetching links for a single file. The explore endpoint only works
when the file is a batch which is unknown until it's actually called.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-16 21:12:26 -05:00

381 lines
13 KiB
Swift

//
// PremiumizeWrapper.swift
// Ferrite
//
// Created by Brian Dashore on 11/28/22.
//
import Foundation
class Premiumize: OAuthDebridSource, ObservableObject {
let id = "Premiumize"
let abbreviation = "PM"
let website = "https://premiumize.me"
let description: String? = "Premiumize is a debrid service that is used for downloads and media playback with seeding. " +
"You must pay to access the service."
@Published var authProcessing: Bool = false
var isLoggedIn: Bool {
getToken() != nil
}
var manualToken: String? {
if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") {
return getToken()
} else {
return nil
}
}
@Published var IAValues: [DebridIA] = []
@Published var cloudDownloads: [DebridCloudDownload] = []
@Published var cloudMagnets: [DebridCloudMagnet] = []
var cloudTTL: Double = 0.0
private let baseAuthUrl = "https://www.premiumize.me/authorize"
private let baseApiUrl = "https://www.premiumize.me/api"
private let clientId = "791565696"
private let jsonDecoder = JSONDecoder()
init() {
// Populate user downloads and magnets
Task {
try? await getUserDownloads()
}
}
// MARK: - Auth
func getAuthUrl() 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 DebridError.InvalidUrl
}
}
func handleAuthCallback(url: URL) throws {
let callbackComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let callbackFragment = callbackComponents?.fragment else {
throw DebridError.InvalidResponse
}
var fragmentComponents = URLComponents()
fragmentComponents.query = callbackFragment
guard let accessToken = fragmentComponents.queryItems?.first(where: { $0.name == "access_token" })?.value else {
throw DebridError.InvalidToken
}
FerriteKeychain.shared.set(accessToken, forKey: "Premiumize.AccessToken")
}
// Adds a manual API key instead of web auth
func setApiKey(_ key: String) {
FerriteKeychain.shared.set(key, forKey: "Premiumize.AccessToken")
UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey")
}
func getToken() -> String? {
FerriteKeychain.shared.get("Premiumize.AccessToken")
}
// Clears tokens. No endpoint to deregister a device
func logout() {
FerriteKeychain.shared.delete("Premiumize.AccessToken")
UserDefaults.standard.removeObject(forKey: "Premiumize.UseManualKey")
}
// MARK: - Common request
// 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 = getToken() else {
throw DebridError.InvalidToken
}
// Use the API query parameter if a manual API key is present
if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") {
guard
let requestUrl = request.url,
var components = URLComponents(url: requestUrl, resolvingAgainstBaseURL: false)
else {
throw DebridError.InvalidUrl
}
let apiTokenItem = URLQueryItem(name: "apikey", value: token)
if components.queryItems == nil {
components.queryItems = [apiTokenItem]
} else {
components.queryItems?.append(apiTokenItem)
}
request.url = components.url
} else {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw DebridError.FailedRequest(description: "No HTTP response given")
}
if response.statusCode >= 200, response.statusCode <= 299 {
return data
} else if response.statusCode == 401 {
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
} else {
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
}
}
// MARK: - Instant availability
func instantAvailability(magnets: [Magnet]) async throws {
let now = Date().timeIntervalSince1970
// Remove magnets that don't have an associated link for PM along with existing TTL logic
let sendMagnets = magnets.filter { magnet in
if magnet.link == nil {
return false
}
if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) {
if now > IAValues[IAIndex].expiryTimeStamp {
IAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else {
return true
}
}
if sendMagnets.isEmpty {
return
}
let availableMagnets = try await divideCacheRequests(magnets: sendMagnets)
// Split DDL requests into chunks of 10
for chunk in availableMagnets.chunked(into: 10) {
let tempIA = try await divideDDLRequests(magnetChunk: chunk)
IAValues += tempIA
}
}
// Function to divide and execute DDL endpoint requests in parallel
// Calls this for 10 requests at a time to not overwhelm API servers
func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [DebridIA] {
let tempIA = try await withThrowingTaskGroup(of: DebridIA.self) { group in
for magnet in magnetChunk {
group.addTask {
try await self.fetchDDL(magnet: magnet)
}
}
var chunkedIA: [DebridIA] = []
for try await ia in group {
chunkedIA.append(ia)
}
return chunkedIA
}
return tempIA
}
// Grabs DDL links
private func fetchDDL(magnet: Magnet) async throws -> DebridIA {
if magnet.hash == nil {
throw DebridError.EmptyData
}
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)
let content = rawResponse.content ?? []
if !content.isEmpty {
let files = content.map { file in
DebridIAFile(
id: 0,
name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path,
streamUrlString: file.link
)
}
return DebridIA(
magnet: magnet,
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files
)
} else {
throw DebridError.EmptyData
}
}
// Function to divide and execute cache endpoint requests in parallel
// Calls this for 100 hashes at a time due to API limits
func divideCacheRequests(magnets: [Magnet]) async throws -> [Magnet] {
let availableMagnets = try await withThrowingTaskGroup(of: [Magnet].self) { group in
for chunk in magnets.chunked(into: 100) {
group.addTask {
try await self.checkCache(magnets: chunk)
}
}
var chunkedMagnets: [Magnet] = []
for try await magnetArray in group {
chunkedMagnets += magnetArray
}
return chunkedMagnets
}
return availableMagnets
}
// Parent function for initial checking of the cache
private 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 DebridError.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 DebridError.EmptyData
} else {
let availableMagnets = magnets.enumerated().compactMap { index, magnet in
if rawResponse.response[safe: index] == true {
return magnet
} else {
return nil
}
}
return availableMagnets
}
}
// MARK: - Downloading
func getRestrictedFile(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> (restrictedFile: DebridIAFile?, newIA: DebridIA?) {
// Store the item in PM cloud for later use
try await createTransfer(magnet: magnet)
if let iaFile {
return (iaFile, nil)
} else if let premiumizeItem = ia, let firstFile = premiumizeItem.files[safe: 0] {
return (firstFile, nil)
} else {
throw DebridError.FailedRequest(description: "Could not fetch your file from the Premiumize API")
}
}
func unrestrictFile(_ restrictedFile: DebridIAFile) async throws -> String {
guard let streamUrlString = restrictedFile.streamUrlString else {
throw DebridError.FailedRequest(description: "Could not get a streaming URL from the Premiumize API")
}
return streamUrlString
}
private func createTransfer(magnet: Magnet) async throws {
guard let magnetLink = magnet.link else {
throw DebridError.FailedRequest(description: "The magnet link is invalid")
}
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/create")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents()
bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnetLink)]
request.httpBody = bodyComponents.query?.data(using: .utf8)
try await performRequest(request: &request, requestName: #function)
}
// MARK: - Cloud methods
func getUserDownloads() async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(AllItemsResponse.self, from: data)
if rawResponse.files.isEmpty {
throw DebridError.EmptyData
}
// The "link" is the ID for Premiumize
cloudDownloads = rawResponse.files.map { file in
DebridCloudDownload(id: file.id, fileName: file.name, link: file.id)
}
}
private func itemDetails(itemID: String) async throws -> ItemDetailsResponse {
var urlComponents = URLComponents(string: "\(baseApiUrl)/item/details")!
urlComponents.queryItems = [URLQueryItem(name: "id", value: itemID)]
guard let url = urlComponents.url else {
throw DebridError.InvalidUrl
}
var request = URLRequest(url: url)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(ItemDetailsResponse.self, from: data)
return rawResponse
}
func checkUserDownloads(link: String) async throws -> String? {
// Link is the cloud item ID
try await itemDetails(itemID: link).link
}
func deleteUserDownload(downloadId: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/delete")!)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents()
bodyComponents.queryItems = [URLQueryItem(name: "id", value: downloadId)]
request.httpBody = bodyComponents.query?.data(using: .utf8)
try await performRequest(request: &request, requestName: #function)
}
// No user magnets for Premiumize
func getUserMagnets() {}
func deleteUserMagnet(cloudMagnetId: String?) {}
}