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:
parent
4d3a16f77e
commit
b85752c92c
10 changed files with 161 additions and 59 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public class SourceManager: ObservableObject {
|
|||
|
||||
@Published var availableSources: [SourceJson] = []
|
||||
|
||||
@Published var urlErrorAlertText = ""
|
||||
var urlErrorAlertText = ""
|
||||
@Published var showUrlErrorAlert = false
|
||||
|
||||
@MainActor
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue