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 InvalidToken
case EmptyData
case EmptyTorrents
case FailedRequest(description: String)
case AuthQuery(description: String)
}
@ -306,21 +307,33 @@ public class RealDebrid {
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 {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
// Error out if no index is provided
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1] {
// Let the user know if a torrent is downloading
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" {
return torrentLink
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
throw RealDebridError.EmptyTorrents
} else {
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
public func deleteTorrent(debridID: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!)
@ -345,4 +358,14 @@ public class RealDebrid {
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 files: [TorrentInfoFile]
let links: [String]
let ended: String
let ended: String?
let speed: Int?
let seeders: Int?
enum CodingKeys: String, CodingKey {
case id, filename
case originalFilename = "original_filename"
case hash, 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
}
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
struct UnrestrictLinkResponse: Codable {
@ -157,3 +170,23 @@ struct UnrestrictLinkResponse: Codable {
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
// Service agnostic variables
@Published var currentDebridTask: Task<Void, Never>?
var currentDebridTask: Task<Void, Never>?
// RealDebrid auth variables
@Published var realDebridEnabled: Bool = false {
var realDebridEnabled: Bool = false {
didSet {
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
}
}
@Published var realDebridAuthProcessing: Bool = false
@Published var realDebridAuthUrl: String = ""
var realDebridAuthUrl: String = ""
// RealDebrid fetch variables
@Published var realDebridIAValues: [RealDebridIA] = []
@Published var realDebridDownloadUrl: String = ""
@Published var selectedRealDebridItem: RealDebridIA?
@Published var selectedRealDebridFile: RealDebridIAFile?
var realDebridDownloadUrl: String = ""
@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() {
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 {
currentDebridTask = nil
showLoadingProgress = false
@ -153,14 +158,10 @@ public class DebridManager: ObservableObject {
return
}
var realDebridId: String?
do {
realDebridId = try await realDebrid.addMagnet(magnetLink: magnetLink)
var fileIds: [Int] = []
if let iaFile = iaFile {
if let iaFile = selectedRealDebridFile {
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
return
}
@ -168,29 +169,52 @@ public class DebridManager: ObservableObject {
fileIds = iaBatchFromFile.files.map(\.id)
}
if let realDebridId = realDebridId {
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
// If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link
let existingTorrents = try await realDebrid.userTorrents().filter { $0.hash == selectedRealDebridItem?.hash }
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: iaFile?.batchFileIndex ?? 0)
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
// 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]
{
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 {
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 {
let error = error as NSError
switch error.code {
case -999:
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
switch error {
case RealDebridError.EmptyTorrents:
showDeleteAlert.toggle()
default:
toastModel?.updateToastDescription("RealDebrid download error: \(error)")
}
let error = error as NSError
// Delete the torrent download if it exists
if let realDebridId = realDebridId {
try? await realDebrid.deleteTorrent(debridID: realDebridId)
switch error.code {
case -999:
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
default:
toastModel?.updateToastDescription("RealDebrid download error: \(error)")
}
await deleteRdTorrent()
}
showLoadingProgress = false
@ -198,4 +222,12 @@ public class DebridManager: ObservableObject {
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 currentChoiceSheet: ChoiceSheetType?
@Published var activityItems: [Any] = []
var activityItems: [Any] = []
// Used to show the activity sheet in the share menu
@Published var showLocalActivitySheet = false
@ -47,10 +47,10 @@ class NavigationViewModel: ObservableObject {
// Used between SourceListView and SourceSettingsView
@Published var showSourceSettings: Bool = false
@Published var selectedSource: Source?
var selectedSource: Source?
@Published var showSourceListEditor: Bool = false
@Published var selectedSourceList: SourceList?
var selectedSourceList: SourceList?
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none

View file

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

View file

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

View file

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

View file

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

View file

@ -28,15 +28,17 @@ struct SearchResultButtonView: View {
switch debridManager.matchSearchResult(result: result) {
case .full:
debridManager.currentDebridTask = Task {
await debridManager.fetchRdDownload(searchResult: result)
if !debridManager.realDebridDownloadUrl.isEmpty {
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl)
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
if debridManager.setSelectedRdResult(result: result) {
debridManager.currentDebridTask = Task {
await debridManager.fetchRdDownload(searchResult: result)
if !debridManager.realDebridDownloadUrl.isEmpty {
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl)
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
if navModel.currentChoiceSheet != .magnet {
debridManager.realDebridDownloadUrl = ""
if navModel.currentChoiceSheet != .magnet {
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
existingBookmark = nil
}

View file

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