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)