RealDebrid: Improve fetch and recache times

To declutter a RealDebrid user's library, check if the file and
unrestricted link exist and serve those existing links. Otherwise
perform a download like normal.

Sometimes RealDebrid deletes cached items, but still keeps them on
instant availability. Add a way to tell the user that the item
is downloading along with an option to cancel it.

Also remove unnecessary published variables from viewmodels

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2022-09-16 12:29:08 -04:00
parent 4d3a16f77e
commit b85752c92c
10 changed files with 161 additions and 59 deletions

View file

@ -14,6 +14,7 @@ public enum RealDebridError: Error {
case InvalidResponse case InvalidResponse
case InvalidToken case InvalidToken
case EmptyData case EmptyData
case EmptyTorrents
case FailedRequest(description: String) case FailedRequest(description: String)
case AuthQuery(description: String) case AuthQuery(description: String)
} }
@ -306,21 +307,33 @@ public class RealDebrid {
try await performRequest(request: &request, requestName: #function) try await performRequest(request: &request, requestName: #function)
} }
// Fetches the info of a torrent // Gets the info of a torrent from a given ID
public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String { public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!) var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
let data = try await performRequest(request: &request, requestName: #function) let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data) let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
// Error out if no index is provided // Let the user know if a torrent is downloading
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1] { if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" {
return torrentLink return torrentLink
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
throw RealDebridError.EmptyTorrents
} else { } else {
throw RealDebridError.EmptyData throw RealDebridError.EmptyData
} }
} }
// Gets the user's torrent library
public func userTorrents() async throws -> [UserTorrentsResponse] {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data)
return rawResponse
}
// Deletes a torrent download from RD // Deletes a torrent download from RD
public func deleteTorrent(debridID: String) async throws { public func deleteTorrent(debridID: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!) var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!)
@ -345,4 +358,14 @@ public class RealDebrid {
return rawResponse.download return rawResponse.download
} }
// Gets the user's downloads
public func userDownloads() async throws -> [UserDownloadsResponse] {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data)
return rawResponse
}
} }

View file

@ -122,14 +122,16 @@ struct TorrentInfoResponse: Codable {
let status, added: String let status, added: String
let files: [TorrentInfoFile] let files: [TorrentInfoFile]
let links: [String] let links: [String]
let ended: String let ended: String?
let speed: Int?
let seeders: Int?
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case id, filename case id, filename
case originalFilename = "original_filename" case originalFilename = "original_filename"
case hash, bytes case hash, bytes
case originalBytes = "original_bytes" case originalBytes = "original_bytes"
case host, split, progress, status, added, files, links, ended case host, split, progress, status, added, files, links, ended, speed, seeders
} }
} }
@ -139,6 +141,17 @@ struct TorrentInfoFile: Codable {
let bytes, selected: Int let bytes, selected: Int
} }
public struct UserTorrentsResponse: Codable {
let id, filename, hash: String
let bytes: Int
let host: String
let split, progress: Int
let status, added: String
let links: [String]
let speed, seeders: Int?
let ended: String?
}
// MARK: - unrestrictLink endpoint // MARK: - unrestrictLink endpoint
struct UnrestrictLinkResponse: Codable { struct UnrestrictLinkResponse: Codable {
@ -157,3 +170,23 @@ struct UnrestrictLinkResponse: Codable {
case chunks, crc, download, streamable case chunks, crc, download, streamable
} }
} }
// MARK: - User downloads list
public struct UserDownloadsResponse: Codable {
let id, filename, mimeType: String
let filesize: Int
let link: String
let host: String
let hostIcon: String
let chunks: Int
let download: String
let streamable: Int
let generated: String
enum CodingKeys: String, CodingKey {
case id, filename, mimeType, filesize, link, host
case hostIcon = "host_icon"
case chunks, download, streamable, generated
}
}

View file

@ -19,23 +19,28 @@ public class DebridManager: ObservableObject {
@Published var showLoadingProgress: Bool = false @Published var showLoadingProgress: Bool = false
// Service agnostic variables // Service agnostic variables
@Published var currentDebridTask: Task<Void, Never>? var currentDebridTask: Task<Void, Never>?
// RealDebrid auth variables // RealDebrid auth variables
@Published var realDebridEnabled: Bool = false { var realDebridEnabled: Bool = false {
didSet { didSet {
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled") UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
} }
} }
@Published var realDebridAuthProcessing: Bool = false @Published var realDebridAuthProcessing: Bool = false
@Published var realDebridAuthUrl: String = "" var realDebridAuthUrl: String = ""
// RealDebrid fetch variables // RealDebrid fetch variables
@Published var realDebridIAValues: [RealDebridIA] = [] @Published var realDebridIAValues: [RealDebridIA] = []
@Published var realDebridDownloadUrl: String = "" var realDebridDownloadUrl: String = ""
@Published var selectedRealDebridItem: RealDebridIA?
@Published var selectedRealDebridFile: RealDebridIAFile? @Published var showDeleteAlert: Bool = false
// TODO: Switch to an individual item based sheet system to remove these variables
var selectedRealDebridItem: RealDebridIA?
var selectedRealDebridFile: RealDebridIAFile?
var selectedRealDebridID: String?
init() { init() {
realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled") realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
@ -138,7 +143,7 @@ public class DebridManager: ObservableObject {
} }
} }
public func fetchRdDownload(searchResult: SearchResult, iaFile: RealDebridIAFile? = nil) async { public func fetchRdDownload(searchResult: SearchResult) async {
defer { defer {
currentDebridTask = nil currentDebridTask = nil
showLoadingProgress = false showLoadingProgress = false
@ -153,14 +158,10 @@ public class DebridManager: ObservableObject {
return return
} }
var realDebridId: String?
do { do {
realDebridId = try await realDebrid.addMagnet(magnetLink: magnetLink)
var fileIds: [Int] = [] var fileIds: [Int] = []
if let iaFile = iaFile { if let iaFile = selectedRealDebridFile {
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else { guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
return return
} }
@ -168,29 +169,52 @@ public class DebridManager: ObservableObject {
fileIds = iaBatchFromFile.files.map(\.id) fileIds = iaBatchFromFile.files.map(\.id)
} }
if let realDebridId = realDebridId { // If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds) let existingTorrents = try await realDebrid.userTorrents().filter { $0.hash == selectedRealDebridItem?.hash }
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: iaFile?.batchFileIndex ?? 0) // If the links match from a user's downloads, no need to re-run a download
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) if let existingTorrent = existingTorrents[safe: 0],
let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0]
{
let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink }
if let existingLink = existingLinks[safe: 0]?.download {
realDebridDownloadUrl = existingLink
} else {
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
realDebridDownloadUrl = downloadLink
}
realDebridDownloadUrl = downloadLink
} else { } else {
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.") // Add a magnet after all the cache checks fail
selectedRealDebridID = try await realDebrid.addMagnet(magnetLink: magnetLink)
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)
realDebridDownloadUrl = downloadLink
} else {
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.")
}
} }
} catch { } catch {
let error = error as NSError switch error {
case RealDebridError.EmptyTorrents:
switch error.code { showDeleteAlert.toggle()
case -999:
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
default: default:
toastModel?.updateToastDescription("RealDebrid download error: \(error)") let error = error as NSError
}
// Delete the torrent download if it exists switch error.code {
if let realDebridId = realDebridId { case -999:
try? await realDebrid.deleteTorrent(debridID: realDebridId) toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
default:
toastModel?.updateToastDescription("RealDebrid download error: \(error)")
}
await deleteRdTorrent()
} }
showLoadingProgress = false showLoadingProgress = false
@ -198,4 +222,12 @@ public class DebridManager: ObservableObject {
print("RealDebrid download error: \(error)") print("RealDebrid download error: \(error)")
} }
} }
public func deleteRdTorrent() async {
if let realDebridId = selectedRealDebridID {
try? await realDebrid.deleteTorrent(debridID: realDebridId)
}
selectedRealDebridID = nil
}
} }

View file

@ -37,7 +37,7 @@ class NavigationViewModel: ObservableObject {
@Published var hideNavigationBar = false @Published var hideNavigationBar = false
@Published var currentChoiceSheet: ChoiceSheetType? @Published var currentChoiceSheet: ChoiceSheetType?
@Published var activityItems: [Any] = [] var activityItems: [Any] = []
// Used to show the activity sheet in the share menu // Used to show the activity sheet in the share menu
@Published var showLocalActivitySheet = false @Published var showLocalActivitySheet = false
@ -47,10 +47,10 @@ class NavigationViewModel: ObservableObject {
// Used between SourceListView and SourceSettingsView // Used between SourceListView and SourceSettingsView
@Published var showSourceSettings: Bool = false @Published var showSourceSettings: Bool = false
@Published var selectedSource: Source? var selectedSource: Source?
@Published var showSourceListEditor: Bool = false @Published var showSourceListEditor: Bool = false
@Published var selectedSourceList: SourceList? var selectedSourceList: SourceList?
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none

View file

@ -18,7 +18,7 @@ class ScrapingViewModel: ObservableObject {
var toastModel: ToastViewModel? var toastModel: ToastViewModel?
let byteCountFormatter: ByteCountFormatter = .init() let byteCountFormatter: ByteCountFormatter = .init()
@Published var runningSearchTask: Task<Void, Error>? var runningSearchTask: Task<Void, Error>?
@Published var searchResults: [SearchResult] = [] @Published var searchResults: [SearchResult] = []
@Published var searchText: String = "" @Published var searchText: String = ""
@Published var filteredSource: Source? @Published var filteredSource: Source?

View file

@ -14,7 +14,7 @@ public class SourceManager: ObservableObject {
@Published var availableSources: [SourceJson] = [] @Published var availableSources: [SourceJson] = []
@Published var urlErrorAlertText = "" var urlErrorAlertText = ""
@Published var showUrlErrorAlert = false @Published var showUrlErrorAlert = false
@MainActor @MainActor

View file

@ -23,7 +23,7 @@ struct BatchChoiceView: View {
if let searchResult = navModel.selectedSearchResult { if let searchResult = navModel.selectedSearchResult {
debridManager.currentDebridTask = Task { debridManager.currentDebridTask = Task {
await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file) await debridManager.fetchRdDownload(searchResult: searchResult)
if !debridManager.realDebridDownloadUrl.isEmpty { if !debridManager.realDebridDownloadUrl.isEmpty {
// The download may complete before this sheet dismisses // The download may complete before this sheet dismisses

View file

@ -49,19 +49,20 @@ struct BookmarksView: View {
PersistenceController.shared.save() PersistenceController.shared.save()
} }
} }
.id(UUID())
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.onAppear { }
if realDebridEnabled { }
viewTask = Task { .onAppear {
let hashes = bookmarks.compactMap { $0.magnetHash } if realDebridEnabled {
await debridManager.populateDebridHashes(hashes) viewTask = Task {
} let hashes = bookmarks.compactMap { $0.magnetHash }
} await debridManager.populateDebridHashes(hashes)
}
.onDisappear {
viewTask?.cancel()
} }
} }
} }
.onDisappear {
viewTask?.cancel()
}
} }
} }

View file

@ -28,15 +28,17 @@ struct SearchResultButtonView: View {
switch debridManager.matchSearchResult(result: result) { switch debridManager.matchSearchResult(result: result) {
case .full: case .full:
debridManager.currentDebridTask = Task { if debridManager.setSelectedRdResult(result: result) {
await debridManager.fetchRdDownload(searchResult: result) debridManager.currentDebridTask = Task {
await debridManager.fetchRdDownload(searchResult: result)
if !debridManager.realDebridDownloadUrl.isEmpty {
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl) if !debridManager.realDebridDownloadUrl.isEmpty {
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl)
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
if navModel.currentChoiceSheet != .magnet { if navModel.currentChoiceSheet != .magnet {
debridManager.realDebridDownloadUrl = "" debridManager.realDebridDownloadUrl = ""
}
} }
} }
} }
@ -91,6 +93,19 @@ struct SearchResultButtonView: View {
} }
} }
} }
.dynamicAlert(
isPresented: $debridManager.showDeleteAlert,
title: "Caching file",
message: "RealDebrid is currently caching this file. Would you like to delete it? \n\nProgress can be checked on the RealDebrid website.",
buttons: [
AlertButton("Yes", role: .destructive) {
Task {
await debridManager.deleteRdTorrent()
}
},
AlertButton(role: .cancel)
]
)
.onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in .onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in
existingBookmark = nil existingBookmark = nil
} }

View file

@ -11,8 +11,6 @@ struct SearchResultsView: View {
@EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var navModel: NavigationViewModel
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
var body: some View { var body: some View {
List { List {
ForEach(scrapingModel.searchResults, id: \.self) { result in ForEach(scrapingModel.searchResults, id: \.self) { result in