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:
kingbri 2022-07-24 14:50:29 -04:00
parent 47f713744b
commit e9670ea118
11 changed files with 265 additions and 50 deletions

View file

@ -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 */,

View file

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

View file

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

View file

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

View file

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

View 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?
}

View file

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

View 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()
}
}

View file

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

View file

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

View file

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