Library: Add support for RealDebrid cloud

RealDebrid saves a user's unrestricted links and "torrents" (magnet
links in this case). Add the ability to see and queue a user's RD
library in Ferrite itself.

This required a further abstraction of the debrid manager to allow
for more types other than search results to be passed to various
functions.

Deleting an item from RD's cloud list deletes the item from RD as well.

NOTE: This does not track download progress, but it does show if a
magnet is currently being downloaded or not.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-01-02 11:29:30 -05:00
parent b0850d43d7
commit 9b7bc55a25
20 changed files with 434 additions and 146 deletions

View file

@ -14,6 +14,8 @@
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; };
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; };
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; }; 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; }; 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
@ -122,6 +124,8 @@
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; }; 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; }; 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; };
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; };
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = "<group>"; };
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
@ -306,6 +310,14 @@
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0C2886D52960C4F800D6FC16 /* Cloud */ = {
isa = PBXGroup;
children = (
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */,
);
path = Cloud;
sourceTree = "<group>";
};
0C44E2A628D4DDC6007711AE /* Classes */ = { 0C44E2A628D4DDC6007711AE /* Classes */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -482,7 +494,9 @@
0CA3B23528C265FD00616D3A /* Library */ = { 0CA3B23528C265FD00616D3A /* Library */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C2886D52960C4F800D6FC16 /* Cloud */,
0CA3B23828C2660D00616D3A /* BookmarksView.swift */, 0CA3B23828C2660D00616D3A /* BookmarksView.swift */,
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */,
0CA3B23628C2660700616D3A /* HistoryView.swift */, 0CA3B23628C2660700616D3A /* HistoryView.swift */,
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */, 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */,
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */, 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */,
@ -651,6 +665,7 @@
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */, 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */, 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */,
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */,
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
@ -722,6 +737,7 @@
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */, 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */,
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */, 0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */, 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */,
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */, 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */,
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */, 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */,
0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */, 0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */,

View file

@ -358,4 +358,11 @@ public class RealDebrid {
return rawResponse return rawResponse
} }
public func deleteDownload(debridID: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(debridID)")!)
request.httpMethod = "DELETE"
try await performRequest(request: &request, requestName: #function)
}
} }

View file

@ -112,9 +112,11 @@ struct PersistenceController {
newBookmark.magnetLink = bookmarkJson.magnetLink newBookmark.magnetLink = bookmarkJson.magnetLink
newBookmark.seeders = bookmarkJson.seeders newBookmark.seeders = bookmarkJson.seeders
newBookmark.leechers = bookmarkJson.leechers newBookmark.leechers = bookmarkJson.leechers
save(backgroundContext)
} }
func createHistory(entryJson: HistoryEntryJson, date: Double?) { func createHistory(_ entryJson: HistoryEntryJson, date: Double? = nil) {
let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date() let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate) let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate)
@ -153,6 +155,8 @@ struct PersistenceController {
newHistoryEntry.parentHistory?.dateString = historyDateString newHistoryEntry.parentHistory?.dateString = historyDateString
newHistoryEntry.parentHistory?.date = historyDate newHistoryEntry.parentHistory?.date = historyDate
save(backgroundContext)
} }
func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? { func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? {

View file

@ -4,12 +4,21 @@
// //
// Created by Brian Dashore on 8/31/22. // Created by Brian Dashore on 8/31/22.
// //
// From https://stackoverflow.com/a/59307884
// //
import Foundation import Foundation
extension String { extension String {
// From https://www.hackingwithswift.com/example-code/strings/how-to-capitalize-the-first-letter-of-a-string
func capitalizingFirstLetter() -> String {
return prefix(1).capitalized + dropFirst()
}
mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter()
}
// From https://stackoverflow.com/a/59307884
private func compare(toVersion targetVersion: String) -> ComparisonResult { private func compare(toVersion targetVersion: String) -> ComparisonResult {
let versionDelimiter = "." let versionDelimiter = "."
var result: ComparisonResult = .orderedSame var result: ComparisonResult = .orderedSame

View file

@ -26,10 +26,10 @@ struct HistoryJson: Codable {
} }
struct HistoryEntryJson: Codable { struct HistoryEntryJson: Codable {
let name: String var name: String? = nil
let subName: String? var subName: String? = nil
let url: String var url: String? = nil
let timeStamp: Double? var timeStamp: Double? = nil
let source: String? let source: String?
} }

View file

@ -36,6 +36,6 @@ public enum DebridType: Int, Codable, Hashable, CaseIterable {
// Wrapper struct for magnet links to contain both the link and hash for easy access // Wrapper struct for magnet links to contain both the link and hash for easy access
public struct Magnet: Codable, Hashable, Sendable { public struct Magnet: Codable, Hashable, Sendable {
let link: String let link: String?
let hash: String let hash: String
} }

View file

@ -150,7 +150,7 @@ public extension RealDebrid {
let bytes, selected: Int let bytes, selected: Int
} }
struct UserTorrentsResponse: Codable, Sendable { struct UserTorrentsResponse: Codable, Hashable, Sendable {
let id, filename, hash: String let id, filename, hash: String
let bytes: Int let bytes: Int
let host: String let host: String
@ -183,7 +183,7 @@ public extension RealDebrid {
// MARK: - User downloads list // MARK: - User downloads list
struct UserDownloadsResponse: Codable, Sendable { struct UserDownloadsResponse: Codable, Hashable, Sendable {
let id, filename: String let id, filename: String
let mimeType: String? let mimeType: String?
let filesize: Int let filesize: Int

View file

@ -123,7 +123,7 @@ public class BackupManager: ObservableObject {
if let storedHistories = backup.history { if let storedHistories = backup.history {
for storedHistory in storedHistories { for storedHistory in storedHistories {
for storedEntry in storedHistory.entries { for storedEntry in storedHistory.entries {
PersistenceController.shared.createHistory(entryJson: storedEntry, date: storedHistory.date) PersistenceController.shared.createHistory(storedEntry, date: storedHistory.date)
} }
} }
} }

View file

@ -50,6 +50,11 @@ public class DebridManager: ObservableObject {
var selectedRealDebridFile: RealDebrid.IAFile? var selectedRealDebridFile: RealDebrid.IAFile?
var selectedRealDebridID: String? var selectedRealDebridID: String?
// RealDebrid cloud variables
@Published var realDebridCloudTorrents: [RealDebrid.UserTorrentsResponse] = []
@Published var realDebridCloudDownloads: [RealDebrid.UserDownloadsResponse] = []
var realDebridCloudTTL: Double = 0.0
// AllDebrid auth variables // AllDebrid auth variables
@Published var allDebridAuthProcessing: Bool = false @Published var allDebridAuthProcessing: Bool = false
@ -107,6 +112,30 @@ public class DebridManager: ObservableObject {
} }
} }
// Cleans all cached IA values in the event of a full IA refresh
public func clearIAValues() {
realDebridIAValues = []
allDebridIAValues = []
premiumizeIAValues = []
}
// Clears all selected files and items
public func clearSelectedDebridItems() {
switch selectedDebridType {
case .realDebrid:
selectedRealDebridFile = nil
selectedRealDebridItem = nil
case .allDebrid:
selectedAllDebridFile = nil
selectedAllDebridItem = nil
case .premiumize:
selectedPremiumizeFile = nil
selectedPremiumizeItem = nil
case .none:
break
}
}
// Common function to populate hashes for debrid services // Common function to populate hashes for debrid services
public func populateDebridIA(_ resultMagnets: [Magnet]) async { public func populateDebridIA(_ resultMagnets: [Magnet]) async {
do { do {
@ -153,7 +182,16 @@ public class DebridManager: ObservableObject {
} }
if enabledDebrids.contains(.premiumize) { if enabledDebrids.contains(.premiumize) {
let availableMagnets = try await premiumize.divideCacheRequests(magnets: sendMagnets) // Only strip magnets that don't have an associated link for PM
let strippedResultMagnets: [Magnet] = resultMagnets.compactMap {
if let magnetLink = $0.link {
return Magnet(link: magnetLink, hash: $0.hash)
} else {
return nil
}
}
let availableMagnets = try await premiumize.divideCacheRequests(magnets: strippedResultMagnets)
// Split DDL requests into chunks of 10 // Split DDL requests into chunks of 10
for chunk in availableMagnets.chunked(into: 10) { for chunk in availableMagnets.chunked(into: 10) {
@ -174,15 +212,15 @@ public class DebridManager: ObservableObject {
} }
} }
// Common function to match search results with a provided debrid service // Common function to match a magnet hash with a provided debrid service
public func matchSearchResult(result: SearchResult?) -> IAStatus { public func matchMagnetHash(_ magnetHash: String?) -> IAStatus {
guard let result else { guard let magnetHash else {
return .none return .none
} }
switch selectedDebridType { switch selectedDebridType {
case .realDebrid: case .realDebrid:
guard let realDebridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { guard let realDebridMatch = realDebridIAValues.first(where: { magnetHash == $0.hash }) else {
return .none return .none
} }
@ -192,7 +230,7 @@ public class DebridManager: ObservableObject {
return .partial return .partial
} }
case .allDebrid: case .allDebrid:
guard let allDebridMatch = allDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { guard let allDebridMatch = allDebridIAValues.first(where: { magnetHash == $0.hash }) else {
return .none return .none
} }
@ -202,7 +240,7 @@ public class DebridManager: ObservableObject {
return .full return .full
} }
case .premiumize: case .premiumize:
guard let premiumizeMatch = premiumizeIAValues.first(where: { result.magnetHash == $0.hash }) else { guard let premiumizeMatch = premiumizeIAValues.first(where: { magnetHash == $0.hash }) else {
return .none return .none
} }
@ -216,8 +254,8 @@ public class DebridManager: ObservableObject {
} }
} }
public func selectDebridResult(result: SearchResult) -> Bool { public func selectDebridResult(magnetHash: String?) -> Bool {
guard let magnetHash = result.magnetHash else { guard let magnetHash = magnetHash else {
toastModel?.updateToastDescription("Could not find the torrent magnet hash") toastModel?.updateToastDescription("Could not find the torrent magnet hash")
return false return false
} }
@ -429,7 +467,7 @@ public class DebridManager: ObservableObject {
// MARK: - Debrid fetch UI linked functions // MARK: - Debrid fetch UI linked functions
// Common function to delegate what debrid service to fetch from // Common function to delegate what debrid service to fetch from
public func fetchDebridDownload(searchResult: SearchResult) async { public func fetchDebridDownload(magnetLink: String?) async {
defer { defer {
currentDebridTask = nil currentDebridTask = nil
showLoadingProgress = false showLoadingProgress = false
@ -437,21 +475,11 @@ public class DebridManager: ObservableObject {
showLoadingProgress = true showLoadingProgress = true
// Premiumize doesn't need a magnet link
guard searchResult.magnetLink != nil || selectedDebridType == .premiumize else {
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
print("Debrid error: Invalid magnet link")
return
}
// Force unwrap is OK for debrid types that aren't ignored since the magnet link was already checked
// Do not force unwrap for Premiumize!
switch selectedDebridType { switch selectedDebridType {
case .realDebrid: case .realDebrid:
await fetchRdDownload(magnetLink: searchResult.magnetLink!) await fetchRdDownload(magnetLink: magnetLink)
case .allDebrid: case .allDebrid:
await fetchAdDownload(magnetLink: searchResult.magnetLink!) await fetchAdDownload(magnetLink: magnetLink)
case .premiumize: case .premiumize:
fetchPmDownload() fetchPmDownload()
case .none: case .none:
@ -459,38 +487,32 @@ public class DebridManager: ObservableObject {
} }
} }
func fetchRdDownload(magnetLink: String) async { func fetchRdDownload(magnetLink: String?) async {
do { do {
var fileIds: [Int] = [] // Bypass the TTL since a download needs to be queried
await fetchRdCloud(bypassTTL: true)
if let iaFile = selectedRealDebridFile {
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
return
}
fileIds = iaBatchFromFile.files.map(\.id)
}
// If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link // If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link
let existingTorrents = try await realDebrid.userTorrents().filter { $0.hash == selectedRealDebridItem?.hash } let existingTorrents = realDebridCloudTorrents.filter { $0.hash == selectedRealDebridItem?.hash && $0.status == "downloaded" }
// If the links match from a user's downloads, no need to re-run a download // If the links match from a user's downloads, no need to re-run a download
if let existingTorrent = existingTorrents[safe: 0], if let existingTorrent = existingTorrents[safe: 0],
let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0]
{ {
let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink } try await checkRdUserDownloads(userTorrentLink: torrentLink)
if let existingLink = existingLinks[safe: 0]?.download { } else if let magnetLink = magnetLink {
downloadUrl = existingLink
} else {
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
downloadUrl = downloadLink
}
} else {
// Add a magnet after all the cache checks fail // Add a magnet after all the cache checks fail
selectedRealDebridID = try await realDebrid.addMagnet(magnetLink: magnetLink) selectedRealDebridID = try await realDebrid.addMagnet(magnetLink: magnetLink)
var fileIds: [Int] = []
if let iaFile = selectedRealDebridFile {
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
return
}
fileIds = iaBatchFromFile.files.map(\.id)
}
if let realDebridId = selectedRealDebridID { if let realDebridId = selectedRealDebridID {
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds) try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
@ -504,6 +526,9 @@ public class DebridManager: ObservableObject {
} else { } else {
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.") 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")
} }
} catch { } catch {
switch error { switch error {
@ -528,6 +553,21 @@ public class DebridManager: ObservableObject {
} }
} }
// Refreshes torrents and downloads from a RD user's account
public func fetchRdCloud(bypassTTL: Bool = false) async {
if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL {
do {
realDebridCloudTorrents = try await realDebrid.userTorrents()
realDebridCloudDownloads = try await realDebrid.userDownloads()
// 5 minutes
realDebridCloudTTL = Date().timeIntervalSince1970 + 300
} catch {
toastModel?.updateToastDescription("RealDebrid cloud fetch error: \(error)")
}
}
}
func deleteRdTorrent() async { func deleteRdTorrent() async {
if let realDebridId = selectedRealDebridID { if let realDebridId = selectedRealDebridID {
try? await realDebrid.deleteTorrent(debridID: realDebridId) try? await realDebrid.deleteTorrent(debridID: realDebridId)
@ -536,7 +576,25 @@ public class DebridManager: ObservableObject {
selectedRealDebridID = nil selectedRealDebridID = nil
} }
func fetchAdDownload(magnetLink: String) async { 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)
downloadUrl = downloadLink
}
}
func fetchAdDownload(magnetLink: String?) async {
guard let magnetLink = magnetLink else {
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
print("AllDebrid error: Invalid magnet link")
return
}
do { do {
let magnetID = try await allDebrid.addMagnet(magnetLink: magnetLink) let magnetID = try await allDebrid.addMagnet(magnetLink: magnetLink)
let lockedLink = try await allDebrid.fetchMagnetStatus( let lockedLink = try await allDebrid.fetchMagnetStatus(

View file

@ -33,6 +33,9 @@ class NavigationViewModel: ObservableObject {
@Published var isSearching: Bool = false @Published var isSearching: Bool = false
@Published var selectedSearchResult: SearchResult? @Published var selectedSearchResult: SearchResult?
@Published var selectedMagnetLink: String?
@Published var selectedHistoryInfo: HistoryEntryJson?
@Published var resultFromCloud: Bool = false
// For giving information in magnet choice sheet // For giving information in magnet choice sheet
@Published var selectedTitle: String = "" @Published var selectedTitle: String = ""
@ -124,6 +127,7 @@ class NavigationViewModel: ObservableObject {
} }
} }
/*
public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) { public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) {
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
@ -141,4 +145,5 @@ class NavigationViewModel: ObservableObject {
PersistenceController.shared.save(backgroundContext) PersistenceController.shared.save(backgroundContext)
} }
*/
} }

View file

@ -10,27 +10,36 @@ import SwiftUI
struct DebridLabelView: View { struct DebridLabelView: View {
@EnvironmentObject var debridManager: DebridManager @EnvironmentObject var debridManager: DebridManager
var result: SearchResult @State var cloudLinks: [String] = []
var magnetHash: String?
let debridAbbreviation: String
var body: some View { var body: some View {
Text(debridAbbreviation) if let selectedDebridType = debridManager.selectedDebridType {
.fontWeight(.bold) Text(selectedDebridType.toString(abbreviated: true))
.padding(2) .fontWeight(.bold)
.background { .padding(2)
Group { .background {
switch debridManager.matchSearchResult(result: result) { Group {
case .full: if cloudLinks.isEmpty {
Color.green switch debridManager.matchMagnetHash(magnetHash) {
case .partial: case .full:
Color.orange Color.green
case .none: case .partial:
Color.red Color.orange
case .none:
Color.red
}
} else if cloudLinks.count == 1 {
Color.green
} else if cloudLinks.count > 1 {
Color.orange
} else {
Color.red
}
} }
.cornerRadius(4)
.opacity(0.5)
} }
.cornerRadius(4) }
.opacity(0.5)
}
} }
} }

View file

@ -31,7 +31,7 @@ struct BookmarksView: View {
if let bookmark = bookmarks[safe: index] { if let bookmark = bookmarks[safe: index] {
PersistenceController.shared.delete(bookmark, context: backgroundContext) PersistenceController.shared.delete(bookmark, context: backgroundContext)
NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
} }
} }
} }
@ -55,8 +55,8 @@ struct BookmarksView: View {
if debridManager.enabledDebrids.count > 0 { if debridManager.enabledDebrids.count > 0 {
viewTask = Task { viewTask = Task {
let magnets = bookmarks.compactMap { let magnets = bookmarks.compactMap {
if let magnetLink = $0.magnetLink, let magnetHash = $0.magnetHash { if let magnetHash = $0.magnetHash {
return Magnet(link: magnetLink, hash: magnetHash) return Magnet(link: $0.magnetLink, hash: magnetHash)
} else { } else {
return nil return nil
} }

View file

@ -0,0 +1,143 @@
//
// RealDebridCloudView.swift
// Ferrite
//
// Created by Brian Dashore on 12/31/22.
//
import SwiftUI
struct RealDebridCloudView: View {
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var debridManager: DebridManager
@State private var viewTask: Task<Void, Never>?
var body: some View {
Group {
DisclosureGroup("Downloads") {
ForEach(debridManager.realDebridCloudDownloads, id: \.self) { downloadResponse in
Button(downloadResponse.filename) {
navModel.resultFromCloud = true
navModel.selectedTitle = downloadResponse.filename
debridManager.downloadUrl = downloadResponse.link
PersistenceController.shared.createHistory(
HistoryEntryJson(
name: downloadResponse.filename,
url: downloadResponse.link,
source: DebridType.realDebrid.toString()
)
)
navModel.runDebridAction(urlString: debridManager.downloadUrl)
}
.backport.tint(.primary)
}
.onDelete { offsets in
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)
}
}
}
}
}
}
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
var historyInfo = HistoryEntryJson(
name: torrentResponse.filename,
source: DebridType.realDebrid.toString()
)
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
PersistenceController.shared.createHistory(historyInfo)
navModel.currentChoiceSheet = .magnet
} catch {
debridManager.toastModel?.updateToastDescription("RealDebrid cloud fetch error: \(error)")
}
}
} else {
debridManager.clearIAValues()
await debridManager.populateDebridIA([Magnet(link: nil, hash: torrentResponse.hash)])
if debridManager.selectDebridResult(magnetHash: torrentResponse.hash) {
navModel.selectedHistoryInfo = historyInfo
navModel.currentChoiceSheet = .batch
}
}
}
}
} label: {
VStack(alignment: .leading, spacing: 10) {
Text(torrentResponse.filename)
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(4)
HStack {
Text(torrentResponse.status.capitalizingFirstLetter())
Spacer()
DebridLabelView(cloudLinks: torrentResponse.links)
}
.font(.caption)
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.backport.tint(.primary)
}
.onDelete { offsets in
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)
}
}
}
}
}
}
}
.onAppear {
viewTask = Task {
await debridManager.fetchRdCloud()
}
}
.onDisappear {
viewTask?.cancel()
}
}
}
struct RealDebridCloudView_Previews: PreviewProvider {
static var previews: some View {
RealDebridCloudView()
}
}

View file

@ -0,0 +1,31 @@
//
// DebridCloudView.swift
// Ferrite
//
// Created by Brian Dashore on 12/31/22.
//
import SwiftUI
struct DebridCloudView: View {
@EnvironmentObject var debridManager: DebridManager
var body: some View {
List {
switch debridManager.selectedDebridType {
case .realDebrid:
RealDebridCloudView()
case .allDebrid, .premiumize, .none:
EmptyView()
}
}
.inlinedList()
.listStyle(.insetGrouped)
}
}
struct DebridCloudView_Previews: PreviewProvider {
static var previews: some View {
DebridCloudView()
}
}

View file

@ -24,15 +24,23 @@ struct SearchResultButtonView: View {
if debridManager.currentDebridTask == nil { if debridManager.currentDebridTask == nil {
navModel.selectedSearchResult = result navModel.selectedSearchResult = result
navModel.selectedTitle = result.title ?? "" navModel.selectedTitle = result.title ?? ""
navModel.resultFromCloud = false
switch debridManager.matchSearchResult(result: result) { switch debridManager.matchMagnetHash(result.magnetHash) {
case .full: case .full:
if debridManager.selectDebridResult(result: result) { if debridManager.selectDebridResult(magnetHash: result.magnetHash) {
debridManager.currentDebridTask = Task { debridManager.currentDebridTask = Task {
await debridManager.fetchDebridDownload(searchResult: result) await debridManager.fetchDebridDownload(magnetLink: result.magnetLink)
if !debridManager.downloadUrl.isEmpty { if !debridManager.downloadUrl.isEmpty {
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.downloadUrl) PersistenceController.shared.createHistory(
HistoryEntryJson(
name: result.title,
url: debridManager.downloadUrl,
source: result.source
)
)
navModel.runDebridAction(urlString: debridManager.downloadUrl) navModel.runDebridAction(urlString: debridManager.downloadUrl)
if navModel.currentChoiceSheet != .magnet { if navModel.currentChoiceSheet != .magnet {
@ -42,11 +50,18 @@ struct SearchResultButtonView: View {
} }
} }
case .partial: case .partial:
if debridManager.selectDebridResult(result: result) { if debridManager.selectDebridResult(magnetHash: result.magnetHash) {
navModel.currentChoiceSheet = .batch navModel.currentChoiceSheet = .batch
} }
case .none: case .none:
navModel.addToHistory(name: result.title, source: result.source, url: result.magnetLink) PersistenceController.shared.createHistory(
HistoryEntryJson(
name: result.title,
url: result.magnetLink,
source: result.source
)
)
navModel.runMagnetAction(magnetString: result.magnetLink) navModel.runMagnetAction(magnetString: result.magnetLink)
} }
} }

View file

@ -30,17 +30,7 @@ struct SearchResultInfoView: View {
Text(size) Text(size)
} }
if debridManager.selectedDebridType == .realDebrid { DebridLabelView(magnetHash: result.magnetHash)
DebridLabelView(result: result, debridAbbreviation: "RD")
}
if debridManager.selectedDebridType == .allDebrid {
DebridLabelView(result: result, debridAbbreviation: "AD")
}
if debridManager.selectedDebridType == .premiumize {
DebridLabelView(result: result, debridAbbreviation: "PM")
}
} }
.font(.caption) .font(.caption)
} }

View file

@ -87,12 +87,12 @@ struct ContentView: View {
await scrapingModel.scanSources(sources: sources) await scrapingModel.scanSources(sources: sources)
if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty {
debridManager.realDebridIAValues = [] debridManager.clearIAValues()
debridManager.allDebridIAValues = []
// Remove magnets that don't have a hash
let magnets = scrapingModel.searchResults.compactMap { let magnets = scrapingModel.searchResults.compactMap {
if let magnetLink = $0.magnetLink, let magnetHash = $0.magnetHash { if let magnetHash = $0.magnetHash {
return Magnet(link: magnetLink, hash: magnetHash) return Magnet(link: $0.magnetLink, hash: magnetHash)
} else { } else {
return nil return nil
} }

View file

@ -11,9 +11,11 @@ struct LibraryView: View {
enum LibraryPickerSegment { enum LibraryPickerSegment {
case bookmarks case bookmarks
case history case history
case debridCloud
} }
@EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var debridManager: DebridManager
@FetchRequest( @FetchRequest(
entity: Bookmark.entity(), entity: Bookmark.entity(),
@ -40,6 +42,10 @@ struct LibraryView: View {
Picker("Segments", selection: $selectedSegment) { Picker("Segments", selection: $selectedSegment) {
Text("Bookmarks").tag(LibraryPickerSegment.bookmarks) Text("Bookmarks").tag(LibraryPickerSegment.bookmarks)
Text("History").tag(LibraryPickerSegment.history) Text("History").tag(LibraryPickerSegment.history)
if !debridManager.enabledDebrids.isEmpty {
Text("Cloud").tag(LibraryPickerSegment.debridCloud)
}
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.padding() .padding()
@ -49,6 +55,8 @@ struct LibraryView: View {
BookmarksView(bookmarks: bookmarks) BookmarksView(bookmarks: bookmarks)
case .history: case .history:
HistoryView(history: history) HistoryView(history: history)
case .debridCloud:
DebridCloudView()
} }
Spacer() Spacer()
@ -63,6 +71,10 @@ struct LibraryView: View {
if history.isEmpty { if history.isEmpty {
EmptyInstructionView(title: "No History", message: "Start watching to build history") EmptyInstructionView(title: "No History", message: "Start watching to build history")
} }
case .debridCloud:
if debridManager.selectedDebridType != .realDebrid {
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
}
} }
} }
.navigationTitle("Library") .navigationTitle("Library")
@ -72,7 +84,7 @@ struct LibraryView: View {
EditButton() EditButton()
switch selectedSegment { switch selectedSegment {
case .bookmarks: case .bookmarks, .debridCloud:
DebridChoiceView() DebridChoiceView()
case .history: case .history:
HistoryActionsView() HistoryActionsView()

View file

@ -58,7 +58,8 @@ struct BatchChoiceView: View {
Task { Task {
try? await Task.sleep(seconds: 1) try? await Task.sleep(seconds: 1)
debridManager.selectedRealDebridItem = nil
debridManager.clearSelectedDebridItems()
} }
} }
} }
@ -68,36 +69,22 @@ struct BatchChoiceView: View {
// Common function to communicate betwen VMs and queue/display a download // Common function to communicate betwen VMs and queue/display a download
func queueCommonDownload(fileName: String) { func queueCommonDownload(fileName: String) {
if let searchResult = navModel.selectedSearchResult { debridManager.currentDebridTask = Task {
debridManager.currentDebridTask = Task { await debridManager.fetchDebridDownload(magnetLink: navModel.resultFromCloud ? nil : navModel.selectedMagnetLink)
await debridManager.fetchDebridDownload(searchResult: searchResult)
if !debridManager.downloadUrl.isEmpty { if !debridManager.downloadUrl.isEmpty {
try? await Task.sleep(seconds: 1) try? await Task.sleep(seconds: 1)
navModel.selectedBatchTitle = fileName navModel.selectedBatchTitle = fileName
navModel.addToHistory(
name: searchResult.title, if var selectedHistoryInfo = navModel.selectedHistoryInfo {
source: searchResult.source, selectedHistoryInfo.url = debridManager.downloadUrl
url: debridManager.downloadUrl, PersistenceController.shared.createHistory(selectedHistoryInfo)
subName: fileName
)
navModel.runDebridAction(urlString: debridManager.downloadUrl)
} }
switch debridManager.selectedDebridType { navModel.runDebridAction(urlString: debridManager.downloadUrl)
case .realDebrid:
debridManager.selectedRealDebridFile = nil
debridManager.selectedRealDebridItem = nil
case .allDebrid:
debridManager.selectedAllDebridFile = nil
debridManager.selectedAllDebridItem = nil
case .premiumize:
debridManager.selectedPremiumizeFile = nil
debridManager.selectedPremiumizeItem = nil
case .none:
break
}
} }
debridManager.clearSelectedDebridItems()
} }
navModel.currentChoiceSheet = nil navModel.currentChoiceSheet = nil

View file

@ -71,30 +71,31 @@ struct MagnetChoiceView: View {
} }
} }
Section(header: "Magnet options") { if !navModel.resultFromCloud {
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") { Section(header: "Magnet options") {
UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
showMagnetCopyAlert.toggle() UIPasteboard.general.string = navModel.selectedMagnetLink
} showMagnetCopyAlert.toggle()
.backport.alert(
isPresented: $showMagnetCopyAlert,
title: "Copied",
message: "Magnet link copied successfully",
buttons: [AlertButton("OK")]
)
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
if let result = navModel.selectedSearchResult,
let magnetLink = result.magnetLink,
let url = URL(string: magnetLink)
{
navModel.activityItems = [url]
navModel.showLocalActivitySheet.toggle()
} }
} .backport.alert(
isPresented: $showMagnetCopyAlert,
title: "Copied",
message: "Magnet link copied successfully",
buttons: [AlertButton("OK")]
)
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
navModel.runMagnetAction(magnetString: navModel.selectedSearchResult?.magnetLink, .webtor) if let magnetLink = navModel.selectedMagnetLink,
let url = URL(string: magnetLink)
{
navModel.activityItems = [url]
navModel.showLocalActivitySheet.toggle()
}
}
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
navModel.runMagnetAction(magnetString: navModel.selectedMagnetLink, .webtor)
}
} }
} }
} }
@ -111,6 +112,7 @@ struct MagnetChoiceView: View {
debridManager.downloadUrl = "" debridManager.downloadUrl = ""
navModel.selectedTitle = "" navModel.selectedTitle = ""
navModel.selectedBatchTitle = "" navModel.selectedBatchTitle = ""
navModel.resultFromCloud = false
} }
.navigationTitle("Link actions") .navigationTitle("Link actions")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)