Debrid: Add InstantAvailability and download to protocol

Unify IA into a passable client side structure and add a common
download method to the DebridSource protocol.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2024-06-02 23:08:05 -04:00
parent 0fe1cbc888
commit 37450ef979
11 changed files with 160 additions and 142 deletions

View file

@ -394,6 +394,7 @@
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */,
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */,
0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */,
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
0C422E7F293542F300486D65 /* PremiumizeModels.swift */,
@ -403,7 +404,6 @@
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */,
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */,
0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */,
);
path = Models;
sourceTree = "<group>";

View file

@ -9,7 +9,6 @@ import Foundation
// TODO: Fix errors
public class AllDebrid: PollingDebridSource {
public let id = "AllDebrid"
public var authTask: Task<Void, Error>?
@ -96,7 +95,7 @@ public class AllDebrid: PollingDebridSource {
}
public func getToken() -> String? {
return FerriteKeychain.shared.get("AllDebrid.ApiKey")
FerriteKeychain.shared.get("AllDebrid.ApiKey")
}
// Clears tokens. No endpoint to deregister a device
@ -146,6 +145,20 @@ public class AllDebrid: PollingDebridSource {
}
}
// Wrapper function to fetch a download link from the API
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
let magnetID = try await addMagnet(magnet: magnet)
let lockedLink = try await fetchMagnetStatus(
magnetId: magnetID,
selectedIndex: iaFile?.fileId ?? 0
)
try await saveLink(link: lockedLink)
let downloadUrl = try await unlockLink(lockedLink: lockedLink)
return downloadUrl
}
// Adds a magnet link to the user's AD account
public func addMagnet(magnet: Magnet) async throws -> Int {
guard let magnetLink = magnet.link else {
@ -255,7 +268,7 @@ public class AllDebrid: PollingDebridSource {
try await performRequest(request: &request, requestName: #function)
}
public func instantAvailability(magnets: [Magnet]) async throws -> [IA] {
public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] {
let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems))
@ -266,10 +279,10 @@ public class AllDebrid: PollingDebridSource {
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)
DebridIAFile(fileId: index, name: magnetFile.name)
}
return IA(
return DebridIA(
magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet),
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files

View file

@ -8,7 +8,6 @@
import Foundation
public class Premiumize: OAuthDebridSource {
public let id = "Premiumize"
let baseAuthUrl = "https://www.premiumize.me/authorize"
@ -58,7 +57,7 @@ public class Premiumize: OAuthDebridSource {
}
public func getToken() -> String? {
return FerriteKeychain.shared.get("Premiumize.AccessToken")
FerriteKeychain.shared.get("Premiumize.AccessToken")
}
// Clears tokens. No endpoint to deregister a device
@ -162,15 +161,15 @@ public class Premiumize: OAuthDebridSource {
// Function to divide and execute DDL endpoint requests in parallel
// Calls this for 10 requests at a time to not overwhelm API servers
public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [IA] {
let tempIA = try await withThrowingTaskGroup(of: Premiumize.IA.self) { group in
public 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: [Premiumize.IA] = []
var chunkedIA: [DebridIA] = []
for try await ia in group {
chunkedIA.append(ia)
}
@ -181,7 +180,7 @@ public class Premiumize: OAuthDebridSource {
}
// Grabs DDL links
func fetchDDL(magnet: Magnet) async throws -> IA {
func fetchDDL(magnet: Magnet) async throws -> DebridIA {
if magnet.hash == nil {
throw PMError.EmptyData
}
@ -200,13 +199,14 @@ public class Premiumize: OAuthDebridSource {
if !rawResponse.content.isEmpty {
let files = rawResponse.content.map { file in
IAFile(
DebridIAFile(
fileId: 0,
name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path,
streamUrlString: file.link
)
}
return IA(
return DebridIA(
magnet: magnet,
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files
@ -216,6 +216,20 @@ public class Premiumize: OAuthDebridSource {
}
}
// Wrapper function to fetch a DDL link from the API
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
// Store the item in PM cloud for later use
try await createTransfer(magnet: magnet)
if let iaFile, let streamUrlString = iaFile.streamUrlString {
return streamUrlString
} else if let premiumizeItem = ia, let firstFile = premiumizeItem.files[safe: 0], let streamUrlString = firstFile.streamUrlString {
return streamUrlString
} else {
throw PMError.FailedRequest(description: "Could not fetch your file from the Premiumize API")
}
}
func createTransfer(magnet: Magnet) async throws {
guard let magnetLink = magnet.link else {
throw PMError.FailedRequest(description: "The magnet link is invalid")

View file

@ -8,7 +8,6 @@
import Foundation
public class RealDebrid: PollingDebridSource {
public let id = "RealDebrid"
public var authTask: Task<Void, Error>?
@ -87,7 +86,7 @@ public class RealDebrid: PollingDebridSource {
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)
let rawResponse = try? 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 {
@ -169,7 +168,7 @@ public class RealDebrid: PollingDebridSource {
return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key
}
// Deletes tokens from device and RD's servers
public func logout() async {
FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
@ -213,9 +212,8 @@ public class RealDebrid: PollingDebridSource {
}
// Checks if the magnet is streamable on RD
// Currently does not work for batch links
public func instantAvailability(magnets: [Magnet]) async throws -> [IA] {
var availableHashes: [RealDebrid.IA] = []
public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] {
var availableHashes: [DebridIA] = []
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.compactMap(\.hash).joined(separator: "/"))")!)
let data = try await performRequest(request: &request, requestName: #function)
@ -232,7 +230,7 @@ public class RealDebrid: PollingDebridSource {
continue
}
// Is this a batch
// Is this a batch?
if data.rd.count > 1 || data.rd[0].count > 1 {
// Batch array
let batches = data.rd.map { fileDict in
@ -244,22 +242,18 @@ public class RealDebrid: PollingDebridSource {
return RealDebrid.IABatch(files: batchFiles)
}
// RD files array
// Possibly sort this in the future, but not sure how at the moment
var files: [RealDebrid.IAFile] = []
var files: [DebridIAFile] = []
for index in batches.indices {
let batchFiles = batches[index].files
for batch in batches {
let batchFileIds = batch.files.map(\.id)
for batchFileIndex in batchFiles.indices {
let batchFile = batchFiles[batchFileIndex]
if !files.contains(where: { $0.name == batchFile.fileName }) {
for batchFile in batch.files {
if !files.contains(where: { $0.fileId == batchFile.id }) {
files.append(
RealDebrid.IAFile(
DebridIAFile(
fileId: batchFile.id,
name: batchFile.fileName,
batchIndex: index,
batchFileIndex: batchFileIndex
batchIds: batchFileIds
)
)
}
@ -268,18 +262,18 @@ public class RealDebrid: PollingDebridSource {
// TTL: 5 minutes
availableHashes.append(
RealDebrid.IA(
DebridIA(
magnet: Magnet(hash: hash, link: nil),
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files,
batches: batches
files: files
)
)
} else {
availableHashes.append(
RealDebrid.IA(
DebridIA(
magnet: Magnet(hash: hash, link: nil),
expiryTimeStamp: Date().timeIntervalSince1970 + 300
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: []
)
)
}
@ -288,6 +282,21 @@ public class RealDebrid: PollingDebridSource {
return availableHashes
}
// Wrapper function to fetch a download link from the API
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
let selectedMagnetId = try await addMagnet(magnet: magnet)
try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? [])
let torrentLink = try await torrentInfo(
debridID: selectedMagnetId,
selectedIndex: iaFile?.fileId ?? 0
)
let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink)
return downloadLink
}
// Adds a magnet link to the user's RD account
public func addMagnet(magnet: Magnet) async throws -> String {
guard let magnetLink = magnet.link else {
@ -335,9 +344,11 @@ public class RealDebrid: PollingDebridSource {
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
let filteredFiles = rawResponse.files.filter { $0.selected == 1 }
let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedIndex })
// Let the user know if a torrent is downloading
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" {
if let torrentLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" {
return torrentLink
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
throw RDError.EmptyTorrents

View file

@ -7,10 +7,24 @@
import Foundation
public struct DebridIAFile {
public struct DebridIA: Sendable, Hashable {
let magnet: Magnet
let expiryTimeStamp: Double
var files: [DebridIAFile]
}
public struct DebridCloudFile {
public struct DebridIAFile: Hashable, Sendable {
let fileId: Int
let name: String
let streamUrlString: String?
let batchIds: [Int]
init(fileId: Int, name: String, streamUrlString: String? = nil, batchIds: [Int] = []) {
self.fileId = fileId
self.name = name
self.streamUrlString = streamUrlString
self.batchIds = batchIds
}
}
public struct DebridCloudFile {}

View file

@ -92,13 +92,6 @@ public extension RealDebrid {
// MARK: - Instant Availability client side structures
struct IA: Codable, Hashable, Sendable {
let magnet: Magnet
let expiryTimeStamp: Double
var files: [IAFile] = []
var batches: [IABatch] = []
}
struct IABatch: Codable, Hashable, Sendable {
let files: [IABatchFile]
}
@ -108,12 +101,6 @@ public extension RealDebrid {
let fileName: String
}
struct IAFile: Codable, Hashable, Sendable {
let name: String
let batchIndex: Int
let batchFileIndex: Int
}
// MARK: - addMagnet endpoint
struct AddMagnetResponse: Codable, Sendable {

View file

@ -14,6 +14,10 @@ public protocol DebridSource {
// Common authentication functions
func setApiKey(_ key: String) -> Bool
func logout() async
// Fetches a download link from a source
// Include the instant availability information with the args
func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String
}
public protocol PollingDebridSource: DebridSource {
@ -25,7 +29,6 @@ public protocol PollingDebridSource: DebridSource {
}
public protocol OAuthDebridSource: DebridSource {
// Fetches the auth URL
func getAuthUrl() throws -> URL

View file

@ -59,12 +59,12 @@ public class DebridManager: ObservableObject {
var realDebridAuthProcessing: Bool = false
// RealDebrid fetch variables
@Published var realDebridIAValues: [RealDebrid.IA] = []
@Published var realDebridIAValues: [DebridIA] = []
@Published var showDeleteAlert: Bool = false
var selectedRealDebridItem: RealDebrid.IA?
var selectedRealDebridFile: RealDebrid.IAFile?
var selectedRealDebridItem: DebridIA?
var selectedRealDebridFile: DebridIAFile?
var selectedRealDebridID: String?
// TODO: Maybe make these generic?
@ -77,10 +77,10 @@ public class DebridManager: ObservableObject {
var allDebridAuthProcessing: Bool = false
// AllDebrid fetch variables
@Published var allDebridIAValues: [AllDebrid.IA] = []
@Published var allDebridIAValues: [DebridIA] = []
var selectedAllDebridItem: AllDebrid.IA?
var selectedAllDebridFile: AllDebrid.IAFile?
var selectedAllDebridItem: DebridIA?
var selectedAllDebridFile: DebridIAFile?
// AllDebrid cloud variables
@Published var allDebridCloudMagnets: [AllDebrid.MagnetStatusData] = []
@ -91,10 +91,10 @@ public class DebridManager: ObservableObject {
var premiumizeAuthProcessing: Bool = false
// Premiumize fetch variables
@Published var premiumizeIAValues: [Premiumize.IA] = []
@Published var premiumizeIAValues: [DebridIA] = []
var selectedPremiumizeItem: Premiumize.IA?
var selectedPremiumizeFile: Premiumize.IAFile?
var selectedPremiumizeItem: DebridIA?
var selectedPremiumizeFile: DebridIAFile?
// Premiumize cloud variables
@Published var premiumizeCloudItems: [Premiumize.UserItem] = []
@ -282,10 +282,10 @@ public class DebridManager: ObservableObject {
return .none
}
if realDebridMatch.batches.isEmpty {
return .full
} else {
if realDebridMatch.files.count > 1 {
return .partial
} else {
return .full
}
case .allDebrid:
guard let allDebridMatch = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
@ -578,7 +578,7 @@ public class DebridManager: ObservableObject {
case .allDebrid:
await fetchAdDownload(magnet: magnet, existingLockedLink: cloudInfo)
case .premiumize:
await fetchPmDownload(cloudItemId: cloudInfo)
await fetchPmDownload(magnet: magnet, cloudItemId: cloudInfo)
case .none:
break
}
@ -586,6 +586,7 @@ public class DebridManager: ObservableObject {
func fetchRdDownload(magnet: Magnet?, existingLink: String?) async {
// If an existing link is passed in args, set it to that. Otherwise, find one from RD cloud.
/*
let torrentLink: String?
if let existingLink {
torrentLink = existingLink
@ -596,42 +597,23 @@ public class DebridManager: ObservableObject {
let existingTorrent = realDebridCloudTorrents.first { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" }
torrentLink = existingTorrent?.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0]
}
*/
do {
// If the links match from a user's downloads, no need to re-run a download
/*
if let torrentLink,
let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink)
{
downloadUrl = downloadLink
} else if let magnet {
// Add a magnet after all the cache checks fail
selectedRealDebridID = try await realDebrid.addMagnet(magnet: magnet)
} else */
if let magnet {
let downloadLink = try await realDebrid.getDownloadLink(
magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile
)
var fileIds: [Int] = []
if let iaFile = selectedRealDebridFile {
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
return
}
fileIds = iaBatchFromFile.files.map(\.id)
}
if let realDebridId = selectedRealDebridID {
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
let torrentLink = try await realDebrid.torrentInfo(
debridID: realDebridId,
selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0
)
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
downloadUrl = downloadLink
} else {
logManager?.error(
"RealDebrid: Could not cache torrent with hash \(String(describing: magnet.hash))",
description: "Could not cache this torrent. Aborting."
)
}
// Update the UI
downloadUrl = downloadLink
} else {
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
}
@ -645,7 +627,7 @@ public class DebridManager: ObservableObject {
default:
await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled")
await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false)
// await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false)
}
logManager?.hideIndeterminateToast()
@ -695,8 +677,6 @@ public class DebridManager: ObservableObject {
do {
if let torrentID {
try await realDebrid.deleteTorrent(debridID: torrentID)
} else if let selectedTorrentID = selectedRealDebridID {
try await realDebrid.deleteTorrent(debridID: selectedTorrentID)
} else {
throw RealDebrid.RDError.FailedRequest(description: "No torrent ID was provided")
}
@ -720,34 +700,36 @@ public class DebridManager: ObservableObject {
}
}
// TODO: Integrate with AD saved links
func fetchAdDownload(magnet: Magnet?, existingLockedLink: String?) async {
// If an existing link is passed in args, set it to that. Otherwise, find one from AD cloud.
let lockedLink: String?
if let existingLockedLink {
lockedLink = existingLockedLink
} else {
// Bypass the TTL for up to date information
await fetchAdCloud(bypassTTL: true)
/*
let lockedLink: String?
if let existingLockedLink {
lockedLink = existingLockedLink
} else {
// Bypass the TTL for up to date information
await fetchAdCloud(bypassTTL: true)
let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" }
lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.id ?? 0]?.link
}
let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" }
lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.fileId ?? 0]?.link
}
*/
do {
if let lockedLink,
let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink)
{
downloadUrl = unlockedLink
} else if let magnet {
let magnetID = try await allDebrid.addMagnet(magnet: magnet)
let lockedLink = try await allDebrid.fetchMagnetStatus(
magnetId: magnetID,
selectedIndex: selectedAllDebridFile?.id ?? 0
/*
if let lockedLink,
let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink)
{
downloadUrl = unlockedLink
} else if let magnet {
*/
if let magnet {
let downloadLink = try await allDebrid.getDownloadLink(
magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile
)
try await allDebrid.saveLink(link: lockedLink)
downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink)
// Update UI
downloadUrl = downloadLink
} else {
throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API")
}
@ -810,28 +792,22 @@ public class DebridManager: ObservableObject {
}
}
func fetchPmDownload(cloudItemId: String? = nil) async {
func fetchPmDownload(magnet: Magnet?, cloudItemId: String? = nil) async {
do {
if let cloudItemId {
downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link
} else if let premiumizeFile = selectedPremiumizeFile {
downloadUrl = premiumizeFile.streamUrlString
} else if
let premiumizeItem = selectedPremiumizeItem,
let firstFile = premiumizeItem.files[safe: 0]
{
downloadUrl = firstFile.streamUrlString
} else if let magnet {
let downloadLink = try await premiumize.getDownloadLink(
magnet: magnet, ia: selectedPremiumizeItem, iaFile: selectedPremiumizeFile
)
downloadUrl = downloadLink
} else {
throw Premiumize.PMError.FailedRequest(description: "There were no items or files found!")
throw Premiumize.PMError.FailedRequest(description: "Could not fetch your file from Premiumize's cache or API")
}
// Fetch one more time to add updated data into the PM cloud cache
await fetchPmCloud(bypassTTL: true)
// Add a PM transfer if the item exists
if let premiumizeItem = selectedPremiumizeItem {
try await premiumize.createTransfer(magnet: premiumizeItem.magnet)
}
} catch {
await sendDebridError(error, prefix: "Premiumize download error", cancelString: "Download or transfer cancelled")
}

View file

@ -21,7 +21,7 @@ struct HybridSecureField: View {
private var isFieldDisabled: Bool = false
init(text: Binding<String>, onCommit: (() -> Void)? = nil, showPassword: Bool = false) {
self._text = text
_text = text
if let onCommit {
self.onCommit = onCommit
}
@ -57,6 +57,6 @@ struct HybridSecureField: View {
extension HybridSecureField {
public func fieldDisabled(_ isFieldDisabled: Bool) -> Self {
modifyViewProp({ $0.isFieldDisabled = isFieldDisabled })
modifyViewProp { $0.isFieldDisabled = isFieldDisabled }
}
}

View file

@ -96,7 +96,7 @@ struct SettingsView: View {
if changed {
Task {
let dataRecords = await WKWebsiteDataStore.default().dataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes())
await WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: dataRecords)
}
}

View file

@ -36,11 +36,11 @@ struct BatchChoiceView: View {
}
case .allDebrid:
ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in
if file.fileName.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.fileName) {
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedAllDebridFile = file
queueCommonDownload(fileName: file.fileName)
queueCommonDownload(fileName: file.name)
}
}
}