From 9b7bc55a25a24352e1ca64a685efa450e6d715e7 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 2 Jan 2023 11:29:30 -0500 Subject: [PATCH] Library: Add support for RealDebrid cloud RealDebrid saves a user's unrestricted links and "torrents" (magnet links in this case). Add the ability to see and queue a user's RD library in Ferrite itself. This required a further abstraction of the debrid manager to allow for more types other than search results to be passed to various functions. Deleting an item from RD's cloud list deletes the item from RD as well. NOTE: This does not track download progress, but it does show if a magnet is currently being downloaded or not. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 16 ++ Ferrite/API/RealDebridWrapper.swift | 7 + .../PersistenceController.swift | 6 +- Ferrite/Extensions/String.swift | 11 +- Ferrite/Models/BackupModels.swift | 8 +- Ferrite/Models/DebridManagerModels.swift | 2 +- Ferrite/Models/RealDebridModels.swift | 4 +- Ferrite/ViewModels/BackupManager.swift | 2 +- Ferrite/ViewModels/DebridManager.swift | 146 ++++++++++++------ Ferrite/ViewModels/NavigationViewModel.swift | 5 + .../Debrid/DebridLabelView.swift | 45 +++--- .../Library/BookmarksView.swift | 6 +- .../Library/Cloud/RealDebridCloudView.swift | 143 +++++++++++++++++ .../Library/DebridCloudView.swift | 31 ++++ .../SearchResult/SearchResultButtonView.swift | 27 +++- .../SearchResult/SearchResultInfoView.swift | 12 +- Ferrite/Views/ContentView.swift | 8 +- Ferrite/Views/LibraryView.swift | 14 +- .../Views/SheetViews/BatchChoiceView.swift | 41 ++--- .../Views/SheetViews/MagnetChoiceView.swift | 46 +++--- 20 files changed, 434 insertions(+), 146 deletions(-) create mode 100644 Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift create mode 100644 Ferrite/Views/ComponentViews/Library/DebridCloudView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 70faf40..3748eb0 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; + 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; }; + 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; }; 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; }; 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; @@ -122,6 +124,8 @@ 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = ""; }; 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = ""; }; + 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = ""; }; + 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = ""; }; 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = ""; }; 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; @@ -306,6 +310,14 @@ path = Models; sourceTree = ""; }; + 0C2886D52960C4F800D6FC16 /* Cloud */ = { + isa = PBXGroup; + children = ( + 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */, + ); + path = Cloud; + sourceTree = ""; + }; 0C44E2A628D4DDC6007711AE /* Classes */ = { isa = PBXGroup; children = ( @@ -482,7 +494,9 @@ 0CA3B23528C265FD00616D3A /* Library */ = { isa = PBXGroup; children = ( + 0C2886D52960C4F800D6FC16 /* Cloud */, 0CA3B23828C2660D00616D3A /* BookmarksView.swift */, + 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */, 0CA3B23628C2660700616D3A /* HistoryView.swift */, 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */, 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */, @@ -651,6 +665,7 @@ 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */, 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */, + 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, @@ -722,6 +737,7 @@ 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */, 0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */, 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */, + 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */, 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */, 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */, 0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */, diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index ad6d06f..0f3f9bc 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -358,4 +358,11 @@ public class RealDebrid { return rawResponse } + + public func deleteDownload(debridID: String) async throws { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(debridID)")!) + request.httpMethod = "DELETE" + + try await performRequest(request: &request, requestName: #function) + } } diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index 6983d0a..fc389a2 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -112,9 +112,11 @@ struct PersistenceController { newBookmark.magnetLink = bookmarkJson.magnetLink newBookmark.seeders = bookmarkJson.seeders newBookmark.leechers = bookmarkJson.leechers + + save(backgroundContext) } - func createHistory(entryJson: HistoryEntryJson, date: Double?) { + func createHistory(_ entryJson: HistoryEntryJson, date: Double? = nil) { let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date() let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate) @@ -153,6 +155,8 @@ struct PersistenceController { newHistoryEntry.parentHistory?.dateString = historyDateString newHistoryEntry.parentHistory?.date = historyDate + + save(backgroundContext) } func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? { diff --git a/Ferrite/Extensions/String.swift b/Ferrite/Extensions/String.swift index 20a1917..bbdb0ce 100644 --- a/Ferrite/Extensions/String.swift +++ b/Ferrite/Extensions/String.swift @@ -4,12 +4,21 @@ // // Created by Brian Dashore on 8/31/22. // -// From https://stackoverflow.com/a/59307884 // import Foundation extension String { + // From https://www.hackingwithswift.com/example-code/strings/how-to-capitalize-the-first-letter-of-a-string + func capitalizingFirstLetter() -> String { + return prefix(1).capitalized + dropFirst() + } + + mutating func capitalizeFirstLetter() { + self = self.capitalizingFirstLetter() + } + + // From https://stackoverflow.com/a/59307884 private func compare(toVersion targetVersion: String) -> ComparisonResult { let versionDelimiter = "." var result: ComparisonResult = .orderedSame diff --git a/Ferrite/Models/BackupModels.swift b/Ferrite/Models/BackupModels.swift index 94b3b27..37b0f10 100644 --- a/Ferrite/Models/BackupModels.swift +++ b/Ferrite/Models/BackupModels.swift @@ -26,10 +26,10 @@ struct HistoryJson: Codable { } struct HistoryEntryJson: Codable { - let name: String - let subName: String? - let url: String - let timeStamp: Double? + var name: String? = nil + var subName: String? = nil + var url: String? = nil + var timeStamp: Double? = nil let source: String? } diff --git a/Ferrite/Models/DebridManagerModels.swift b/Ferrite/Models/DebridManagerModels.swift index 0cdf395..278d1c0 100644 --- a/Ferrite/Models/DebridManagerModels.swift +++ b/Ferrite/Models/DebridManagerModels.swift @@ -36,6 +36,6 @@ public enum DebridType: Int, Codable, Hashable, CaseIterable { // Wrapper struct for magnet links to contain both the link and hash for easy access public struct Magnet: Codable, Hashable, Sendable { - let link: String + let link: String? let hash: String } diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index 3a9e1a0..2223959 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -150,7 +150,7 @@ public extension RealDebrid { let bytes, selected: Int } - struct UserTorrentsResponse: Codable, Sendable { + struct UserTorrentsResponse: Codable, Hashable, Sendable { let id, filename, hash: String let bytes: Int let host: String @@ -183,7 +183,7 @@ public extension RealDebrid { // MARK: - User downloads list - struct UserDownloadsResponse: Codable, Sendable { + struct UserDownloadsResponse: Codable, Hashable, Sendable { let id, filename: String let mimeType: String? let filesize: Int diff --git a/Ferrite/ViewModels/BackupManager.swift b/Ferrite/ViewModels/BackupManager.swift index 045ecf9..5e7c516 100644 --- a/Ferrite/ViewModels/BackupManager.swift +++ b/Ferrite/ViewModels/BackupManager.swift @@ -123,7 +123,7 @@ public class BackupManager: ObservableObject { if let storedHistories = backup.history { for storedHistory in storedHistories { for storedEntry in storedHistory.entries { - PersistenceController.shared.createHistory(entryJson: storedEntry, date: storedHistory.date) + PersistenceController.shared.createHistory(storedEntry, date: storedHistory.date) } } } diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 8cb3c78..020335a 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -50,6 +50,11 @@ public class DebridManager: ObservableObject { var selectedRealDebridFile: RealDebrid.IAFile? var selectedRealDebridID: String? + // RealDebrid cloud variables + @Published var realDebridCloudTorrents: [RealDebrid.UserTorrentsResponse] = [] + @Published var realDebridCloudDownloads: [RealDebrid.UserDownloadsResponse] = [] + var realDebridCloudTTL: Double = 0.0 + // AllDebrid auth variables @Published var allDebridAuthProcessing: Bool = false @@ -107,6 +112,30 @@ public class DebridManager: ObservableObject { } } + // Cleans all cached IA values in the event of a full IA refresh + public func clearIAValues() { + realDebridIAValues = [] + allDebridIAValues = [] + premiumizeIAValues = [] + } + + // Clears all selected files and items + public func clearSelectedDebridItems() { + switch selectedDebridType { + case .realDebrid: + selectedRealDebridFile = nil + selectedRealDebridItem = nil + case .allDebrid: + selectedAllDebridFile = nil + selectedAllDebridItem = nil + case .premiumize: + selectedPremiumizeFile = nil + selectedPremiumizeItem = nil + case .none: + break + } + } + // Common function to populate hashes for debrid services public func populateDebridIA(_ resultMagnets: [Magnet]) async { do { @@ -153,7 +182,16 @@ public class DebridManager: ObservableObject { } if enabledDebrids.contains(.premiumize) { - let availableMagnets = try await premiumize.divideCacheRequests(magnets: sendMagnets) + // Only strip magnets that don't have an associated link for PM + let strippedResultMagnets: [Magnet] = resultMagnets.compactMap { + if let magnetLink = $0.link { + return Magnet(link: magnetLink, hash: $0.hash) + } else { + return nil + } + } + + let availableMagnets = try await premiumize.divideCacheRequests(magnets: strippedResultMagnets) // Split DDL requests into chunks of 10 for chunk in availableMagnets.chunked(into: 10) { @@ -174,15 +212,15 @@ public class DebridManager: ObservableObject { } } - // Common function to match search results with a provided debrid service - public func matchSearchResult(result: SearchResult?) -> IAStatus { - guard let result else { + // Common function to match a magnet hash with a provided debrid service + public func matchMagnetHash(_ magnetHash: String?) -> IAStatus { + guard let magnetHash else { return .none } switch selectedDebridType { case .realDebrid: - guard let realDebridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { + guard let realDebridMatch = realDebridIAValues.first(where: { magnetHash == $0.hash }) else { return .none } @@ -192,7 +230,7 @@ public class DebridManager: ObservableObject { return .partial } case .allDebrid: - guard let allDebridMatch = allDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { + guard let allDebridMatch = allDebridIAValues.first(where: { magnetHash == $0.hash }) else { return .none } @@ -202,7 +240,7 @@ public class DebridManager: ObservableObject { return .full } case .premiumize: - guard let premiumizeMatch = premiumizeIAValues.first(where: { result.magnetHash == $0.hash }) else { + guard let premiumizeMatch = premiumizeIAValues.first(where: { magnetHash == $0.hash }) else { return .none } @@ -216,8 +254,8 @@ public class DebridManager: ObservableObject { } } - public func selectDebridResult(result: SearchResult) -> Bool { - guard let magnetHash = result.magnetHash else { + public func selectDebridResult(magnetHash: String?) -> Bool { + guard let magnetHash = magnetHash else { toastModel?.updateToastDescription("Could not find the torrent magnet hash") return false } @@ -429,7 +467,7 @@ public class DebridManager: ObservableObject { // MARK: - Debrid fetch UI linked functions // Common function to delegate what debrid service to fetch from - public func fetchDebridDownload(searchResult: SearchResult) async { + public func fetchDebridDownload(magnetLink: String?) async { defer { currentDebridTask = nil showLoadingProgress = false @@ -437,21 +475,11 @@ public class DebridManager: ObservableObject { showLoadingProgress = true - // Premiumize doesn't need a magnet link - guard searchResult.magnetLink != nil || selectedDebridType == .premiumize else { - toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") - print("Debrid error: Invalid magnet link") - - return - } - - // Force unwrap is OK for debrid types that aren't ignored since the magnet link was already checked - // Do not force unwrap for Premiumize! switch selectedDebridType { case .realDebrid: - await fetchRdDownload(magnetLink: searchResult.magnetLink!) + await fetchRdDownload(magnetLink: magnetLink) case .allDebrid: - await fetchAdDownload(magnetLink: searchResult.magnetLink!) + await fetchAdDownload(magnetLink: magnetLink) case .premiumize: fetchPmDownload() case .none: @@ -459,38 +487,32 @@ public class DebridManager: ObservableObject { } } - func fetchRdDownload(magnetLink: String) async { + func fetchRdDownload(magnetLink: String?) async { do { - var fileIds: [Int] = [] - - if let iaFile = selectedRealDebridFile { - guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else { - return - } - - fileIds = iaBatchFromFile.files.map(\.id) - } + // Bypass the TTL since a download needs to be queried + await fetchRdCloud(bypassTTL: true) // 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 existingTorrents = realDebridCloudTorrents.filter { $0.hash == selectedRealDebridItem?.hash && $0.status == "downloaded" } // 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 { - downloadUrl = existingLink - } else { - let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) - - downloadUrl = downloadLink - } - - } else { + try await checkRdUserDownloads(userTorrentLink: torrentLink) + } else if let magnetLink = magnetLink { // Add a magnet after all the cache checks fail selectedRealDebridID = try await realDebrid.addMagnet(magnetLink: magnetLink) + var fileIds: [Int] = [] + if let iaFile = selectedRealDebridFile { + guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else { + return + } + + fileIds = iaBatchFromFile.files.map(\.id) + } + if let realDebridId = selectedRealDebridID { try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds) @@ -504,6 +526,9 @@ public class DebridManager: ObservableObject { } else { 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") } } catch { switch error { @@ -528,6 +553,21 @@ public class DebridManager: ObservableObject { } } + // Refreshes torrents and downloads from a RD user's account + public func fetchRdCloud(bypassTTL: Bool = false) async { + if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL { + do { + realDebridCloudTorrents = try await realDebrid.userTorrents() + realDebridCloudDownloads = try await realDebrid.userDownloads() + + // 5 minutes + realDebridCloudTTL = Date().timeIntervalSince1970 + 300 + } catch { + toastModel?.updateToastDescription("RealDebrid cloud fetch error: \(error)") + } + } + } + func deleteRdTorrent() async { if let realDebridId = selectedRealDebridID { try? await realDebrid.deleteTorrent(debridID: realDebridId) @@ -536,7 +576,25 @@ public class DebridManager: ObservableObject { selectedRealDebridID = nil } - func fetchAdDownload(magnetLink: String) async { + 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) + + downloadUrl = downloadLink + } + } + + func fetchAdDownload(magnetLink: String?) async { + guard let magnetLink = magnetLink else { + toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") + print("AllDebrid error: Invalid magnet link") + + return + } + do { let magnetID = try await allDebrid.addMagnet(magnetLink: magnetLink) let lockedLink = try await allDebrid.fetchMagnetStatus( diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index e1a7cf3..04d707c 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -33,6 +33,9 @@ class NavigationViewModel: ObservableObject { @Published var isSearching: Bool = false @Published var selectedSearchResult: SearchResult? + @Published var selectedMagnetLink: String? + @Published var selectedHistoryInfo: HistoryEntryJson? + @Published var resultFromCloud: Bool = false // For giving information in magnet choice sheet @Published var selectedTitle: String = "" @@ -124,6 +127,7 @@ class NavigationViewModel: ObservableObject { } } + /* public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) { let backgroundContext = PersistenceController.shared.backgroundContext @@ -141,4 +145,5 @@ class NavigationViewModel: ObservableObject { PersistenceController.shared.save(backgroundContext) } + */ } diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift index 6d1bdcc..21740b1 100644 --- a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift +++ b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift @@ -10,27 +10,36 @@ import SwiftUI struct DebridLabelView: View { @EnvironmentObject var debridManager: DebridManager - var result: SearchResult - - let debridAbbreviation: String + @State var cloudLinks: [String] = [] + var magnetHash: String? var body: some View { - Text(debridAbbreviation) - .fontWeight(.bold) - .padding(2) - .background { - Group { - switch debridManager.matchSearchResult(result: result) { - case .full: - Color.green - case .partial: - Color.orange - case .none: - Color.red + if let selectedDebridType = debridManager.selectedDebridType { + Text(selectedDebridType.toString(abbreviated: true)) + .fontWeight(.bold) + .padding(2) + .background { + Group { + if cloudLinks.isEmpty { + switch debridManager.matchMagnetHash(magnetHash) { + case .full: + Color.green + case .partial: + Color.orange + case .none: + Color.red + } + } else if cloudLinks.count == 1 { + Color.green + } else if cloudLinks.count > 1 { + Color.orange + } else { + Color.red + } } + .cornerRadius(4) + .opacity(0.5) } - .cornerRadius(4) - .opacity(0.5) - } + } } } diff --git a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift index 10ad01d..9167a78 100644 --- a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift +++ b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift @@ -31,7 +31,7 @@ struct BookmarksView: View { if let bookmark = bookmarks[safe: index] { PersistenceController.shared.delete(bookmark, context: backgroundContext) - NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) + NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark) } } } @@ -55,8 +55,8 @@ struct BookmarksView: View { if debridManager.enabledDebrids.count > 0 { viewTask = Task { let magnets = bookmarks.compactMap { - if let magnetLink = $0.magnetLink, let magnetHash = $0.magnetHash { - return Magnet(link: magnetLink, hash: magnetHash) + if let magnetHash = $0.magnetHash { + return Magnet(link: $0.magnetLink, hash: magnetHash) } else { return nil } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift new file mode 100644 index 0000000..3397479 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -0,0 +1,143 @@ +// +// RealDebridCloudView.swift +// Ferrite +// +// Created by Brian Dashore on 12/31/22. +// + +import SwiftUI + +struct RealDebridCloudView: View { + @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager + + @State private var viewTask: Task? + + var body: some View { + Group { + DisclosureGroup("Downloads") { + ForEach(debridManager.realDebridCloudDownloads, id: \.self) { downloadResponse in + Button(downloadResponse.filename) { + navModel.resultFromCloud = true + navModel.selectedTitle = downloadResponse.filename + debridManager.downloadUrl = downloadResponse.link + + PersistenceController.shared.createHistory( + HistoryEntryJson( + name: downloadResponse.filename, + url: downloadResponse.link, + source: DebridType.realDebrid.toString() + ) + ) + + navModel.runDebridAction(urlString: debridManager.downloadUrl) + } + .backport.tint(.primary) + } + .onDelete { offsets in + for index in offsets { + if let downloadResponse = debridManager.realDebridCloudDownloads[safe: index] { + Task { + do { + try await debridManager.realDebrid.deleteDownload(debridID: downloadResponse.id) + + // Bypass TTL to get current RD values + await debridManager.fetchRdCloud(bypassTTL: true) + } catch { + print(error) + } + } + } + } + } + } + + 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 + + var historyInfo = HistoryEntryJson( + name: torrentResponse.filename, + source: DebridType.realDebrid.toString() + ) + + 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 + + PersistenceController.shared.createHistory(historyInfo) + navModel.currentChoiceSheet = .magnet + } catch { + debridManager.toastModel?.updateToastDescription("RealDebrid cloud fetch error: \(error)") + } + } + } else { + debridManager.clearIAValues() + await debridManager.populateDebridIA([Magnet(link: nil, hash: torrentResponse.hash)]) + + if debridManager.selectDebridResult(magnetHash: torrentResponse.hash) { + navModel.selectedHistoryInfo = historyInfo + navModel.currentChoiceSheet = .batch + } + } + } + } + } label: { + VStack(alignment: .leading, spacing: 10) { + Text(torrentResponse.filename) + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(4) + + HStack { + Text(torrentResponse.status.capitalizingFirstLetter()) + Spacer() + DebridLabelView(cloudLinks: torrentResponse.links) + } + .font(.caption) + } + } + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + .backport.tint(.primary) + } + .onDelete { offsets in + for index in offsets { + if let torrentResponse = debridManager.realDebridCloudTorrents[safe: index] { + Task { + do { + try await debridManager.realDebrid.deleteTorrent(debridID: torrentResponse.id) + + // Bypass TTL to get current RD values + await debridManager.fetchRdCloud(bypassTTL: true) + } catch { + print(error) + } + } + } + } + } + } + } + .onAppear { + viewTask = Task { + await debridManager.fetchRdCloud() + } + } + .onDisappear { + viewTask?.cancel() + } + } +} + +struct RealDebridCloudView_Previews: PreviewProvider { + static var previews: some View { + RealDebridCloudView() + } +} diff --git a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift new file mode 100644 index 0000000..54125f0 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift @@ -0,0 +1,31 @@ +// +// DebridCloudView.swift +// Ferrite +// +// Created by Brian Dashore on 12/31/22. +// + +import SwiftUI + +struct DebridCloudView: View { + @EnvironmentObject var debridManager: DebridManager + + var body: some View { + List { + switch debridManager.selectedDebridType { + case .realDebrid: + RealDebridCloudView() + case .allDebrid, .premiumize, .none: + EmptyView() + } + } + .inlinedList() + .listStyle(.insetGrouped) + } +} + +struct DebridCloudView_Previews: PreviewProvider { + static var previews: some View { + DebridCloudView() + } +} diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index debe4a1..ce424b3 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -24,15 +24,23 @@ struct SearchResultButtonView: View { if debridManager.currentDebridTask == nil { navModel.selectedSearchResult = result navModel.selectedTitle = result.title ?? "" + navModel.resultFromCloud = false - switch debridManager.matchSearchResult(result: result) { + switch debridManager.matchMagnetHash(result.magnetHash) { case .full: - if debridManager.selectDebridResult(result: result) { + if debridManager.selectDebridResult(magnetHash: result.magnetHash) { debridManager.currentDebridTask = Task { - await debridManager.fetchDebridDownload(searchResult: result) + await debridManager.fetchDebridDownload(magnetLink: result.magnetLink) if !debridManager.downloadUrl.isEmpty { - navModel.addToHistory(name: result.title, source: result.source, url: debridManager.downloadUrl) + PersistenceController.shared.createHistory( + HistoryEntryJson( + name: result.title, + url: debridManager.downloadUrl, + source: result.source + ) + ) + navModel.runDebridAction(urlString: debridManager.downloadUrl) if navModel.currentChoiceSheet != .magnet { @@ -42,11 +50,18 @@ struct SearchResultButtonView: View { } } case .partial: - if debridManager.selectDebridResult(result: result) { + if debridManager.selectDebridResult(magnetHash: result.magnetHash) { navModel.currentChoiceSheet = .batch } case .none: - navModel.addToHistory(name: result.title, source: result.source, url: result.magnetLink) + PersistenceController.shared.createHistory( + HistoryEntryJson( + name: result.title, + url: result.magnetLink, + source: result.source + ) + ) + navModel.runMagnetAction(magnetString: result.magnetLink) } } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift index cceec97..86d1751 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift @@ -30,17 +30,7 @@ struct SearchResultInfoView: View { Text(size) } - if debridManager.selectedDebridType == .realDebrid { - DebridLabelView(result: result, debridAbbreviation: "RD") - } - - if debridManager.selectedDebridType == .allDebrid { - DebridLabelView(result: result, debridAbbreviation: "AD") - } - - if debridManager.selectedDebridType == .premiumize { - DebridLabelView(result: result, debridAbbreviation: "PM") - } + DebridLabelView(magnetHash: result.magnetHash) } .font(.caption) } diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 145ea41..10c15d0 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -87,12 +87,12 @@ struct ContentView: View { await scrapingModel.scanSources(sources: sources) if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { - debridManager.realDebridIAValues = [] - debridManager.allDebridIAValues = [] + debridManager.clearIAValues() + // Remove magnets that don't have a hash let magnets = scrapingModel.searchResults.compactMap { - if let magnetLink = $0.magnetLink, let magnetHash = $0.magnetHash { - return Magnet(link: magnetLink, hash: magnetHash) + if let magnetHash = $0.magnetHash { + return Magnet(link: $0.magnetLink, hash: magnetHash) } else { return nil } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 87e3e1f..4824e79 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -11,9 +11,11 @@ struct LibraryView: View { enum LibraryPickerSegment { case bookmarks case history + case debridCloud } @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager @FetchRequest( entity: Bookmark.entity(), @@ -40,6 +42,10 @@ struct LibraryView: View { Picker("Segments", selection: $selectedSegment) { Text("Bookmarks").tag(LibraryPickerSegment.bookmarks) Text("History").tag(LibraryPickerSegment.history) + + if !debridManager.enabledDebrids.isEmpty { + Text("Cloud").tag(LibraryPickerSegment.debridCloud) + } } .pickerStyle(.segmented) .padding() @@ -49,6 +55,8 @@ struct LibraryView: View { BookmarksView(bookmarks: bookmarks) case .history: HistoryView(history: history) + case .debridCloud: + DebridCloudView() } Spacer() @@ -63,6 +71,10 @@ struct LibraryView: View { if history.isEmpty { EmptyInstructionView(title: "No History", message: "Start watching to build history") } + case .debridCloud: + if debridManager.selectedDebridType != .realDebrid { + EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") + } } } .navigationTitle("Library") @@ -72,7 +84,7 @@ struct LibraryView: View { EditButton() switch selectedSegment { - case .bookmarks: + case .bookmarks, .debridCloud: DebridChoiceView() case .history: HistoryActionsView() diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index d527ba6..90ecd01 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -58,7 +58,8 @@ struct BatchChoiceView: View { Task { try? await Task.sleep(seconds: 1) - debridManager.selectedRealDebridItem = nil + + debridManager.clearSelectedDebridItems() } } } @@ -68,36 +69,22 @@ struct BatchChoiceView: View { // Common function to communicate betwen VMs and queue/display a download func queueCommonDownload(fileName: String) { - if let searchResult = navModel.selectedSearchResult { - debridManager.currentDebridTask = Task { - await debridManager.fetchDebridDownload(searchResult: searchResult) + debridManager.currentDebridTask = Task { + await debridManager.fetchDebridDownload(magnetLink: navModel.resultFromCloud ? nil : navModel.selectedMagnetLink) - if !debridManager.downloadUrl.isEmpty { - try? await Task.sleep(seconds: 1) - navModel.selectedBatchTitle = fileName - navModel.addToHistory( - name: searchResult.title, - source: searchResult.source, - url: debridManager.downloadUrl, - subName: fileName - ) - navModel.runDebridAction(urlString: debridManager.downloadUrl) + if !debridManager.downloadUrl.isEmpty { + try? await Task.sleep(seconds: 1) + navModel.selectedBatchTitle = fileName + + if var selectedHistoryInfo = navModel.selectedHistoryInfo { + selectedHistoryInfo.url = debridManager.downloadUrl + PersistenceController.shared.createHistory(selectedHistoryInfo) } - switch debridManager.selectedDebridType { - case .realDebrid: - debridManager.selectedRealDebridFile = nil - debridManager.selectedRealDebridItem = nil - case .allDebrid: - debridManager.selectedAllDebridFile = nil - debridManager.selectedAllDebridItem = nil - case .premiumize: - debridManager.selectedPremiumizeFile = nil - debridManager.selectedPremiumizeItem = nil - case .none: - break - } + navModel.runDebridAction(urlString: debridManager.downloadUrl) } + + debridManager.clearSelectedDebridItems() } navModel.currentChoiceSheet = nil diff --git a/Ferrite/Views/SheetViews/MagnetChoiceView.swift b/Ferrite/Views/SheetViews/MagnetChoiceView.swift index dfef583..aaeaf31 100644 --- a/Ferrite/Views/SheetViews/MagnetChoiceView.swift +++ b/Ferrite/Views/SheetViews/MagnetChoiceView.swift @@ -71,30 +71,31 @@ struct MagnetChoiceView: View { } } - Section(header: "Magnet options") { - ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") { - UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink - showMagnetCopyAlert.toggle() - } - .backport.alert( - isPresented: $showMagnetCopyAlert, - title: "Copied", - message: "Magnet link copied successfully", - buttons: [AlertButton("OK")] - ) - - ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") { - if let result = navModel.selectedSearchResult, - let magnetLink = result.magnetLink, - let url = URL(string: magnetLink) - { - navModel.activityItems = [url] - navModel.showLocalActivitySheet.toggle() + if !navModel.resultFromCloud { + Section(header: "Magnet options") { + ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") { + UIPasteboard.general.string = navModel.selectedMagnetLink + showMagnetCopyAlert.toggle() } - } + .backport.alert( + isPresented: $showMagnetCopyAlert, + title: "Copied", + message: "Magnet link copied successfully", + buttons: [AlertButton("OK")] + ) - ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { - navModel.runMagnetAction(magnetString: navModel.selectedSearchResult?.magnetLink, .webtor) + ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") { + if let magnetLink = navModel.selectedMagnetLink, + let url = URL(string: magnetLink) + { + navModel.activityItems = [url] + navModel.showLocalActivitySheet.toggle() + } + } + + ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { + navModel.runMagnetAction(magnetString: navModel.selectedMagnetLink, .webtor) + } } } } @@ -111,6 +112,7 @@ struct MagnetChoiceView: View { debridManager.downloadUrl = "" navModel.selectedTitle = "" navModel.selectedBatchTitle = "" + navModel.resultFromCloud = false } .navigationTitle("Link actions") .navigationBarTitleDisplayMode(.inline)