Debrid: Refactor IA and download functions

Use the common protocol to handle these.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2024-06-05 22:16:40 -04:00 committed by Brian Dashore
parent 7d5cdc5d06
commit aa739133be
6 changed files with 126 additions and 189 deletions

View file

@ -19,9 +19,10 @@ public class AllDebrid: PollingDebridSource, ObservableObject {
getToken() != nil
}
public var IAValues: [DebridIA] = []
public var cloudDownloads: [DebridCloudDownload] = []
public var cloudTorrents: [DebridCloudTorrent] = []
@Published public var IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = []
public var cloudTTL: Double = 0.0
let baseApiUrl = "https://api.alldebrid.com/v4"
let appName = "Ferrite"
@ -163,7 +164,26 @@ public class AllDebrid: PollingDebridSource, ObservableObject {
// MARK: - Instant availability
public func instantAvailability(magnets: [Magnet]) async throws {
let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
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
}
let queryItems = sendMagnets.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)

View file

@ -19,6 +19,7 @@ public class Premiumize: OAuthDebridSource, ObservableObject {
@Published public var IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = []
public var cloudTTL: Double = 0.0
let baseAuthUrl = "https://www.premiumize.me/authorize"
let baseApiUrl = "https://www.premiumize.me/api"
@ -127,16 +128,31 @@ public class Premiumize: OAuthDebridSource, ObservableObject {
// MARK: - Instant availability
public func instantAvailability(magnets: [Magnet]) async throws {
// Only strip magnets that don't have an associated link for PM
let strippedMagnets: [Magnet] = magnets.compactMap {
if let magnetLink = $0.link {
return Magnet(hash: $0.hash, link: magnetLink)
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 nil
return true
}
}
let availableMagnets = try await divideCacheRequests(magnets: strippedMagnets)
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) {

View file

@ -20,14 +20,10 @@ public class RealDebrid: PollingDebridSource, ObservableObject {
FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil
}
@Published public var IAValues: [DebridIA] = [] {
willSet {
self.objectWillChange.send()
}
}
@Published public var IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = []
var cloudTTL: Double = 0.0
public var cloudTTL: Double = 0.0
let baseAuthUrl = "https://api.real-debrid.com/oauth/v2"
let baseApiUrl = "https://api.real-debrid.com/rest/1.0"
@ -237,7 +233,26 @@ public class RealDebrid: PollingDebridSource, ObservableObject {
// Checks if the magnet is streamable on RD
public func instantAvailability(magnets: [Magnet]) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.compactMap(\.hash).joined(separator: "/"))")!)
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: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(sendMagnets.compactMap(\.hash).joined(separator: "/"))")!)
let data = try await performRequest(request: &request, requestName: #function)

View file

@ -36,6 +36,7 @@ public protocol DebridSource: AnyObservableObject {
// Cloud variables
var cloudDownloads: [DebridCloudDownload] { get set }
var cloudTorrents: [DebridCloudTorrent] { get set }
var cloudTTL: Double { get set }
// User downloads functions
func getUserDownloads() async throws -> [DebridCloudDownload]

View file

@ -27,12 +27,8 @@ public class DebridManager: ObservableObject {
}
@Published var selectedDebridSource: DebridSource?
/*
func debridSourceFromName(_ name: String? = nil) -> DebridSource? {
debridSources.first { $0.id.name == name ?? selectedDebridId?.name }
}
*/
var selectedDebridItem: DebridIA?
var selectedDebridFile: DebridIAFile?
// Service agnostic variables
@Published var enabledDebrids: Set<DebridType> = [] {
@ -184,78 +180,22 @@ public class DebridManager: ObservableObject {
// Clears all selected files and items
public func clearSelectedDebridItems() {
switch selectedDebridType {
case .realDebrid:
selectedRealDebridFile = nil
selectedRealDebridItem = nil
case .allDebrid:
selectedAllDebridFile = nil
selectedAllDebridItem = nil
case .premiumize:
selectedPremiumizeFile = nil
selectedPremiumizeItem = nil
case .none:
break
}
selectedDebridItem = nil
selectedDebridFile = nil
}
// Common function to populate hashes for debrid services
public func populateDebridIA(_ resultMagnets: [Magnet]) async {
let now = Date()
// If a hash isn't found in the IA, update it
// If the hash is expired, remove it and update it
let sendMagnets = resultMagnets.filter { magnet in
if let IAIndex = realDebrid.IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.realDebrid) {
if now.timeIntervalSince1970 > realDebrid.IAValues[IAIndex].expiryTimeStamp {
realDebrid.IAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else if let IAIndex = allDebrid.IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.allDebrid) {
if now.timeIntervalSince1970 > allDebrid.IAValues[IAIndex].expiryTimeStamp {
allDebrid.IAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else if let IAIndex = premiumize.IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.premiumize) {
if now.timeIntervalSince1970 > premiumize.IAValues[IAIndex].expiryTimeStamp {
premiumize.IAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else {
return true
}
}
// Don't exit the function if the API fetch errors
if !sendMagnets.isEmpty {
if enabledDebrids.contains(.realDebrid) {
do {
try await realDebrid.instantAvailability(magnets: sendMagnets)
} catch {
await sendDebridError(error, prefix: "RealDebrid IA fetch error")
}
for debridSource in debridSources {
if !debridSource.isLoggedIn {
continue
}
if enabledDebrids.contains(.allDebrid) {
do {
try await allDebrid.instantAvailability(magnets: sendMagnets)
} catch {
await sendDebridError(error, prefix: "AllDebrid IA fetch error")
}
}
if enabledDebrids.contains(.premiumize) {
do {
try await premiumize.instantAvailability(magnets: sendMagnets)
} catch {
await sendDebridError(error, prefix: "Premiumize IA fetch error")
}
// Don't exit the function if the API fetch errors
do {
try await debridSource.instantAvailability(magnets: resultMagnets)
} catch {
await sendDebridError(error, prefix: "\(debridSource.id) IA fetch error")
}
}
}
@ -281,32 +221,15 @@ public class DebridManager: ObservableObject {
return false
}
switch selectedDebridSource?.id {
case .some("RealDebrid"):
if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedRealDebridItem = realDebridItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
return false
}
case .some("AllDebrid"):
if let allDebridItem = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedAllDebridItem = allDebridItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)")
return false
}
case .some("Premiumize"):
if let premiumizeItem = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedPremiumizeItem = premiumizeItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)")
return false
}
default:
guard let selectedSource = selectedDebridSource else {
return false
}
if let IAItem = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedDebridItem = IAItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated \(selectedSource.id) entry for magnet hash \(magnetHash)")
return false
}
}
@ -535,23 +458,19 @@ public class DebridManager: ObservableObject {
self.currentDebridTask = nil
})
switch selectedDebridType {
case .realDebrid:
await fetchRdDownload(magnet: magnet, cloudInfo: cloudInfo)
case .allDebrid:
await fetchAdDownload(magnet: magnet, cloudInfo: cloudInfo)
case .premiumize:
await fetchPmDownload(magnet: magnet, cloudInfo: cloudInfo)
case .none:
break
guard let debridSource = selectedDebridSource else {
return
}
}
func fetchRdDownload(magnet: Magnet?, cloudInfo: String?) async {
do {
if let cloudInfo {
downloadUrl = try await debridSource.checkUserDownloads(link: cloudInfo) ?? ""
return
}
if let magnet {
let downloadLink = try await realDebrid.getDownloadLink(
magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile
let downloadLink = try await debridSource.getDownloadLink(
magnet: magnet, ia: selectedDebridItem, iaFile: selectedDebridFile
)
// Update the UI
@ -560,6 +479,28 @@ public class DebridManager: ObservableObject {
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
}
// Fetch one more time to add updated data into the RD cloud cache
// TODO: Add common fetch cloud method
//await fetchRdCloud(bypassTTL: true)
} catch {
// TODO: Fix error types and unify errors
print("Error \(error)")
}
}
func fetchRdDownload(magnet: Magnet?, cloudInfo: String?) async {
do {
guard let magnet else {
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
}
let downloadLink = try await realDebrid.getDownloadLink(
magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile
)
// Update the UI
downloadUrl = downloadLink
// Fetch one more time to add updated data into the RD cloud cache
await fetchRdCloud(bypassTTL: true)
} catch {
@ -631,21 +572,6 @@ public class DebridManager: ObservableObject {
}
}
func checkRdUserDownloads(userTorrentLink: String) async -> String? {
do {
let existingLinks = realDebridCloudDownloads.first { $0.link == userTorrentLink }
if let existingLink = existingLinks?.fileName {
return existingLink
} else {
return try await realDebrid.unrestrictLink(debridDownloadLink: userTorrentLink)
}
} catch {
await sendDebridError(error, prefix: "RealDebrid download check error")
return nil
}
}
func fetchAdDownload(magnet: Magnet?, cloudInfo: String?) async {
do {
if let magnet {
@ -666,22 +592,6 @@ public class DebridManager: ObservableObject {
}
}
func checkAdUserLinks(lockedLink: String) async -> String? {
do {
let existingLinks = allDebridCloudLinks.first { $0.link == lockedLink }
if let existingLink = existingLinks?.link {
return existingLink
} else {
try await allDebrid.saveLink(link: lockedLink)
return try await allDebrid.unlockLink(lockedLink: lockedLink)
}
} catch {
await sendDebridError(error, prefix: "AllDebrid download check error")
return nil
}
}
// Refreshes torrents and downloads from a RD user's account
public func fetchAdCloud(bypassTTL: Bool = false) async {
if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL {

View file

@ -23,39 +23,14 @@ struct BatchChoiceView: View {
var body: some View {
NavView {
List {
switch debridManager.selectedDebridSource?.id {
case .some("RealDebrid"):
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedRealDebridFile = file
ForEach(debridManager.selectedDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedDebridFile = file
queueCommonDownload(fileName: file.name)
}
queueCommonDownload(fileName: file.name)
}
}
case .some("AllDebrid"):
ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedAllDebridFile = file
queueCommonDownload(fileName: file.name)
}
}
}
case .some("Premiumize"):
ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedPremiumizeFile = file
queueCommonDownload(fileName: file.name)
}
}
}
default:
EmptyView()
}
}
.tint(.primary)