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 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue