Debrid: Add support for AllDebrid cloud and cleanup

This commit adds support for viewing a user's AllDebrid magnet list.
AllDebrid does not save unlocked links, but they do save which magnets
a user has queried.

Also clean up various functions in DebridManager.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-01-06 16:03:31 -05:00
parent 90ed4f8353
commit 2258036f7b
12 changed files with 243 additions and 64 deletions

View file

@ -38,6 +38,7 @@
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; };
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; };
@ -148,6 +149,7 @@
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = "<group>"; };
@ -317,6 +319,7 @@
children = (
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */,
0CAF9318296399190050812A /* PremiumizeCloudView.swift */,
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */,
);
path = Cloud;
sourceTree = "<group>";
@ -706,6 +709,7 @@
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */,
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,

View file

@ -21,7 +21,6 @@ public class AllDebrid {
// 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 {
@ -161,19 +160,40 @@ public class AllDebrid {
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] {
if let linkWrapper = rawResponse.magnets[safe: 0]?.links[safe: selectedIndex ?? -1] {
return linkWrapper.link
} else {
throw ADError.EmptyTorrents
}
}
public func userMagnets() async throws -> [MagnetStatusData] {
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status"))
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
if rawResponse.magnets.isEmpty {
throw ADError.EmptyData
} else {
return rawResponse.magnets
}
}
public func deleteMagnet(magnetId: Int) async throws {
let queryItems = [
URLQueryItem(name: "id", value: String(magnetId))
]
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems))
try await performRequest(request: &request, requestName: #function)
}
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

View file

@ -83,12 +83,24 @@ public extension AllDebrid {
// MARK: - MagnetStatusResponse
struct MagnetStatusResponse: Codable {
let magnets: MagnetStatusData
let magnets: [MagnetStatusData]
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let data = try? container.decode(MagnetStatusData.self, forKey: .magnets) {
self.magnets = [data]
} else if let data = try? container.decode([MagnetStatusData].self, forKey: .magnets) {
self.magnets = data
} else {
self.magnets = []
}
}
}
// MARK: - MagnetStatusData
internal struct MagnetStatusData: Codable {
struct MagnetStatusData: Codable {
let id: Int
let filename: String
let size: Int

View file

@ -64,6 +64,10 @@ public class DebridManager: ObservableObject {
var selectedAllDebridItem: AllDebrid.IA?
var selectedAllDebridFile: AllDebrid.IAFile?
// AllDebrid cloud variables
@Published var allDebridCloudMagnets: [AllDebrid.MagnetStatusData] = []
var allDebridCloudTTL: Double = 0.0
// Premiumize auth variables
@Published var premiumizeAuthProcessing: Bool = false
@ -471,7 +475,8 @@ public class DebridManager: ObservableObject {
// MARK: - Debrid fetch UI linked functions
// Common function to delegate what debrid service to fetch from
public func fetchDebridDownload(magnet: Magnet?) async {
// Cloudinfo is used for any extra information provided by debrid cloud
public func fetchDebridDownload(magnet: Magnet?, cloudInfo: String? = nil) async {
defer {
currentDebridTask = nil
showLoadingProgress = false
@ -481,29 +486,35 @@ public class DebridManager: ObservableObject {
switch selectedDebridType {
case .realDebrid:
await fetchRdDownload(magnet: magnet)
await fetchRdDownload(magnet: magnet, existingLink: cloudInfo)
case .allDebrid:
await fetchAdDownload(magnet: magnet)
await fetchAdDownload(magnet: magnet, existingLockedLink: cloudInfo)
case .premiumize:
await fetchPmDownload()
await fetchPmDownload(cloudItemId: cloudInfo)
case .none:
break
}
}
func fetchRdDownload(magnet: Magnet?) async {
do {
// Bypass the TTL since a download needs to be queried
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
} else {
// Bypass the TTL for up to date information
await fetchRdCloud(bypassTTL: true)
// If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link
let existingTorrents = realDebridCloudTorrents.filter { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" }
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 existingTorrent = existingTorrents[safe: 0],
let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0]
if let torrentLink,
let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink)
{
try 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)
@ -531,8 +542,7 @@ public class DebridManager: ObservableObject {
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.")
}
} else {
toastModel?.updateToastDescription("Could not fetch your file from RealDebrid's cache or API")
print("RealDebrid error: No magnet link or cached file found")
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
}
} catch {
switch error {
@ -588,42 +598,81 @@ public class DebridManager: ObservableObject {
}
}
func checkRdUserDownloads(userTorrentLink: String) async throws {
let existingLinks = realDebridCloudDownloads.filter { $0.link == userTorrentLink }
if let existingLink = existingLinks[safe: 0]?.download {
downloadUrl = existingLink
} else {
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: userTorrentLink)
func checkRdUserDownloads(userTorrentLink: String) async -> String? {
do {
let existingLinks = realDebridCloudDownloads.first { $0.link == userTorrentLink }
if let existingLink = existingLinks?.download {
return existingLink
} else {
return try await realDebrid.unrestrictLink(debridDownloadLink: userTorrentLink)
}
} catch {
await sendDebridError(error, prefix: "RealDebrid download check error")
downloadUrl = downloadLink
return nil
}
}
func fetchAdDownload(magnet: Magnet?) async {
guard let magnet else {
toastModel?.updateToastDescription("Could not run your action because the magnet is invalid.")
print("AllDebrid error: Invalid magnet")
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)
return
let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" }
lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.id ?? 0]?.link
}
do {
let magnetID = try await allDebrid.addMagnet(magnet: magnet)
let lockedLink = try await allDebrid.fetchMagnetStatus(
magnetId: magnetID,
selectedIndex: selectedAllDebridFile?.id ?? 0
)
let unlockedLink = try await allDebrid.unlockLink(lockedLink: lockedLink)
if let lockedLink {
downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink)
} else if let magnet {
let magnetID = try await allDebrid.addMagnet(magnet: magnet)
let lockedLink = try await allDebrid.fetchMagnetStatus(
magnetId: magnetID,
selectedIndex: selectedAllDebridFile?.id ?? 0
)
downloadUrl = unlockedLink
downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink)
} else {
throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API")
}
} catch {
await sendDebridError(error, prefix: "AllDebrid download error", cancelString: "Download cancelled")
}
}
// Refreshes torrents and downloads from a RD user's account
public func fetchAdCloud(bypassTTL: Bool = false) async {
if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL {
do {
allDebridCloudMagnets = try await allDebrid.userMagnets()
realDebridCloudDownloads = try await realDebrid.userDownloads()
// 5 minutes
allDebridCloudTTL = Date().timeIntervalSince1970 + 300
} catch {
await sendDebridError(error, prefix: "AlLDebrid cloud fetch error")
}
}
}
func deleteAdMagnet(magnetId: Int) async {
do {
try await allDebrid.deleteMagnet(magnetId: magnetId)
await fetchAdCloud(bypassTTL: true)
} catch {
await sendDebridError(error, prefix: "AllDebrid delete error")
}
}
func fetchPmDownload(cloudItemId: String? = nil) async {
do {
if let cloudItemId = cloudItemId {
if let cloudItemId {
downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link
} else if let premiumizeFile = selectedPremiumizeFile {
downloadUrl = premiumizeFile.streamUrlString

View file

@ -0,0 +1,93 @@
//
// AllDebridCloudView.swift
// Ferrite
//
// Created by Brian Dashore on 1/5/23.
//
import SwiftUI
struct AllDebridCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel
@State private var viewTask: Task<Void, Never>?
var body: some View {
DisclosureGroup("Magnets") {
ForEach(debridManager.allDebridCloudMagnets, id: \.id) { magnet in
Button {
if magnet.status == "Ready" && !magnet.links.isEmpty {
navModel.resultFromCloud = true
navModel.selectedTitle = magnet.filename
var historyInfo = HistoryEntryJson(
name: magnet.filename,
source: DebridType.allDebrid.toString()
)
Task {
if magnet.links.count == 1 {
if let lockedLink = magnet.links[safe: 0]?.link {
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: lockedLink)
if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo)
navModel.runDebridAction(urlString: debridManager.downloadUrl)
}
}
} else {
debridManager.clearIAValues()
let magnet = Magnet(hash: magnet.hash, link: nil)
await debridManager.populateDebridIA([magnet])
if debridManager.selectDebridResult(magnet: magnet) {
navModel.selectedHistoryInfo = historyInfo
navModel.currentChoiceSheet = .batch
}
}
}
}
} label: {
VStack(alignment: .leading, spacing: 10) {
Text(magnet.filename)
HStack {
Text(magnet.status)
Spacer()
DebridLabelView(cloudLinks: magnet.links.map(\.link))
}
.font(.caption)
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.backport.tint(.black)
}
.onDelete { offsets in
for index in offsets {
if let magnet = debridManager.allDebridCloudMagnets[safe: index] {
Task {
await debridManager.deleteAdMagnet(magnetId: magnet.id)
}
}
}
}
}
.onAppear {
viewTask = Task {
await debridManager.fetchAdCloud()
}
}
.onDisappear {
viewTask?.cancel()
}
}
}
struct AllDebridCloudView_Previews: PreviewProvider {
static var previews: some View {
AllDebridCloudView()
}
}

View file

@ -14,8 +14,6 @@ struct PremiumizeCloudView: View {
@State private var viewTask: Task<Void, Never>?
@State private var searchText: String = ""
var body: some View {
DisclosureGroup("Items") {
ForEach(debridManager.premiumizeCloudItems, id: \.id) { item in
@ -24,14 +22,14 @@ struct PremiumizeCloudView: View {
navModel.resultFromCloud = true
navModel.selectedTitle = item.name
await debridManager.fetchPmDownload(cloudItemId: item.id)
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: item.id)
if !debridManager.downloadUrl.isEmpty {
PersistenceController.shared.createHistory(
HistoryEntryJson(
name: item.name,
url: debridManager.downloadUrl,
source: "Premiumize"
source: DebridType.premiumize.toString()
)
)

View file

@ -48,27 +48,24 @@ struct RealDebridCloudView: View {
DisclosureGroup("Torrents") {
ForEach(debridManager.realDebridCloudTorrents, id: \.self) { torrentResponse in
Button {
Task {
if torrentResponse.status == "downloaded" && !torrentResponse.links.isEmpty {
navModel.resultFromCloud = true
navModel.selectedTitle = torrentResponse.filename
if torrentResponse.status == "downloaded" && !torrentResponse.links.isEmpty {
navModel.resultFromCloud = true
navModel.selectedTitle = torrentResponse.filename
var historyInfo = HistoryEntryJson(
name: torrentResponse.filename,
source: DebridType.realDebrid.toString()
)
var historyInfo = HistoryEntryJson(
name: torrentResponse.filename,
source: DebridType.realDebrid.toString()
)
Task {
if torrentResponse.links.count == 1 {
if let downloadLink = torrentResponse.links[safe: 0] {
do {
try await debridManager.checkRdUserDownloads(userTorrentLink: downloadLink)
navModel.selectedTitle = torrentResponse.filename
historyInfo.url = downloadLink
if let torrentLink = torrentResponse.links[safe: 0] {
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink)
if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo)
navModel.currentChoiceSheet = .magnet
} catch {
debridManager.toastModel?.updateToastDescription("RealDebrid cloud fetch error: \(error)")
navModel.runDebridAction(urlString: debridManager.downloadUrl)
}
}
} else {

View file

@ -6,7 +6,6 @@
//
import SwiftUI
import SwiftUIX
struct DebridCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@ -20,7 +19,9 @@ struct DebridCloudView: View {
RealDebridCloudView()
case .premiumize:
PremiumizeCloudView()
case .allDebrid, .none:
case .allDebrid:
AllDebridCloudView()
case .none:
EmptyView()
}
}

View file

@ -51,6 +51,10 @@ struct SearchResultButtonView: View {
}
case .partial:
if debridManager.selectDebridResult(magnet: result.magnet) {
navModel.selectedHistoryInfo = HistoryEntryJson(
name: result.title,
source: result.source
)
navModel.currentChoiceSheet = .batch
}
case .none:

View file

@ -72,7 +72,7 @@ struct LibraryView: View {
EmptyInstructionView(title: "No History", message: "Start watching to build history")
}
case .debridCloud:
if debridManager.selectedDebridType == nil || debridManager.selectedDebridType == .allDebrid {
if debridManager.selectedDebridType == nil {
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
}
}

View file

@ -161,7 +161,7 @@ struct SettingsView: View {
await debridManager.handleCallback(url: callbackURL, error: error)
}
}
.prefersEphemeralWebBrowserSession(false)
.prefersEphemeralWebBrowserSession(true)
}
.navigationTitle("Settings")
}

View file

@ -78,6 +78,7 @@ struct BatchChoiceView: View {
if var selectedHistoryInfo = navModel.selectedHistoryInfo {
selectedHistoryInfo.url = debridManager.downloadUrl
selectedHistoryInfo.subName = fileName
PersistenceController.shared.createHistory(selectedHistoryInfo)
}