RealDebrid: Add batch torrent support
Batch torrents are torrents that have multiple files bundled within one torrent file. RealDebrid does support these, but it is difficult to get them to work. The main flow requires setting a specific combination in RealDebrid to allow for link generation. However, this is not intuitive to users and is bad API design on RealDebrid's part. Ferrite's implementation presents users with all the possible files from batches (duplicates deleted) and selects the user-chosen file to download. That way, only the user chosen file is presented to play on an external video player. This still needs work for optimization purposes, but this commit does produce a working build. Signed-off-by: kingbri <bdashore3@gmail.com>
This commit is contained in:
parent
47f713744b
commit
e9670ea118
11 changed files with 265 additions and 50 deletions
|
|
@ -32,6 +32,8 @@
|
||||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; };
|
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; };
|
||||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; };
|
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; };
|
||||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||||
|
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||||
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||||
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; };
|
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
|
@ -43,7 +45,7 @@
|
||||||
0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = "<group>"; };
|
0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = "<group>"; };
|
||||||
0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = "<group>"; };
|
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = "<group>"; };
|
||||||
0CA148C4288903F000DE2211 /* RealDebridModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
|
0CA148C4288903F000DE2211 /* RealDebridModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
|
||||||
0CA148C6288903F000DE2211 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
0CA148C6288903F000DE2211 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
0CA148C7288903F000DE2211 /* FerriteApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FerriteApp.swift; sourceTree = "<group>"; };
|
0CA148C7288903F000DE2211 /* FerriteApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FerriteApp.swift; sourceTree = "<group>"; };
|
||||||
0CA148C9288903F000DE2211 /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
|
0CA148C9288903F000DE2211 /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -58,6 +60,8 @@
|
||||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
0CA148D3288903F000DE2211 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||||
0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||||
|
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||||
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
|
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
|
@ -128,6 +132,7 @@
|
||||||
0CA148BB288903F000DE2211 /* SettingsView.swift */,
|
0CA148BB288903F000DE2211 /* SettingsView.swift */,
|
||||||
0CA148BE288903F000DE2211 /* CardView.swift */,
|
0CA148BE288903F000DE2211 /* CardView.swift */,
|
||||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
||||||
|
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
|
|
@ -139,6 +144,7 @@
|
||||||
0CA148CD288903F000DE2211 /* DebridManager.swift */,
|
0CA148CD288903F000DE2211 /* DebridManager.swift */,
|
||||||
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */,
|
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */,
|
||||||
0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
|
0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
|
||||||
|
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -261,6 +267,7 @@
|
||||||
files = (
|
files = (
|
||||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||||
|
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||||
0CA148E4288903F000DE2211 /* Keychain.swift in Sources */,
|
0CA148E4288903F000DE2211 /* Keychain.swift in Sources */,
|
||||||
|
|
@ -272,6 +279,7 @@
|
||||||
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
||||||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||||
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
|
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
|
||||||
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||||
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,10 @@ public struct TokenResponse: Codable {
|
||||||
// MARK: - instantAvailability endpoint
|
// MARK: - instantAvailability endpoint
|
||||||
|
|
||||||
// Thanks Skitty!
|
// Thanks Skitty!
|
||||||
struct InstantAvailabilityResponse: Codable {
|
public struct InstantAvailabilityResponse: Codable {
|
||||||
var data: InstantAvailabilityData?
|
var data: InstantAvailabilityData?
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
|
|
||||||
if let data = try? container.decode(InstantAvailabilityData.self) {
|
if let data = try? container.decode(InstantAvailabilityData.self) {
|
||||||
|
|
@ -72,6 +72,34 @@ struct InstantAvailabilityInfo: Codable {
|
||||||
var filesize: Int
|
var filesize: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Instant Availability client side structures
|
||||||
|
public struct RealDebridIA: Codable, Hashable {
|
||||||
|
let hash: String
|
||||||
|
var files: [RealDebridIAFile] = []
|
||||||
|
var batches: [RealDebridIABatch] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct RealDebridIABatch: Codable, Hashable {
|
||||||
|
let files: [RealDebridIABatchFile]
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct RealDebridIABatchFile: Codable, Hashable {
|
||||||
|
let id: Int
|
||||||
|
let fileName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct RealDebridIAFile: Codable, Hashable {
|
||||||
|
let name: String
|
||||||
|
let batchIndex: Int
|
||||||
|
let batchFileIndex: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RealDebridIAStatus: Codable, Hashable {
|
||||||
|
case full
|
||||||
|
case partial
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - addMagnet endpoint
|
// MARK: - addMagnet endpoint
|
||||||
public struct AddMagnetResponse: Codable {
|
public struct AddMagnetResponse: Codable {
|
||||||
let id: String
|
let id: String
|
||||||
|
|
|
||||||
|
|
@ -218,8 +218,8 @@ public class RealDebrid: ObservableObject {
|
||||||
|
|
||||||
// Checks if the magnet is streamable on RD
|
// Checks if the magnet is streamable on RD
|
||||||
// Currently does not work for batch links
|
// Currently does not work for batch links
|
||||||
public func instantAvailability(magnetHashes: [String]) async throws -> [String] {
|
public func instantAvailability(magnetHashes: [String]) async throws -> [RealDebridIA] {
|
||||||
var availableHashes: [String] = []
|
var availableHashes: [RealDebridIA] = []
|
||||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!)
|
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!)
|
||||||
|
|
||||||
let data = try await performRequest(request: &request, requestName: #function)
|
let data = try await performRequest(request: &request, requestName: #function)
|
||||||
|
|
@ -232,9 +232,47 @@ public class RealDebrid: ObservableObject {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not include if a hash is a batch
|
if data.rd.isEmpty {
|
||||||
if !(data.rd.count > 1), !(data.rd[safe: 0]?.keys.count ?? 0 > 1) {
|
continue
|
||||||
availableHashes.append(hash)
|
}
|
||||||
|
|
||||||
|
// Is this a batch
|
||||||
|
if data.rd.count > 1 || data.rd[0].count > 1 {
|
||||||
|
// Batch array
|
||||||
|
let batches = data.rd.map { fileDict in
|
||||||
|
let batchFiles: [RealDebridIABatchFile] = fileDict.map { (key, value) in
|
||||||
|
// Force unwrapped ID. Is safe because ID is guaranteed on a successful response
|
||||||
|
return RealDebridIABatchFile(id: Int(key)!, fileName: value.filename)
|
||||||
|
}.sorted(by: { $0.id < $1.id })
|
||||||
|
|
||||||
|
return RealDebridIABatch(files: batchFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RD files array
|
||||||
|
// Possibly sort this in the future, but not sure how at the moment
|
||||||
|
var files: [RealDebridIAFile] = []
|
||||||
|
|
||||||
|
for index in batches.indices {
|
||||||
|
let batchFiles = batches[index].files
|
||||||
|
|
||||||
|
for batchFileIndex in batchFiles.indices {
|
||||||
|
let batchFile = batchFiles[batchFileIndex]
|
||||||
|
|
||||||
|
if !files.contains(where: { $0.name == batchFile.fileName }) {
|
||||||
|
files.append(
|
||||||
|
RealDebridIAFile(
|
||||||
|
name: batchFile.fileName,
|
||||||
|
batchIndex: index,
|
||||||
|
batchFileIndex: batchFileIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableHashes.append(RealDebridIA(hash: hash, files: files, batches: batches))
|
||||||
|
} else {
|
||||||
|
availableHashes.append(RealDebridIA(hash: hash))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,13 +297,19 @@ public class RealDebrid: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queues the magnet link for downloading
|
// Queues the magnet link for downloading
|
||||||
public func selectFiles(debridID: String) async throws {
|
public func selectFiles(debridID: String, fileIds: [Int]) async throws {
|
||||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/selectFiles/\(debridID)")!)
|
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/selectFiles/\(debridID)")!)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
var bodyComponents = URLComponents()
|
var bodyComponents = URLComponents()
|
||||||
bodyComponents.queryItems = [URLQueryItem(name: "files", value: "all")]
|
|
||||||
|
if fileIds.isEmpty {
|
||||||
|
bodyComponents.queryItems = [URLQueryItem(name: "files", value: "all")]
|
||||||
|
} else {
|
||||||
|
let joinedIds = fileIds.map(String.init).joined(separator: ",")
|
||||||
|
bodyComponents.queryItems = [URLQueryItem(name: "files", value: joinedIds)]
|
||||||
|
}
|
||||||
|
|
||||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||||
|
|
||||||
|
|
@ -273,13 +317,14 @@ public class RealDebrid: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the info of a torrent
|
// Fetches the info of a torrent
|
||||||
public func torrentInfo(debridID: String) async throws -> String {
|
public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String {
|
||||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
|
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
|
||||||
|
|
||||||
let data = try await performRequest(request: &request, requestName: #function)
|
let data = try await performRequest(request: &request, requestName: #function)
|
||||||
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
|
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
|
||||||
|
|
||||||
if let torrentLink = rawResponse.links[safe: 0] {
|
// Error out if no index is provided
|
||||||
|
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1] {
|
||||||
return torrentLink
|
return torrentLink
|
||||||
} else {
|
} else {
|
||||||
throw RealDebridError.EmptyData
|
throw RealDebridError.EmptyData
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ struct FerriteApp: App {
|
||||||
@StateObject var scrapingModel: ScrapingViewModel = .init()
|
@StateObject var scrapingModel: ScrapingViewModel = .init()
|
||||||
@StateObject var toastModel: ToastViewModel = .init()
|
@StateObject var toastModel: ToastViewModel = .init()
|
||||||
@StateObject var debridManager: DebridManager = .init()
|
@StateObject var debridManager: DebridManager = .init()
|
||||||
|
@StateObject var navigationModel: NavigationViewModel = .init()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
|
|
@ -23,6 +24,7 @@ struct FerriteApp: App {
|
||||||
.environmentObject(debridManager)
|
.environmentObject(debridManager)
|
||||||
.environmentObject(scrapingModel)
|
.environmentObject(scrapingModel)
|
||||||
.environmentObject(toastModel)
|
.environmentObject(toastModel)
|
||||||
|
.environmentObject(navigationModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ public class DebridManager: ObservableObject {
|
||||||
|
|
||||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||||
|
|
||||||
@Published var realDebridHashes: [String] = []
|
@Published var realDebridHashes: [RealDebridIA] = []
|
||||||
@Published var realDebridAuthUrl: String = ""
|
@Published var realDebridAuthUrl: String = ""
|
||||||
@Published var realDebridDownloadUrl: String = ""
|
@Published var realDebridDownloadUrl: String = ""
|
||||||
|
@Published var selectedRealDebridItem: RealDebridIA?
|
||||||
|
@Published var selectedRealDebridFile: RealDebridIAFile?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
realDebrid.parentManager = self
|
realDebrid.parentManager = self
|
||||||
|
|
@ -50,6 +52,38 @@ public class DebridManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func matchSearchResult(result: SearchResult?) -> RealDebridIAStatus {
|
||||||
|
guard let result = result else {
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let debridMatch = realDebridHashes.first(where: { result.magnetHash == $0.hash }) else {
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
|
if debridMatch.batches.isEmpty {
|
||||||
|
return .full
|
||||||
|
} else {
|
||||||
|
return .partial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public func setSelectedRdResult(result: SearchResult) -> Bool {
|
||||||
|
guard let magnetHash = result.magnetHash else {
|
||||||
|
toastModel?.toastDescription = "Could not find the torrent magnet hash"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let realDebridItem = realDebridHashes.first(where: { magnetHash == $0.hash }) {
|
||||||
|
selectedRealDebridItem = realDebridItem
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
toastModel?.toastDescription = "Could not find the associated RealDebrid entry for magnet hash \(magnetHash)"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func authenticateRd() async {
|
public func authenticateRd() async {
|
||||||
do {
|
do {
|
||||||
let url = try await realDebrid.getVerificationInfo()
|
let url = try await realDebrid.getVerificationInfo()
|
||||||
|
|
@ -67,12 +101,23 @@ public class DebridManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func fetchRdDownload(searchResult: SearchResult) async {
|
public func fetchRdDownload(searchResult: SearchResult, iaFile: RealDebridIAFile? = nil) async {
|
||||||
do {
|
do {
|
||||||
let realDebridId = try await realDebrid.addMagnet(magnetLink: searchResult.magnetLink)
|
let realDebridId = try await realDebrid.addMagnet(magnetLink: searchResult.magnetLink)
|
||||||
try await realDebrid.selectFiles(debridID: realDebridId)
|
|
||||||
|
|
||||||
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId)
|
var fileIds: [Int] = []
|
||||||
|
|
||||||
|
if let iaFile = iaFile {
|
||||||
|
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileIds = iaBatchFromFile.files.map({ $0.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
|
||||||
|
|
||||||
|
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: iaFile == nil ? 0 : iaFile?.batchFileIndex)
|
||||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
|
|
||||||
21
Ferrite/Models/NavigationViewModel.swift
Normal file
21
Ferrite/Models/NavigationViewModel.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
//
|
||||||
|
// NavigationViewModel.swift
|
||||||
|
// Ferrite
|
||||||
|
//
|
||||||
|
// Created by Brian Dashore on 7/24/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class NavigationViewModel: ObservableObject {
|
||||||
|
enum ChoiceSheetType: Identifiable {
|
||||||
|
var id: Int {
|
||||||
|
hashValue
|
||||||
|
}
|
||||||
|
|
||||||
|
case magnet
|
||||||
|
case batch
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var currentChoiceSheet: ChoiceSheetType?
|
||||||
|
}
|
||||||
|
|
@ -54,9 +54,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
@Published var searchResults: [SearchResult] = []
|
@Published var searchResults: [SearchResult] = []
|
||||||
@Published var debridHashes: [String] = []
|
@Published var debridHashes: [String] = []
|
||||||
@Published var searchText: String = ""
|
@Published var searchText: String = ""
|
||||||
|
@Published var selectedSearchResult: SearchResult?
|
||||||
@Published var realDebridAuthUrl: String = ""
|
|
||||||
@Published var showWebView: Bool = false
|
|
||||||
|
|
||||||
// Fetches the HTML body for the source website
|
// Fetches the HTML body for the source website
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
|
||||||
61
Ferrite/Views/BatchChoiceView.swift
Normal file
61
Ferrite/Views/BatchChoiceView.swift
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
//
|
||||||
|
// BatchChoiceView.swift
|
||||||
|
// Ferrite
|
||||||
|
//
|
||||||
|
// Created by Brian Dashore on 7/24/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct BatchChoiceView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
|
@EnvironmentObject var navigationModel: NavigationViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavView {
|
||||||
|
List {
|
||||||
|
// To present this sheet, an RD item had to be set, this force unwrap is therefore safe
|
||||||
|
ForEach(debridManager.selectedRealDebridItem!.files, id: \.self) { file in
|
||||||
|
Button(file.name) {
|
||||||
|
debridManager.selectedRealDebridFile = file
|
||||||
|
|
||||||
|
if let searchResult = scrapingModel.selectedSearchResult {
|
||||||
|
Task {
|
||||||
|
await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file)
|
||||||
|
|
||||||
|
// The download may complete before this sheet dismisses
|
||||||
|
try? await Task.sleep(seconds: 1)
|
||||||
|
navigationModel.currentChoiceSheet = .magnet
|
||||||
|
|
||||||
|
debridManager.selectedRealDebridFile = nil
|
||||||
|
debridManager.selectedRealDebridItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Select a file")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Done") {
|
||||||
|
debridManager.selectedRealDebridItem = nil
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BatchChoiceView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
BatchChoiceView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ struct ContentView: View {
|
||||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
|
|
||||||
|
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
@ -25,7 +27,10 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
await scrapingModel.scrapeWebsite(source: source, html: html)
|
await scrapingModel.scrapeWebsite(source: source, html: html)
|
||||||
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
|
||||||
|
if realDebridEnabled {
|
||||||
|
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,18 @@ import ActivityView
|
||||||
struct MagnetChoiceView: View {
|
struct MagnetChoiceView: View {
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
|
|
||||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||||
|
|
||||||
@Binding var selectedResult: SearchResult?
|
|
||||||
|
|
||||||
@State private var showActivityView = false
|
@State private var showActivityView = false
|
||||||
@State private var activityItem: ActivityItem?
|
@State private var activityItem: ActivityItem?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
Form {
|
Form {
|
||||||
if realDebridEnabled, debridManager.realDebridHashes.contains(selectedResult?.magnetHash ?? "") {
|
if realDebridEnabled, debridManager.matchSearchResult(result: scrapingModel.selectedSearchResult) != .none {
|
||||||
Section("Real Debrid options") {
|
Section("Real Debrid options") {
|
||||||
Button("Play on Outplayer") {
|
Button("Play on Outplayer") {
|
||||||
guard let downloadUrl = URL(string: "outplayer://\(debridManager.realDebridDownloadUrl)") else {
|
guard let downloadUrl = URL(string: "outplayer://\(debridManager.realDebridDownloadUrl)") else {
|
||||||
|
|
@ -66,11 +65,11 @@ struct MagnetChoiceView: View {
|
||||||
|
|
||||||
Section("Magnet options") {
|
Section("Magnet options") {
|
||||||
Button("Copy magnet") {
|
Button("Copy magnet") {
|
||||||
UIPasteboard.general.string = selectedResult?.magnetLink
|
UIPasteboard.general.string = scrapingModel.selectedSearchResult?.magnetLink
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Share magnet") {
|
Button("Share magnet") {
|
||||||
if let result = selectedResult, let url = URL(string: result.magnetLink) {
|
if let result = scrapingModel.selectedSearchResult, let url = URL(string: result.magnetLink) {
|
||||||
activityItem = ActivityItem(items: url)
|
activityItem = ActivityItem(items: url)
|
||||||
showActivityView.toggle()
|
showActivityView.toggle()
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +82,8 @@ struct MagnetChoiceView: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Done") {
|
Button("Done") {
|
||||||
|
debridManager.realDebridDownloadUrl = ""
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,16 +94,6 @@ struct MagnetChoiceView: View {
|
||||||
|
|
||||||
struct MagnetChoiceView_Previews: PreviewProvider {
|
struct MagnetChoiceView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
MagnetChoiceView(
|
MagnetChoiceView()
|
||||||
selectedResult:
|
|
||||||
.constant(
|
|
||||||
SearchResult(
|
|
||||||
title: "",
|
|
||||||
source: "",
|
|
||||||
size: "",
|
|
||||||
magnetLink: "",
|
|
||||||
magnetHash: nil)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,36 +13,42 @@ struct SearchResultsView: View {
|
||||||
|
|
||||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
@EnvironmentObject var debridManager: DebridManager
|
||||||
|
@EnvironmentObject var navigationModel: NavigationViewModel
|
||||||
|
|
||||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||||
|
|
||||||
@State var selectedResult: SearchResult?
|
|
||||||
|
|
||||||
@State private var showExternalSheet = false
|
|
||||||
@State private var resultUsesRd = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Button {
|
Button {
|
||||||
selectedResult = result
|
scrapingModel.selectedSearchResult = result
|
||||||
|
|
||||||
if debridManager.realDebridHashes.contains(result.magnetHash ?? ""), realDebridEnabled {
|
switch debridManager.matchSearchResult(result: result) {
|
||||||
|
case .full:
|
||||||
Task {
|
Task {
|
||||||
await debridManager.fetchRdDownload(searchResult: result)
|
await debridManager.fetchRdDownload(searchResult: result)
|
||||||
showExternalSheet.toggle()
|
navigationModel.currentChoiceSheet = .magnet
|
||||||
}
|
}
|
||||||
} else {
|
case .partial:
|
||||||
showExternalSheet.toggle()
|
if debridManager.setSelectedRdResult(result: result) {
|
||||||
|
navigationModel.currentChoiceSheet = .batch
|
||||||
|
}
|
||||||
|
case .none:
|
||||||
|
navigationModel.currentChoiceSheet = .magnet
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text(result.title)
|
Text(result.title)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showExternalSheet) {
|
.sheet(item: $navigationModel.currentChoiceSheet) { item in
|
||||||
MagnetChoiceView(selectedResult: $selectedResult)
|
switch item {
|
||||||
|
case .magnet:
|
||||||
|
MagnetChoiceView()
|
||||||
|
case .batch:
|
||||||
|
BatchChoiceView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.tint(colorScheme == .light ? .black : .white)
|
.tint(colorScheme == .light ? .black : .white)
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 5)
|
||||||
|
|
@ -59,11 +65,16 @@ struct SearchResultsView: View {
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.padding(2)
|
.padding(2)
|
||||||
.background {
|
.background {
|
||||||
if debridManager.realDebridHashes.contains(result.magnetHash ?? "") {
|
switch debridManager.matchSearchResult(result: result) {
|
||||||
|
case .full:
|
||||||
Color.green
|
Color.green
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
.opacity(0.5)
|
.opacity(0.5)
|
||||||
} else {
|
case .partial:
|
||||||
|
Color.orange
|
||||||
|
.cornerRadius(4)
|
||||||
|
.opacity(0.5)
|
||||||
|
case .none:
|
||||||
Color.red
|
Color.red
|
||||||
.cornerRadius(4)
|
.cornerRadius(4)
|
||||||
.opacity(0.5)
|
.opacity(0.5)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue