diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 3748eb0..2159b51 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -100,6 +100,7 @@ 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; + 0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; }; 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; }; @@ -205,6 +206,7 @@ 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = ""; }; 0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0CAF9318296399190050812A /* PremiumizeCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeCloudView.swift; sourceTree = ""; }; 0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = ""; }; 0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = ""; }; 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = ""; }; @@ -314,6 +316,7 @@ isa = PBXGroup; children = ( 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */, + 0CAF9318296399190050812A /* PremiumizeCloudView.swift */, ); path = Cloud; sourceTree = ""; @@ -679,6 +682,7 @@ 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */, 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, 0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */, + 0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */, 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */, diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 8764c3d..3c2f72f 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -71,7 +71,7 @@ public class Premiumize { return data } else if response.statusCode == 401 { deleteTokens() - throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.") + throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.") } else { throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") } @@ -177,4 +177,58 @@ public class Premiumize { throw PMError.EmptyData } } + + func createTransfer(magnetLink: String) async throws { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/create")!) + request.httpMethod = "POST" + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + var bodyComponents = URLComponents() + bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnetLink)] + + request.httpBody = bodyComponents.query?.data(using: .utf8) + + try await performRequest(request: &request, requestName: #function) + } + + func userItems() async throws -> [UserItem] { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(AllItemsResponse.self, from: data) + + if rawResponse.files.isEmpty { + throw PMError.EmptyData + } + + return rawResponse.files + } + + func itemDetails(itemID: String) async throws -> ItemDetailsResponse { + var urlComponents = URLComponents(string: "\(baseApiUrl)/item/details")! + urlComponents.queryItems = [URLQueryItem(name: "id", value: itemID)] + guard let url = urlComponents.url else { + throw PMError.InvalidUrl + } + + var request = URLRequest(url: url) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(ItemDetailsResponse.self, from: data) + + return rawResponse + } + + func deleteItem(itemID: String) async throws { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/delete")!) + request.httpMethod = "POST" + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + var bodyComponents = URLComponents() + bodyComponents.queryItems = [URLQueryItem(name: "id", value: itemID)] + + request.httpBody = bodyComponents.query?.data(using: .utf8) + + try await performRequest(request: &request, requestName: #function) + } } diff --git a/Ferrite/Models/PremiumizeModels.swift b/Ferrite/Models/PremiumizeModels.swift index 4a1daca..8beaa9c 100644 --- a/Ferrite/Models/PremiumizeModels.swift +++ b/Ferrite/Models/PremiumizeModels.swift @@ -39,7 +39,7 @@ public extension Premiumize { let filesize: Int } - // MARK: - Content + // MARK: Content struct DDLData: Codable { let path: String @@ -65,4 +65,38 @@ public extension Premiumize { let name: String let streamUrlString: String } + + // MARK: - AllItemsResponse (listall endpoint) + struct AllItemsResponse: Codable { + let status: String + let files: [UserItem] + } + + // MARK: User Items + // Abridged for required parameters + struct UserItem: Codable { + let id: String + let name: String + let mimeType: String + + enum CodingKeys: String, CodingKey { + case id, name + case mimeType = "mime_type" + } + } + + // MARK: - ItemDetailsResponse + + // Abridged for required parameters + struct ItemDetailsResponse: Codable { + let id: String + let name: String + let link: String + let mimeType: String + + enum CodingKeys: String, CodingKey { + case id, name, link + case mimeType = "mime_type" + } + } } diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 020335a..9ceef73 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -73,6 +73,10 @@ public class DebridManager: ObservableObject { var selectedPremiumizeItem: Premiumize.IA? var selectedPremiumizeFile: Premiumize.IAFile? + // Premiumize cloud variables + @Published var premiumizeCloudItems: [Premiumize.UserItem] = [] + var premiumizeCloudTTL: Double = 0.0 + init() { if let rawDebridList = UserDefaults.standard.string(forKey: "Debrid.EnabledArray"), let serializedDebridList = Set(rawValue: rawDebridList) @@ -481,7 +485,7 @@ public class DebridManager: ObservableObject { case .allDebrid: await fetchAdDownload(magnetLink: magnetLink) case .premiumize: - fetchPmDownload() + await fetchPmDownload() case .none: break } @@ -544,7 +548,7 @@ public class DebridManager: ObservableObject { toastModel?.updateToastDescription("RealDebrid download error: \(error)") } - await deleteRdTorrent() + await deleteRdTorrent(torrentID: selectedRealDebridID) } showLoadingProgress = false @@ -564,16 +568,36 @@ public class DebridManager: ObservableObject { realDebridCloudTTL = Date().timeIntervalSince1970 + 300 } catch { toastModel?.updateToastDescription("RealDebrid cloud fetch error: \(error)") + print("RealDebrid cloud fetch error: \(error)") } } } - func deleteRdTorrent() async { - if let realDebridId = selectedRealDebridID { - try? await realDebrid.deleteTorrent(debridID: realDebridId) - } + func deleteRdDownload(downloadID: String) async { + do { + try await realDebrid.deleteDownload(debridID: downloadID) - selectedRealDebridID = nil + // Bypass TTL to get current RD values + await fetchRdCloud(bypassTTL: true) + } catch { + toastModel?.updateToastDescription("RealDebrid download delete error: \(error)") + print("RealDebrid download delete error: \(error)") + } + } + + func deleteRdTorrent(torrentID: String? = nil) async { + do { + if let torrentID = torrentID { + try await realDebrid.deleteTorrent(debridID: torrentID) + } else if let selectedTorrentID = selectedRealDebridID { + try await realDebrid.deleteTorrent(debridID: selectedTorrentID) + } else { + throw RealDebrid.RDError.FailedRequest(description: "No torrent ID was provided") + } + } catch { + toastModel?.updateToastDescription("RealDebrid torrent delete error: \(error)") + print("RealDebrid torrent delete error: \(error)") + } } func checkRdUserDownloads(userTorrentLink: String) async throws { @@ -615,21 +639,53 @@ public class DebridManager: ObservableObject { } } - func fetchPmDownload() { - guard let premiumizeItem = selectedPremiumizeItem else { - toastModel?.updateToastDescription("Could not run your action because the result is invalid") - print("Premiumize download error: Invalid selected Premiumize item") - - return + func fetchPmDownload(cloudItemId: String? = nil) async { + do { + if let cloudItemId = cloudItemId { + downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link + } else if let premiumizeFile = selectedPremiumizeFile { + downloadUrl = premiumizeFile.streamUrlString + } else if + let premiumizeItem = selectedPremiumizeItem, + let firstFile = premiumizeItem.files[safe: 0] + { + downloadUrl = firstFile.streamUrlString + } else { + throw Premiumize.PMError.FailedRequest(description: "There were no items or files found!") + } + } catch { + toastModel?.updateToastDescription("Premiumize download error: \(error)") + print("Premiumize download error: \(error)") } + } - if let premiumizeFile = selectedPremiumizeFile { - downloadUrl = premiumizeFile.streamUrlString - } else if let firstFile = premiumizeItem.files[safe: 0] { - downloadUrl = firstFile.streamUrlString - } else { - toastModel?.updateToastDescription("Could not run your action because the result could not be found") - print("Premiumize download error: Could not find the selected Premiumize file") + // Refreshes items and fetches from a PM user account + public func fetchPmCloud(bypassTTL: Bool = false) async { + if bypassTTL || Date().timeIntervalSince1970 > premiumizeCloudTTL { + do { + let userItems = try await premiumize.userItems() + withAnimation { + premiumizeCloudItems = userItems + } + + // 5 minutes + premiumizeCloudTTL = Date().timeIntervalSince1970 + 300 + } catch { + toastModel?.updateToastDescription("Premiumize cloud fetch error: \(error)") + print("Premiumize cloud fetch error: \(error)") + } + } + } + + public func deletePmItem(id: String) async { + do { + try await premiumize.deleteItem(itemID: id) + + // Bypass TTL to get current RD values + await fetchPmCloud(bypassTTL: true) + } catch { + toastModel?.updateToastDescription("Premiumize cloud delete error: \(error)") + print("Premiumize cloud delete error: \(error)") } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift new file mode 100644 index 0000000..f8d7969 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -0,0 +1,70 @@ +// +// PremiumizeCloudView.swift +// Ferrite +// +// Created by Brian Dashore on 1/2/23. +// + +import SwiftUI +import SwiftUIX + +struct PremiumizeCloudView: View { + @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var navModel: NavigationViewModel + + @State private var viewTask: Task? + + @State private var searchText: String = "" + + var body: some View { + DisclosureGroup("Items") { + ForEach(debridManager.premiumizeCloudItems, id: \.id) { item in + Button(item.name) { + Task { + navModel.resultFromCloud = true + navModel.selectedTitle = item.name + + await debridManager.fetchPmDownload(cloudItemId: item.id) + + if !debridManager.downloadUrl.isEmpty { + PersistenceController.shared.createHistory( + HistoryEntryJson( + name: item.name, + url: debridManager.downloadUrl, + source: "Premiumize" + ) + ) + + navModel.runDebridAction(urlString: debridManager.downloadUrl) + } + } + } + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + .backport.tint(.black) + } + .onDelete { offsets in + for index in offsets { + if let item = debridManager.premiumizeCloudItems[safe: index] { + Task { + await debridManager.deletePmItem(id: item.id) + } + } + } + } + } + .onAppear { + viewTask = Task { + await debridManager.fetchPmCloud() + } + } + .onDisappear { + viewTask?.cancel() + } + } +} + +struct PremiumizeCloudView_Previews: PreviewProvider { + static var previews: some View { + PremiumizeCloudView() + } +} diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 3397479..1dd1413 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -38,14 +38,7 @@ struct RealDebridCloudView: View { 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) - } + await debridManager.deleteRdDownload(downloadID: downloadResponse.id) } } } @@ -111,14 +104,7 @@ struct RealDebridCloudView: View { 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) - } + await debridManager.deleteRdTorrent(torrentID: torrentResponse.id) } } } diff --git a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift index 54125f0..0ee250c 100644 --- a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift @@ -6,21 +6,28 @@ // import SwiftUI +import SwiftUIX struct DebridCloudView: View { @EnvironmentObject var debridManager: DebridManager var body: some View { - List { - switch debridManager.selectedDebridType { - case .realDebrid: - RealDebridCloudView() - case .allDebrid, .premiumize, .none: - EmptyView() + NavView { + VStack { + List { + switch debridManager.selectedDebridType { + case .realDebrid: + RealDebridCloudView() + case .premiumize: + PremiumizeCloudView() + case .allDebrid, .none: + EmptyView() + } + } + .inlinedList() + .listStyle(.grouped) } } - .inlinedList() - .listStyle(.insetGrouped) } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 4824e79..3d2e3fe 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 != .realDebrid { + if debridManager.selectedDebridType == nil || debridManager.selectedDebridType == .allDebrid { EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") } }