Library: Add support for Premiumize cloud

Add the ability to view a user's Premiumize files in Ferrite. Files
can be deleted from a user's account directly in Ferrite's list.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-01-03 14:22:20 -05:00
parent 9b7bc55a25
commit 9f54397b77
8 changed files with 258 additions and 47 deletions

View file

@ -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 = "<group>"; };
0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
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 = "<group>"; };
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
@ -314,6 +316,7 @@
isa = PBXGroup;
children = (
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */,
0CAF9318296399190050812A /* PremiumizeCloudView.swift */,
);
path = Cloud;
sourceTree = "<group>";
@ -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 */,

View file

@ -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)
}
}

View file

@ -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"
}
}
}

View file

@ -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<DebridType>(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)")
}
}
}

View file

@ -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<Void, Never>?
@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()
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}