Different services can send different statuses for if a file is cached or not. Therefore, make this scoped to the debrid service rather than expecting everything to state "downloaded". Also it feels pretty blank if the disclosure groups are gone when a cloud array is empty, so remove those checks. Signed-off-by: kingbri <bdashore3@proton.me>
277 lines
9.9 KiB
Swift
277 lines
9.9 KiB
Swift
//
|
|
// OffCloudWrapper.swift
|
|
// Ferrite
|
|
//
|
|
// Created by Brian Dashore on 6/12/24.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
class OffCloud: DebridSource, ObservableObject {
|
|
let id = "OffCloud"
|
|
let abbreviation = "OC"
|
|
let website = "https://offcloud.com"
|
|
let description: String? = "OffCloud is a debrid service that is used for downloads and media playback. " +
|
|
"You must pay to access this service. \n\n" +
|
|
"This service does not inform if a magnet link is a batch before downloading."
|
|
let cachedStatus: [String] = ["downloaded"]
|
|
|
|
@Published var authProcessing: Bool = false
|
|
var isLoggedIn: Bool {
|
|
getToken() != nil
|
|
}
|
|
|
|
var manualToken: String? {
|
|
if UserDefaults.standard.bool(forKey: "OffCloud.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 baseApiUrl = "https://offcloud.com/api"
|
|
private let jsonDecoder = JSONDecoder()
|
|
private let jsonEncoder = JSONEncoder()
|
|
|
|
init() {
|
|
// Populate user downloads and magnets
|
|
Task {
|
|
try? await getUserMagnets()
|
|
}
|
|
}
|
|
|
|
func setApiKey(_ key: String) {
|
|
FerriteKeychain.shared.set(key, forKey: "OffCloud.ApiKey")
|
|
UserDefaults.standard.set(true, forKey: "OffCloud.UseManualKey")
|
|
}
|
|
|
|
func logout() async {
|
|
FerriteKeychain.shared.delete("OffCloud.ApiKey")
|
|
UserDefaults.standard.removeObject(forKey: "OffCloud.UseManualKey")
|
|
}
|
|
|
|
private func getToken() -> String? {
|
|
FerriteKeychain.shared.get("OffCloud.ApiKey")
|
|
}
|
|
|
|
// Wrapper request function which matches the responses and returns data
|
|
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
|
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 TorBox in Settings.")
|
|
} else {
|
|
print(response)
|
|
throw DebridError.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 DebridError.InvalidUrl
|
|
}
|
|
|
|
guard let token = getToken() else {
|
|
throw DebridError.InvalidToken
|
|
}
|
|
|
|
components.queryItems = [
|
|
URLQueryItem(name: "key", value: token)
|
|
] + queryItems
|
|
|
|
if let url = components.url {
|
|
return url
|
|
} else {
|
|
throw DebridError.InvalidUrl
|
|
}
|
|
}
|
|
|
|
func instantAvailability(magnets: [Magnet]) async throws {
|
|
let now = Date().timeIntervalSince1970
|
|
|
|
let sendMagnets = magnets.filter { magnet in
|
|
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
|
|
}
|
|
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cache"))
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
let body = InstantAvailabilityRequest(hashes: sendMagnets.compactMap(\.hash))
|
|
request.httpBody = try jsonEncoder.encode(body)
|
|
|
|
let data = try await performRequest(request: &request, requestName: #function)
|
|
let rawResponse = try jsonDecoder.decode(InstantAvailabilityResponse.self, from: data)
|
|
|
|
let availableHashes = rawResponse.cachedItems.map {
|
|
DebridIA(
|
|
magnet: Magnet(hash: $0, link: nil),
|
|
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
|
files: []
|
|
)
|
|
}
|
|
|
|
IAValues += availableHashes
|
|
}
|
|
|
|
// Cloud in OffCloud's API
|
|
func getRestrictedFile(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> (restrictedFile: DebridIAFile?, newIA: DebridIA?) {
|
|
let selectedCloudMagnet: DebridCloudMagnet
|
|
|
|
// Don't queue a new job if the magnet already exists in the user's account
|
|
if let existingCloudMagnet = cloudMagnets.first(where: { $0.hash == magnet.hash && cachedStatus.contains($0.status) }) {
|
|
selectedCloudMagnet = existingCloudMagnet
|
|
} else {
|
|
let cloudDownloadResponse = try await offcloudDownload(magnet: magnet)
|
|
|
|
guard cachedStatus.contains(cloudDownloadResponse.status) else {
|
|
throw DebridError.IsCaching
|
|
}
|
|
|
|
selectedCloudMagnet = DebridCloudMagnet(
|
|
id: cloudDownloadResponse.requestId,
|
|
fileName: cloudDownloadResponse.fileName,
|
|
status: cloudDownloadResponse.status,
|
|
hash: "",
|
|
links: [cloudDownloadResponse.url]
|
|
)
|
|
}
|
|
|
|
let cloudExploreResponse = try await cloudExplore(requestId: selectedCloudMagnet.id)
|
|
|
|
// Request will error if the file isn't a batch
|
|
if case let .links(cloudExploreLinks) = cloudExploreResponse {
|
|
var copiedIA = ia
|
|
|
|
copiedIA?.files = cloudExploreLinks.enumerated().compactMap { index, exploreLink in
|
|
guard let exploreURL = URL(string: exploreLink) else {
|
|
return nil
|
|
}
|
|
|
|
return DebridIAFile(
|
|
id: index,
|
|
name: exploreURL.lastPathComponent,
|
|
streamUrlString: exploreLink.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
|
)
|
|
}
|
|
|
|
return (nil, copiedIA)
|
|
} else if case let .error(cloudExploreError) = cloudExploreResponse,
|
|
cloudExploreError.error.lowercased() == "bad archive"
|
|
{
|
|
guard let selectedCloudLink = selectedCloudMagnet.links[safe: 0] else {
|
|
throw DebridError.EmptyUserMagnets
|
|
}
|
|
|
|
let restrictedFile = DebridIAFile(
|
|
id: 0,
|
|
name: selectedCloudMagnet.fileName,
|
|
streamUrlString: "\(selectedCloudLink)/\(selectedCloudMagnet.fileName)"
|
|
)
|
|
|
|
return (restrictedFile, nil)
|
|
} else {
|
|
return (nil, nil)
|
|
}
|
|
}
|
|
|
|
// Called as "cloud" in offcloud's API
|
|
private func offcloudDownload(magnet: Magnet) async throws -> CloudDownloadResponse {
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cloud"))
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
|
|
guard let magnetLink = magnet.link else {
|
|
throw DebridError.EmptyData
|
|
}
|
|
|
|
let body = CloudDownloadRequest(url: magnetLink)
|
|
request.httpBody = try jsonEncoder.encode(body)
|
|
|
|
let data = try await performRequest(request: &request, requestName: "cloud")
|
|
let rawResponse = try jsonDecoder.decode(CloudDownloadResponse.self, from: data)
|
|
|
|
return rawResponse
|
|
}
|
|
|
|
private func cloudExplore(requestId: String) async throws -> CloudExploreResponse {
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cloud/explore/\(requestId)"))
|
|
|
|
let data = try await performRequest(request: &request, requestName: "cloudExplore")
|
|
let rawResponse = try jsonDecoder.decode(CloudExploreResponse.self, from: data)
|
|
|
|
return rawResponse
|
|
}
|
|
|
|
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 OffCloud API")
|
|
}
|
|
|
|
return streamUrlString
|
|
}
|
|
|
|
func getUserDownloads() {}
|
|
|
|
func checkUserDownloads(link: String) -> String? {
|
|
link
|
|
}
|
|
|
|
func deleteUserDownload(downloadId: String) {}
|
|
|
|
func getUserMagnets() async throws {
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cloud/history"))
|
|
|
|
let data = try await performRequest(request: &request, requestName: "cloudHistory")
|
|
let rawResponse = try jsonDecoder.decode([CloudHistoryResponse].self, from: data)
|
|
|
|
cloudMagnets = rawResponse.compactMap { cloudHistory in
|
|
guard let magnetHash = Magnet(hash: nil, link: cloudHistory.originalLink).hash else {
|
|
return nil
|
|
}
|
|
|
|
return DebridCloudMagnet(
|
|
id: cloudHistory.requestId,
|
|
fileName: cloudHistory.fileName,
|
|
status: cloudHistory.status,
|
|
hash: magnetHash,
|
|
links: [cloudHistory.originalLink]
|
|
)
|
|
}
|
|
}
|
|
|
|
// Uses the base website because this isn't present in the API path but still works like the API?
|
|
func deleteUserMagnet(cloudMagnetId: String?) async throws {
|
|
guard let cloudMagnetId else {
|
|
throw DebridError.InvalidPostBody
|
|
}
|
|
|
|
var request = URLRequest(url: try buildRequestURL(urlString: "\(website)/cloud/remove/\(cloudMagnetId)"))
|
|
try await performRequest(request: &request, requestName: "cloudRemove")
|
|
}
|
|
}
|