diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 2159b51..a4a2d25 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; }; 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; }; 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; }; + 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; }; @@ -148,6 +149,7 @@ 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = ""; }; + 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; 0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = ""; }; 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = ""; }; @@ -317,6 +319,7 @@ children = ( 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */, 0CAF9318296399190050812A /* PremiumizeCloudView.swift */, + 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */, ); path = Cloud; sourceTree = ""; @@ -706,6 +709,7 @@ 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */, 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */, 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, + 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 3f76226..fbf0163 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -21,7 +21,6 @@ public class AllDebrid { // Fetches information for PIN auth public func getPinInfo() async throws -> PinResponse { let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get") - print("Auth URL: \(url)") let request = URLRequest(url: url) do { @@ -161,19 +160,40 @@ public class AllDebrid { let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data // Better to fetch no link at all than the wrong link - if let linkWrapper = rawResponse.magnets.links[safe: selectedIndex ?? -1] { + if let linkWrapper = rawResponse.magnets[safe: 0]?.links[safe: selectedIndex ?? -1] { return linkWrapper.link } else { throw ADError.EmptyTorrents } } + public func userMagnets() async throws -> [MagnetStatusData] { + var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status")) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data + + if rawResponse.magnets.isEmpty { + throw ADError.EmptyData + } else { + return rawResponse.magnets + } + } + + public func deleteMagnet(magnetId: Int) async throws { + let queryItems = [ + URLQueryItem(name: "id", value: String(magnetId)) + ] + var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems)) + + try await performRequest(request: &request, requestName: #function) + } + public func unlockLink(lockedLink: String) async throws -> String { let queryItems = [ URLQueryItem(name: "link", value: lockedLink) ] var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/link/unlock", queryItems: queryItems)) - print(request) let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode(ADResponse.self, from: data).data diff --git a/Ferrite/Models/AllDebridModels.swift b/Ferrite/Models/AllDebridModels.swift index b37d130..fec4bc4 100644 --- a/Ferrite/Models/AllDebridModels.swift +++ b/Ferrite/Models/AllDebridModels.swift @@ -83,12 +83,24 @@ public extension AllDebrid { // MARK: - MagnetStatusResponse struct MagnetStatusResponse: Codable { - let magnets: MagnetStatusData + let magnets: [MagnetStatusData] + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let data = try? container.decode(MagnetStatusData.self, forKey: .magnets) { + self.magnets = [data] + } else if let data = try? container.decode([MagnetStatusData].self, forKey: .magnets) { + self.magnets = data + } else { + self.magnets = [] + } + } } // MARK: - MagnetStatusData - internal struct MagnetStatusData: Codable { + struct MagnetStatusData: Codable { let id: Int let filename: String let size: Int diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 77267d5..b4a0255 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -64,6 +64,10 @@ public class DebridManager: ObservableObject { var selectedAllDebridItem: AllDebrid.IA? var selectedAllDebridFile: AllDebrid.IAFile? + // AllDebrid cloud variables + @Published var allDebridCloudMagnets: [AllDebrid.MagnetStatusData] = [] + var allDebridCloudTTL: Double = 0.0 + // Premiumize auth variables @Published var premiumizeAuthProcessing: Bool = false @@ -471,7 +475,8 @@ public class DebridManager: ObservableObject { // MARK: - Debrid fetch UI linked functions // Common function to delegate what debrid service to fetch from - public func fetchDebridDownload(magnet: Magnet?) async { + // Cloudinfo is used for any extra information provided by debrid cloud + public func fetchDebridDownload(magnet: Magnet?, cloudInfo: String? = nil) async { defer { currentDebridTask = nil showLoadingProgress = false @@ -481,29 +486,35 @@ public class DebridManager: ObservableObject { switch selectedDebridType { case .realDebrid: - await fetchRdDownload(magnet: magnet) + await fetchRdDownload(magnet: magnet, existingLink: cloudInfo) case .allDebrid: - await fetchAdDownload(magnet: magnet) + await fetchAdDownload(magnet: magnet, existingLockedLink: cloudInfo) case .premiumize: - await fetchPmDownload() + await fetchPmDownload(cloudItemId: cloudInfo) case .none: break } } - func fetchRdDownload(magnet: Magnet?) async { - do { - // Bypass the TTL since a download needs to be queried + func fetchRdDownload(magnet: Magnet?, existingLink: String?) async { + // If an existing link is passed in args, set it to that. Otherwise, find one from RD cloud. + let torrentLink: String? + if let existingLink { + torrentLink = existingLink + } else { + // Bypass the TTL for up to date information await fetchRdCloud(bypassTTL: true) - // If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link - let existingTorrents = realDebridCloudTorrents.filter { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" } + let existingTorrent = realDebridCloudTorrents.first { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" } + torrentLink = existingTorrent?.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] + } + do { // 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] + if let torrentLink, + let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink) { - try await checkRdUserDownloads(userTorrentLink: torrentLink) + downloadUrl = downloadLink } else if let magnet { // Add a magnet after all the cache checks fail selectedRealDebridID = try await realDebrid.addMagnet(magnet: magnet) @@ -531,8 +542,7 @@ public class DebridManager: ObservableObject { 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") + throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") } } catch { switch error { @@ -588,42 +598,81 @@ public class DebridManager: ObservableObject { } } - 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) + func checkRdUserDownloads(userTorrentLink: String) async -> String? { + do { + let existingLinks = realDebridCloudDownloads.first { $0.link == userTorrentLink } + if let existingLink = existingLinks?.download { + return existingLink + } else { + return try await realDebrid.unrestrictLink(debridDownloadLink: userTorrentLink) + } + } catch { + await sendDebridError(error, prefix: "RealDebrid download check error") - downloadUrl = downloadLink + return nil } } - func fetchAdDownload(magnet: Magnet?) async { - guard let magnet else { - toastModel?.updateToastDescription("Could not run your action because the magnet is invalid.") - print("AllDebrid error: Invalid magnet") + func fetchAdDownload(magnet: Magnet?, existingLockedLink: String?) async { + // If an existing link is passed in args, set it to that. Otherwise, find one from AD cloud. + let lockedLink: String? + if let existingLockedLink { + lockedLink = existingLockedLink + } else { + // Bypass the TTL for up to date information + await fetchAdCloud(bypassTTL: true) - return + let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" } + lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.id ?? 0]?.link } do { - let magnetID = try await allDebrid.addMagnet(magnet: magnet) - let lockedLink = try await allDebrid.fetchMagnetStatus( - magnetId: magnetID, - selectedIndex: selectedAllDebridFile?.id ?? 0 - ) - let unlockedLink = try await allDebrid.unlockLink(lockedLink: lockedLink) + if let lockedLink { + downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink) + } else if let magnet { + let magnetID = try await allDebrid.addMagnet(magnet: magnet) + let lockedLink = try await allDebrid.fetchMagnetStatus( + magnetId: magnetID, + selectedIndex: selectedAllDebridFile?.id ?? 0 + ) - downloadUrl = unlockedLink + downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink) + } else { + throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API") + } } catch { await sendDebridError(error, prefix: "AllDebrid download error", cancelString: "Download cancelled") } } + // Refreshes torrents and downloads from a RD user's account + public func fetchAdCloud(bypassTTL: Bool = false) async { + if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL { + do { + allDebridCloudMagnets = try await allDebrid.userMagnets() + realDebridCloudDownloads = try await realDebrid.userDownloads() + + // 5 minutes + allDebridCloudTTL = Date().timeIntervalSince1970 + 300 + } catch { + await sendDebridError(error, prefix: "AlLDebrid cloud fetch error") + } + } + } + + func deleteAdMagnet(magnetId: Int) async { + do { + try await allDebrid.deleteMagnet(magnetId: magnetId) + + await fetchAdCloud(bypassTTL: true) + } catch { + await sendDebridError(error, prefix: "AllDebrid delete error") + } + } + func fetchPmDownload(cloudItemId: String? = nil) async { do { - if let cloudItemId = cloudItemId { + if let cloudItemId { downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link } else if let premiumizeFile = selectedPremiumizeFile { downloadUrl = premiumizeFile.streamUrlString diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift new file mode 100644 index 0000000..2476f27 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -0,0 +1,93 @@ +// +// AllDebridCloudView.swift +// Ferrite +// +// Created by Brian Dashore on 1/5/23. +// + +import SwiftUI + +struct AllDebridCloudView: View { + @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var navModel: NavigationViewModel + + @State private var viewTask: Task? + + var body: some View { + DisclosureGroup("Magnets") { + ForEach(debridManager.allDebridCloudMagnets, id: \.id) { magnet in + Button { + if magnet.status == "Ready" && !magnet.links.isEmpty { + navModel.resultFromCloud = true + navModel.selectedTitle = magnet.filename + + var historyInfo = HistoryEntryJson( + name: magnet.filename, + source: DebridType.allDebrid.toString() + ) + + Task { + if magnet.links.count == 1 { + if let lockedLink = magnet.links[safe: 0]?.link { + await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: lockedLink) + + if !debridManager.downloadUrl.isEmpty { + historyInfo.url = debridManager.downloadUrl + PersistenceController.shared.createHistory(historyInfo) + navModel.runDebridAction(urlString: debridManager.downloadUrl) + } + } + } else { + debridManager.clearIAValues() + let magnet = Magnet(hash: magnet.hash, link: nil) + await debridManager.populateDebridIA([magnet]) + + if debridManager.selectDebridResult(magnet: magnet) { + navModel.selectedHistoryInfo = historyInfo + navModel.currentChoiceSheet = .batch + } + } + } + } + + } label: { + VStack(alignment: .leading, spacing: 10) { + Text(magnet.filename) + + HStack { + Text(magnet.status) + Spacer() + DebridLabelView(cloudLinks: magnet.links.map(\.link)) + } + .font(.caption) + } + } + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + .backport.tint(.black) + } + .onDelete { offsets in + for index in offsets { + if let magnet = debridManager.allDebridCloudMagnets[safe: index] { + Task { + await debridManager.deleteAdMagnet(magnetId: magnet.id) + } + } + } + } + } + .onAppear { + viewTask = Task { + await debridManager.fetchAdCloud() + } + } + .onDisappear { + viewTask?.cancel() + } + } +} + +struct AllDebridCloudView_Previews: PreviewProvider { + static var previews: some View { + AllDebridCloudView() + } +} diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index f8d7969..14166cc 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -14,8 +14,6 @@ struct PremiumizeCloudView: View { @State private var viewTask: Task? - @State private var searchText: String = "" - var body: some View { DisclosureGroup("Items") { ForEach(debridManager.premiumizeCloudItems, id: \.id) { item in @@ -24,14 +22,14 @@ struct PremiumizeCloudView: View { navModel.resultFromCloud = true navModel.selectedTitle = item.name - await debridManager.fetchPmDownload(cloudItemId: item.id) + await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: item.id) if !debridManager.downloadUrl.isEmpty { PersistenceController.shared.createHistory( HistoryEntryJson( name: item.name, url: debridManager.downloadUrl, - source: "Premiumize" + source: DebridType.premiumize.toString() ) ) diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index fb365e6..79f5d12 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -48,27 +48,24 @@ struct RealDebridCloudView: View { 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 + if torrentResponse.status == "downloaded" && !torrentResponse.links.isEmpty { + navModel.resultFromCloud = true + navModel.selectedTitle = torrentResponse.filename - var historyInfo = HistoryEntryJson( - name: torrentResponse.filename, - source: DebridType.realDebrid.toString() - ) + var historyInfo = HistoryEntryJson( + name: torrentResponse.filename, + source: DebridType.realDebrid.toString() + ) + Task { 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 - + if let torrentLink = torrentResponse.links[safe: 0] { + await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink) + if !debridManager.downloadUrl.isEmpty { + historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo) - navModel.currentChoiceSheet = .magnet - } catch { - debridManager.toastModel?.updateToastDescription("RealDebrid cloud fetch error: \(error)") + + navModel.runDebridAction(urlString: debridManager.downloadUrl) } } } else { diff --git a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift index 0ee250c..8ae90a8 100644 --- a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import SwiftUIX struct DebridCloudView: View { @EnvironmentObject var debridManager: DebridManager @@ -20,7 +19,9 @@ struct DebridCloudView: View { RealDebridCloudView() case .premiumize: PremiumizeCloudView() - case .allDebrid, .none: + case .allDebrid: + AllDebridCloudView() + case .none: EmptyView() } } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index cec3138..66ddb99 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -51,6 +51,10 @@ struct SearchResultButtonView: View { } case .partial: if debridManager.selectDebridResult(magnet: result.magnet) { + navModel.selectedHistoryInfo = HistoryEntryJson( + name: result.title, + source: result.source + ) navModel.currentChoiceSheet = .batch } case .none: diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 3d2e3fe..07604a7 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -72,7 +72,7 @@ struct LibraryView: View { EmptyInstructionView(title: "No History", message: "Start watching to build history") } case .debridCloud: - if debridManager.selectedDebridType == nil || debridManager.selectedDebridType == .allDebrid { + if debridManager.selectedDebridType == nil { EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") } } diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index a1cb620..ed777c3 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -161,7 +161,7 @@ struct SettingsView: View { await debridManager.handleCallback(url: callbackURL, error: error) } } - .prefersEphemeralWebBrowserSession(false) + .prefersEphemeralWebBrowserSession(true) } .navigationTitle("Settings") } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index fa1f094..00edb19 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -78,6 +78,7 @@ struct BatchChoiceView: View { if var selectedHistoryInfo = navModel.selectedHistoryInfo { selectedHistoryInfo.url = debridManager.downloadUrl + selectedHistoryInfo.subName = fileName PersistenceController.shared.createHistory(selectedHistoryInfo) }