From efecfa3236a928fff458f9f958b6940da1313f68 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 4 Aug 2022 21:23:47 -0400 Subject: [PATCH] Sources: Add RSS, descriptions, and settings RSS parsing has been added as a method to parse source since they're easier on the website's end to parse. Source settings have been added. The only current setting is the fetch mode which selects which parser/scraper to use. By default, if an RSS parser is found, it's selected. A source now has info shown regarding versioning and authorship. A source list's repository name and author string are now required. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 20 ++ .../Classes/Source+CoreDataProperties.swift | 30 +- ...ourceComplexQuery+CoreDataProperties.swift | 1 + .../SourceHtmlParser+CoreDataProperties.swift | 50 ++- .../SourceList+CoreDataProperties.swift | 4 +- .../SourceRssParser+CoreDataClass.swift | 15 + .../SourceRssParser+CoreDataProperties.swift | 56 ++++ .../SourceSeedLeech+CoreDataProperties.swift | 1 + .../Classes/SourceTracker+CoreDataClass.swift | 15 + .../SourceTracker+CoreDataProperties.swift | 27 ++ .../FerriteDB.xcdatamodel/contents | 56 +++- Ferrite/Models/SourceModels.swift | 45 ++- Ferrite/ViewModels/NavigationViewModel.swift | 5 + Ferrite/ViewModels/ScrapingViewModel.swift | 286 ++++++++++++++++-- Ferrite/ViewModels/SourceManager.swift | 220 ++++++++++---- Ferrite/Views/ContentView.swift | 5 +- .../SettingsViews/SettingsSourceUrlView.swift | 2 +- Ferrite/Views/SourceListView.swift | 47 ++- Ferrite/Views/SourceSettingsView.swift | 56 ++++ 19 files changed, 787 insertions(+), 154 deletions(-) create mode 100644 Ferrite/DataManagement/Classes/SourceRssParser+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/Classes/SourceTracker+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/SourceTracker+CoreDataProperties.swift create mode 100644 Ferrite/Views/SourceSettingsView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index bab8354..2398548 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; + 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; }; + 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */; }; + 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; }; 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; }; 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; }; 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */; }; @@ -55,6 +58,8 @@ 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; + 0CF501F2289AE06A0099C785 /* SourceTracker+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF501F0289AE06A0099C785 /* SourceTracker+CoreDataClass.swift */; }; + 0CF501F3289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF501F1289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift */; }; 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; }; /* End PBXBuildFile section */ @@ -67,6 +72,9 @@ 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = ""; }; 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; + 0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = ""; }; + 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = ""; }; + 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = ""; }; 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = ""; }; 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = ""; }; @@ -103,6 +111,8 @@ 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; + 0CF501F0289AE06A0099C785 /* SourceTracker+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceTracker+CoreDataClass.swift"; sourceTree = ""; }; + 0CF501F1289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceTracker+CoreDataProperties.swift"; sourceTree = ""; }; 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -125,6 +135,10 @@ 0C0D50DE288DF72D0035ECC8 /* Classes */ = { isa = PBXGroup; children = ( + 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */, + 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */, + 0CF501F0289AE06A0099C785 /* SourceTracker+CoreDataClass.swift */, + 0CF501F1289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift */, 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */, 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */, 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */, @@ -218,6 +232,7 @@ 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */, 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, 0C0D50E6288DFF850035ECC8 /* SourceListView.swift */, + 0C733286289C4C820058D1FE /* SourceSettingsView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */, ); path = Views; @@ -365,15 +380,18 @@ 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */, + 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, + 0CF501F2289AE06A0099C785 /* SourceTracker+CoreDataClass.swift in Sources */, 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */, 0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, + 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, @@ -388,7 +406,9 @@ 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */, 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */, + 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */, 0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */, + 0CF501F3289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift in Sources */, 0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, diff --git a/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift index 1213858..4b1b7ce 100644 --- a/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift @@ -2,23 +2,31 @@ // Source+CoreDataProperties.swift // Ferrite // -// Created by Brian Dashore on 7/30/22. +// Created by Brian Dashore on 8/3/22. // // -import CoreData import Foundation +import CoreData -public extension Source { - @nonobjc class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "Source") + +extension Source { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Source") } - @NSManaged var name: String - @NSManaged var enabled: Bool - @NSManaged var version: String - @NSManaged var baseUrl: String - @NSManaged var htmlParser: SourceHtmlParser? + @NSManaged public var baseUrl: String + @NSManaged public var enabled: Bool + @NSManaged public var name: String + @NSManaged public var author: String? + @NSManaged public var preferredParser: Int16 + @NSManaged public var version: String + @NSManaged public var htmlParser: SourceHtmlParser? + @NSManaged public var rssParser: SourceRssParser? + } -extension Source: Identifiable {} +extension Source : Identifiable { + +} diff --git a/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift index eaa9c56..56b25f8 100644 --- a/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift @@ -15,6 +15,7 @@ public extension SourceComplexQuery { } @NSManaged var attribute: String + @NSManaged var lookupAttribute: String? @NSManaged var query: String @NSManaged var regex: String? } diff --git a/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift index e7e6268..0b1de8f 100644 --- a/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift @@ -2,25 +2,49 @@ // SourceHtmlParser+CoreDataProperties.swift // Ferrite // -// Created by Brian Dashore on 8/2/22. +// Created by Brian Dashore on 8/3/22. // // -import CoreData import Foundation +import CoreData -public extension SourceHtmlParser { - @nonobjc class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "SourceHtmlParser") + +extension SourceHtmlParser { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "SourceHtmlParser") } - @NSManaged var rows: String - @NSManaged var searchUrl: String - @NSManaged var magnet: SourceMagnet? - @NSManaged var parentSource: Source? - @NSManaged var size: SourceSize? - @NSManaged var title: SourceTitle? - @NSManaged var seedLeech: SourceSeedLeech? + @NSManaged public var rows: String + @NSManaged public var searchUrl: String + @NSManaged public var magnetLink: SourceMagnetLink? + @NSManaged public var parentSource: Source? + @NSManaged public var seedLeech: SourceSeedLeech? + @NSManaged public var size: SourceSize? + @NSManaged public var title: SourceTitle? + @NSManaged public var magnetHash: SourceMagnetHash? + @NSManaged public var trackers: NSSet? + } -extension SourceHtmlParser: Identifiable {} +// MARK: Generated accessors for trackers +extension SourceHtmlParser { + + @objc(addTrackersObject:) + @NSManaged public func addToTrackers(_ value: SourceTracker) + + @objc(removeTrackersObject:) + @NSManaged public func removeFromTrackers(_ value: SourceTracker) + + @objc(addTrackers:) + @NSManaged public func addToTrackers(_ values: NSSet) + + @objc(removeTrackers:) + @NSManaged public func removeFromTrackers(_ values: NSSet) + +} + +extension SourceHtmlParser : Identifiable { + +} diff --git a/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift index aa85143..fb205bb 100644 --- a/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift @@ -14,8 +14,8 @@ public extension SourceList { NSFetchRequest(entityName: "SourceList") } - @NSManaged var repoAuthor: String? - @NSManaged var repoName: String? + @NSManaged var author: String + @NSManaged var name: String @NSManaged var urlString: String } diff --git a/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataClass.swift b/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataClass.swift new file mode 100644 index 0000000..1fda4d9 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// SourceRssParser+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 8/3/22. +// +// + +import Foundation +import CoreData + +@objc(SourceRssParser) +public class SourceRssParser: NSManagedObject { + +} diff --git a/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift new file mode 100644 index 0000000..1b36d5e --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift @@ -0,0 +1,56 @@ +// +// SourceRssParser+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 8/3/22. +// +// + +import Foundation +import CoreData + + +extension SourceRssParser { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "SourceRssParser") + } + + @NSManaged public var items: String + @NSManaged public var searchUrl: String + @NSManaged public var rssUrl: String? + @NSManaged public var parentSource: Source? + @NSManaged public var trackers: NSSet? + @NSManaged public var magnetLink: SourceMagnetLink? + @NSManaged public var size: SourceSize? + @NSManaged public var title: SourceTitle? + @NSManaged public var seedLeech: SourceSeedLeech? + @NSManaged public var magnetHash: SourceMagnetHash? + + var trackerArray: [SourceTracker] { + let trackerSet = trackers as? Set ?? [] + + return trackerSet.map { $0 } + } +} + +// MARK: Generated accessors for trackers +extension SourceRssParser { + + @objc(addTrackersObject:) + @NSManaged public func addToTrackers(_ value: SourceTracker) + + @objc(removeTrackersObject:) + @NSManaged public func removeFromTrackers(_ value: SourceTracker) + + @objc(addTrackers:) + @NSManaged public func addToTrackers(_ values: NSSet) + + @objc(removeTrackers:) + @NSManaged public func removeFromTrackers(_ values: NSSet) + +} + +extension SourceRssParser : Identifiable { + +} diff --git a/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift index 365bb59..b2362a0 100644 --- a/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift @@ -20,6 +20,7 @@ public extension SourceSeedLeech { @NSManaged var seederRegex: String? @NSManaged var seeders: String? @NSManaged var attribute: String + @NSManaged var lookupAttribute: String? @NSManaged var parentParser: SourceHtmlParser? } diff --git a/Ferrite/DataManagement/Classes/SourceTracker+CoreDataClass.swift b/Ferrite/DataManagement/Classes/SourceTracker+CoreDataClass.swift new file mode 100644 index 0000000..8fc0067 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceTracker+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// SourceTracker+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 8/3/22. +// +// + +import Foundation +import CoreData + +@objc(SourceTracker) +public class SourceTracker: NSManagedObject { + +} diff --git a/Ferrite/DataManagement/Classes/SourceTracker+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceTracker+CoreDataProperties.swift new file mode 100644 index 0000000..6b819db --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceTracker+CoreDataProperties.swift @@ -0,0 +1,27 @@ +// +// SourceTracker+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 8/3/22. +// +// + +import Foundation +import CoreData + + +extension SourceTracker { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "SourceTracker") + } + + @NSManaged public var urlString: String + @NSManaged public var parentRssParser: SourceRssParser? + @NSManaged public var parentHtmlParser: SourceHtmlParser? + +} + +extension SourceTracker : Identifiable { + +} diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents index 7696a03..900fa28 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents @@ -1,48 +1,80 @@ + + + - + + - + + - - - + + + + - - + + - + + + + + - + + + + + + + + + + + + + + + - + + - + + - + + + + + + + \ No newline at end of file diff --git a/Ferrite/Models/SourceModels.swift b/Ferrite/Models/SourceModels.swift index e2d99ef..3b56644 100644 --- a/Ferrite/Models/SourceModels.swift +++ b/Ferrite/Models/SourceModels.swift @@ -8,36 +8,52 @@ import Foundation public struct SourceListJson: Codable { - let repoName: String? - let repoAuthor: String? - let sources: [SourceJson] - - enum CodingKeys: String, CodingKey { - case repoName = "name" - case repoAuthor = "author" - case sources - } + let name: String + let author: String + var sources: [SourceJson] } public struct SourceJson: Codable, Hashable { let name: String let version: String let baseUrl: String + var author: String? + let rssParser: SourceRssParserJson? let htmlParser: SourceHtmlParserJson? } +public enum SourcePreferredParser: Int16, CaseIterable { + case none = 0 + case scraping = 1 + case rss = 2 + case siteApi = 3 +} + +public struct SourceRssParserJson: Codable, Hashable { + let rssUrl: String? + let searchUrl: String + let items: String + let magnetHash: SouceComplexQueryJson? + let magnetLink: SouceComplexQueryJson? + let title: SouceComplexQueryJson? + let size: SouceComplexQueryJson? + let sl: SourceSLJson? + let trackers: [String]? +} + public struct SourceHtmlParserJson: Codable, Hashable { let searchUrl: String let rows: String let magnet: SourceMagnetJson - let title: SouceComplexQuery? - let size: SouceComplexQuery? + let title: SouceComplexQueryJson? + let size: SouceComplexQueryJson? let sl: SourceSLJson? } -public struct SouceComplexQuery: Codable, Hashable { +public struct SouceComplexQueryJson: Codable, Hashable { let query: String - let attribute: String + let lookupAttribute: String? + let attribute: String? let regex: String? } @@ -52,7 +68,8 @@ public struct SourceSLJson: Codable, Hashable { let seeders: String? let leechers: String? let combined: String? - let attribute: String + let attribute: String? + let lookupAttribute: String? let seederRegex: String? let leecherRegex: String? } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 20b09f8..f5a38d5 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -8,6 +8,7 @@ import SwiftUI class NavigationViewModel: ObservableObject { + // Used between SearchResultsView and MagnetChoiceView enum ChoiceSheetType: Identifiable { var id: Int { hashValue @@ -18,4 +19,8 @@ class NavigationViewModel: ObservableObject { } @Published var currentChoiceSheet: ChoiceSheetType? + + // Used between SourceListView and SourceSettingsView + @Published var showSourceSettings: Bool = false + @Published var selectedSource: Source? } diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 38b4c73..b7742d4 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -25,6 +25,7 @@ class ScrapingViewModel: ObservableObject { // Link the toast view model for single-directional communication var toastModel: ToastViewModel? + let byteCountFormatter: ByteCountFormatter = .init() @Published var searchResults: [SearchResult] = [] @Published var searchText: String = "" @@ -42,22 +43,56 @@ class ScrapingViewModel: ObservableObject { for source in sources { if source.enabled { - if let htmlParser = source.htmlParser { - guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - toastModel?.toastDescription = "Could not process search query, invalid characters present." - print("Could not process search query, invalid characters present") + // Default to HTML scraping + let preferredParser = SourcePreferredParser(rawValue: source.preferredParser) ?? .none - continue + switch preferredParser { + case .scraping: + if let htmlParser = source.htmlParser { + guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + toastModel?.toastDescription = "Could not process search query, invalid characters present." + print("Could not process search query, invalid characters present") + + continue + } + + let urlString = source.baseUrl + htmlParser.searchUrl.replacingOccurrences(of: "{query}", with: encodedQuery) + + guard let html = await fetchWebsiteData(urlString: urlString) else { + continue + } + + let sourceResults = await scrapeHtml(source: source, html: html) + tempResults += sourceResults } + case .rss: + if let rssParser = source.rssParser { + guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + toastModel?.toastDescription = "Could not process search query, invalid characters present." + print("Could not process search query, invalid characters present") - let urlString = source.baseUrl + htmlParser.searchUrl.replacingOccurrences(of: "{query}", with: encodedQuery) + continue + } - guard let html = await fetchWebsiteHtml(urlString: urlString) else { - continue + let replacedSearchUrl = rssParser.searchUrl.replacingOccurrences(of: "{query}", with: encodedQuery) + + // If there is an RSS base URL, use that instead + var urlString: String + if let rssUrl = rssParser.rssUrl { + urlString = rssUrl + replacedSearchUrl + } else { + urlString = source.baseUrl + replacedSearchUrl + } + + guard let rss = await fetchWebsiteData(urlString: urlString) else { + continue + } + + let sourceResults = scrapeRss(source: source, rss: rss) + tempResults += sourceResults } - - let sourceResults = await scrapeWebsite(source: source, html: html) - tempResults += sourceResults + case .siteApi, .none: + continue } } } @@ -65,9 +100,9 @@ class ScrapingViewModel: ObservableObject { searchResults = tempResults } - // Fetches the HTML for a URL + // Fetches the data for a URL @MainActor - public func fetchWebsiteHtml(urlString: String) async -> String? { + public func fetchWebsiteData(urlString: String) async -> String? { guard let url = URL(string: urlString) else { toastModel?.toastDescription = "Source doesn't contain a valid URL, contact the source dev!" print("Source doesn't contain a valid URL, contact the source dev!") @@ -80,17 +115,139 @@ class ScrapingViewModel: ObservableObject { let html = String(data: data, encoding: .ascii) return html } catch { - toastModel?.toastDescription = "Error in fetching HTML \(error)" - print("Error in fetching HTML \(error)") + toastModel?.toastDescription = "Error in fetching data \(error)" + print("Error in fetching data \(error)") return nil } } - // Returns results to UI - // Results must have a link and title, but other parameters aren't required + // RSS feed scraper @MainActor - public func scrapeWebsite(source: Source, html: String) async -> [SearchResult] { + public func scrapeRss(source: Source, rss: String) -> [SearchResult] { + guard let rssParser = source.rssParser else { + return [] + } + + var tempResults: [SearchResult] = [] + + var items = Elements() + + do { + let document = try SwiftSoup.parse(rss, "", Parser.xmlParser()) + items = try document.getElementsByTag("item") + } catch { + toastModel?.toastDescription = "RSS scraping error, couldn't fetch items: \(error)" + print("RSS scraping error, couldn't fetch items: \(error)") + + return [] + } + + for item in items { + // Parse magnet link or translate hash + var magnetHash: String? + if let magnetHashParser = rssParser.magnetHash { + magnetHash = try? runRssComplexQuery( + item: item, + query: magnetHashParser.query, + attribute: magnetHashParser.attribute, + lookupAttribute: magnetHashParser.lookupAttribute, + regexString: magnetHashParser.regex + ) + } + + var title: String? + if let titleParser = rssParser.title { + title = try? runRssComplexQuery( + item: item, + query: titleParser.query, + attribute: titleParser.attribute, + lookupAttribute: titleParser.lookupAttribute, + regexString: titleParser.regex + ) + } + + var link: String? + if let magnetLinkParser = rssParser.magnetLink { + link = try? runRssComplexQuery( + item: item, + query: magnetLinkParser.query, + attribute: magnetLinkParser.attribute, + lookupAttribute: magnetLinkParser.lookupAttribute, + regexString: magnetLinkParser.regex + ) + } else if let magnetHash = magnetHash { + link = generateMagnetLink(magnetHash: magnetHash, title: title, trackers: rssParser.trackerArray) + } else { + continue + } + + guard let href = link, href.starts(with: "magnet:") else { + continue + } + + if magnetHash == nil { + magnetHash = fetchMagnetHash(magnetLink: href) + } + + var size: String? + if let sizeParser = rssParser.size { + size = try? runRssComplexQuery( + item: item, + query: sizeParser.query, + attribute: sizeParser.attribute, + lookupAttribute: sizeParser.lookupAttribute, + regexString: sizeParser.regex + ) + } + + if let sizeString = size, let sizeInt = Int64(sizeString) { + size = byteCountFormatter.string(fromByteCount: sizeInt) + } + + var seeders: String? + var leechers: String? + if let seederLeecher = rssParser.seedLeech { + if let seederQuery = seederLeecher.seeders { + seeders = try? runRssComplexQuery( + item: item, + query: seederQuery, + attribute: seederLeecher.attribute, + lookupAttribute: seederLeecher.lookupAttribute, + regexString: seederLeecher.seederRegex + ) + } + + if let leecherQuery = seederLeecher.leechers { + leechers = try? runRssComplexQuery( + item: item, + query: leecherQuery, + attribute: seederLeecher.attribute, + lookupAttribute: seederLeecher.lookupAttribute, + regexString: seederLeecher.leecherRegex + ) + } + } + + let result = SearchResult( + title: title ?? "No title", + source: source.name, + size: size ?? "", + magnetLink: href, + magnetHash: magnetHash, + seeders: seeders, + leechers: leechers + ) + + tempResults.append(result) + } + + return tempResults + } + + // HTML scraper + @MainActor + public func scrapeHtml(source: Source, html: String) async -> [SearchResult] { guard let htmlParser = source.htmlParser else { return [] } @@ -115,7 +272,7 @@ class ScrapingViewModel: ObservableObject { // Fetches the magnet link // If the magnet is located on an external page, fetch the external page and grab the magnet link // External page fetching affects source performance - guard let magnetParser = htmlParser.magnet else { + guard let magnetParser = htmlParser.magnetLink else { continue } @@ -125,7 +282,7 @@ class ScrapingViewModel: ObservableObject { continue } - guard let magnetHtml = await fetchWebsiteHtml(urlString: source.baseUrl + externalMagnetLink) else { + guard let magnetHtml = await fetchWebsiteData(urlString: source.baseUrl + externalMagnetLink) else { continue } @@ -140,7 +297,7 @@ class ScrapingViewModel: ObservableObject { href = try linkResult.attr(magnetParser.attribute) } } else { - guard let link = try runComplexQuery( + guard let link = try runHtmlComplexQuery( row: row, query: magnetParser.query, attribute: magnetParser.attribute, @@ -162,7 +319,7 @@ class ScrapingViewModel: ObservableObject { // Fetches the episode/movie title var title: String? if let titleParser = htmlParser.title { - title = try? runComplexQuery( + title = try? runHtmlComplexQuery( row: row, query: titleParser.query, attribute: titleParser.attribute, @@ -171,9 +328,10 @@ class ScrapingViewModel: ObservableObject { } // Fetches the torrent's size + // TODO: Add int translation var size: String? if let sizeParser = htmlParser.size { - size = try? runComplexQuery( + size = try? runHtmlComplexQuery( row: row, query: sizeParser.query, attribute: sizeParser.attribute, @@ -186,7 +344,7 @@ class ScrapingViewModel: ObservableObject { var leechers: String? if let seederLeecher = htmlParser.seedLeech { if let combinedQuery = seederLeecher.combined { - if let combinedString = try? runComplexQuery( + if let combinedString = try? runHtmlComplexQuery( row: row, query: combinedQuery, attribute: seederLeecher.attribute, @@ -202,7 +360,7 @@ class ScrapingViewModel: ObservableObject { } } else { if let seederQuery = seederLeecher.seeders { - seeders = try? runComplexQuery( + seeders = try? runHtmlComplexQuery( row: row, query: seederQuery, attribute: seederLeecher.attribute, @@ -211,7 +369,7 @@ class ScrapingViewModel: ObservableObject { } if let leecherQuery = seederLeecher.seeders { - leechers = try? runComplexQuery( + leechers = try? runHtmlComplexQuery( row: row, query: leecherQuery, attribute: seederLeecher.attribute, @@ -243,7 +401,8 @@ class ScrapingViewModel: ObservableObject { return tempResults } - func runComplexQuery(row: Element, query: String, attribute: String, regexString: String?) throws -> String? { + // Complex query parsing for HTML scraping + func runHtmlComplexQuery(row: Element, query: String, attribute: String, regexString: String?) throws -> String? { var parsedValue: String? let result = try row.select(query).first() @@ -258,7 +417,36 @@ class ScrapingViewModel: ObservableObject { // A capture group must be used in the provided regex if let regexString = regexString, let parsedValue = parsedValue, - let regexValue = try Regex(regexString).firstMatch(in: parsedValue)?.groups[safe: 0]?.value + let regexValue = try? Regex(regexString).firstMatch(in: parsedValue)?.groups[safe: 0]?.value + { + return regexValue + } else { + return parsedValue + } + } + + // Complex query parsing for RSS scraping + func runRssComplexQuery(item: Element, query: String, attribute: String, lookupAttribute: String?, regexString: String?) throws -> String? { + var parsedValue: String? + + switch attribute { + case "text": + parsedValue = try item.getElementsByTag(query).first()?.text() + default: + // If there's a key/value to lookup the attribute with, query it. Othewise assume the value is in the same attribute + if let lookupAttribute = lookupAttribute { + let containerElement = try item.getElementsByAttributeValue(lookupAttribute, query).first() + parsedValue = try containerElement?.attr(attribute) + } else { + let containerElement = try item.getElementsByAttribute(attribute).first() + parsedValue = try containerElement?.attr(attribute) + } + } + + // A capture group must be used in the provided regex + if let regexString = regexString, + let parsedValue = parsedValue, + let regexValue = try? Regex(regexString).firstMatch(in: parsedValue)?.groups[safe: 0]?.value { return regexValue } else { @@ -284,4 +472,46 @@ class ScrapingViewModel: ObservableObject { return String(magnetHash).lowercased() } } + + func parseSizeString(sizeString: String) -> String? { + // Test if the string can be a full integer + guard let size = Int(sizeString) else { + return nil + } + + let length = sizeString.count + + if length > 9 { + // This is a GB + return String("\(Double(size) / 1e9) GB") + } else if length > 6 { + // This is a MB + return String("\(Double(size) / 1e6) MB") + } else if length > 3 { + // This is a KB + return String("\(Double(size) / 1e3) KB") + } else { + return nil + } + } + + public func generateMagnetLink(magnetHash: String, title: String?, trackers: [SourceTracker]) -> String { + var magnetLinkArray: [String] = ["magnet:?xt=urn:btih:"] + + magnetLinkArray.append(magnetHash) + + if let title = title, let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) { + magnetLinkArray.append("&dn=\(encodedTitle)") + } + + for tracker in trackers { + if URL(string: tracker.urlString) != nil, + let encodedUrlString = tracker.urlString.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) + { + magnetLinkArray.append("&tr=\(encodedUrlString)") + } + } + + return magnetLinkArray.joined() + } } diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index dd99f4b..24aab10 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -18,18 +18,22 @@ public class SourceManager: ObservableObject { @MainActor public func fetchSourcesFromUrl() async { - let sourceUrlRequest = SourceList.fetchRequest() + let sourceListRequest = SourceList.fetchRequest() do { - let sourceUrls = try PersistenceController.shared.backgroundContext.fetch(sourceUrlRequest) + let sourceLists = try PersistenceController.shared.backgroundContext.fetch(sourceListRequest) var tempSourceUrls: [SourceJson] = [] - for sourceUrl in sourceUrls { - guard let url = URL(string: sourceUrl.urlString) else { + for sourceList in sourceLists { + guard let url = URL(string: sourceList.urlString) else { return } let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url)) - let sourceResponse = try JSONDecoder().decode(SourceListJson.self, from: data) + var sourceResponse = try JSONDecoder().decode(SourceListJson.self, from: data) + + for index in sourceResponse.sources.indices { + sourceResponse.sources[index].author = sourceList.author + } tempSourceUrls += sourceResponse.sources } @@ -61,55 +65,23 @@ public class SourceManager: ObservableObject { newSource.name = sourceJson.name newSource.version = sourceJson.version newSource.baseUrl = sourceJson.baseUrl + newSource.author = sourceJson.author + + // Adds an RSS parser if present + if let rssParserJson = sourceJson.rssParser { + addRssParser(newSource: newSource, rssParserJson: rssParserJson) + } // Adds an HTML parser if present if let htmlParserJson = sourceJson.htmlParser { - let newSourceHtmlParser = SourceHtmlParser(context: backgroundContext) - newSourceHtmlParser.searchUrl = htmlParserJson.searchUrl - newSourceHtmlParser.rows = htmlParserJson.rows + addHtmlParser(newSource: newSource, htmlParserJson: htmlParserJson) + } - // Adds a title complex query if present - if let titleJson = htmlParserJson.title { - let newSourceTitle = SourceTitle(context: backgroundContext) - newSourceTitle.query = titleJson.query - newSourceTitle.attribute = titleJson.attribute - newSourceTitle.regex = titleJson.regex - - newSourceHtmlParser.title = newSourceTitle - } - - // Adds a size complex query if present - if let sizeJson = htmlParserJson.size { - let newSourceSize = SourceSize(context: backgroundContext) - newSourceSize.query = sizeJson.query - newSourceSize.attribute = sizeJson.attribute - newSourceSize.regex = sizeJson.regex - - newSourceHtmlParser.size = newSourceSize - } - - if let seedLeechJson = htmlParserJson.sl { - let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext) - newSourceSeedLeech.seeders = seedLeechJson.seeders - newSourceSeedLeech.leechers = seedLeechJson.leechers - newSourceSeedLeech.combined = seedLeechJson.combined - newSourceSeedLeech.attribute = seedLeechJson.attribute - newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex - newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex - - newSourceHtmlParser.seedLeech = newSourceSeedLeech - } - - // Adds a magnet complex query and its unique properties - let newSourceMagnet = SourceMagnet(context: backgroundContext) - newSourceMagnet.externalLinkQuery = htmlParserJson.magnet.externalLinkQuery - newSourceMagnet.query = htmlParserJson.magnet.query - newSourceMagnet.attribute = htmlParserJson.magnet.attribute - newSourceMagnet.regex = htmlParserJson.magnet.regex - - newSourceHtmlParser.magnet = newSourceMagnet - - newSource.htmlParser = newSourceHtmlParser + // Add an API condition as well + if newSource.rssParser != nil { + newSource.preferredParser = Int16(SourcePreferredParser.rss.rawValue) + } else { + newSource.preferredParser = Int16(SourcePreferredParser.scraping.rawValue) } newSource.enabled = true @@ -123,6 +95,125 @@ public class SourceManager: ObservableObject { } } + func addRssParser(newSource: Source, rssParserJson: SourceRssParserJson) { + let backgroundContext = PersistenceController.shared.backgroundContext + + let newSourceRssParser = SourceRssParser(context: backgroundContext) + newSourceRssParser.rssUrl = rssParserJson.rssUrl + newSourceRssParser.searchUrl = rssParserJson.searchUrl + newSourceRssParser.items = rssParserJson.items + + if let magnetLinkJson = rssParserJson.magnetLink { + let newSourceMagnetLink = SourceMagnetLink(context: backgroundContext) + newSourceMagnetLink.query = magnetLinkJson.query + newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text" + newSourceMagnetLink.lookupAttribute = magnetLinkJson.lookupAttribute + + newSourceRssParser.magnetLink = newSourceMagnetLink + } + + if let magnetHashJson = rssParserJson.magnetHash { + let newSourceMagnetHash = SourceMagnetHash(context: backgroundContext) + newSourceMagnetHash.query = magnetHashJson.query + newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text" + newSourceMagnetHash.lookupAttribute = magnetHashJson.lookupAttribute + + newSourceRssParser.magnetHash = newSourceMagnetHash + } + + if let titleJson = rssParserJson.title { + let newSourceTitle = SourceTitle(context: backgroundContext) + newSourceTitle.query = titleJson.query + newSourceTitle.attribute = titleJson.attribute ?? "text" + newSourceTitle.lookupAttribute = newSourceTitle.lookupAttribute + + newSourceRssParser.title = newSourceTitle + } + + if let sizeJson = rssParserJson.size { + let newSourceSize = SourceSize(context: backgroundContext) + newSourceSize.query = sizeJson.query + newSourceSize.attribute = sizeJson.attribute ?? "text" + newSourceSize.lookupAttribute = sizeJson.lookupAttribute + + newSourceRssParser.size = newSourceSize + } + + if let seedLeechJson = rssParserJson.sl { + let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext) + newSourceSeedLeech.seeders = seedLeechJson.seeders + newSourceSeedLeech.leechers = seedLeechJson.leechers + newSourceSeedLeech.combined = seedLeechJson.combined + newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text" + newSourceSeedLeech.lookupAttribute = seedLeechJson.lookupAttribute + newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex + newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex + + newSourceRssParser.seedLeech = newSourceSeedLeech + } + + if let trackerJson = rssParserJson.trackers { + for urlString in trackerJson { + let newSourceTracker = SourceTracker(context: backgroundContext) + newSourceTracker.urlString = urlString + newSourceTracker.parentRssParser = newSourceRssParser + } + } + + newSource.rssParser = newSourceRssParser + } + + func addHtmlParser(newSource: Source, htmlParserJson: SourceHtmlParserJson) { + let backgroundContext = PersistenceController.shared.backgroundContext + + let newSourceHtmlParser = SourceHtmlParser(context: backgroundContext) + newSourceHtmlParser.searchUrl = htmlParserJson.searchUrl + newSourceHtmlParser.rows = htmlParserJson.rows + + // Adds a title complex query if present + if let titleJson = htmlParserJson.title { + let newSourceTitle = SourceTitle(context: backgroundContext) + newSourceTitle.query = titleJson.query + newSourceTitle.attribute = titleJson.attribute ?? "text" + newSourceTitle.regex = titleJson.regex + + newSourceHtmlParser.title = newSourceTitle + } + + // Adds a size complex query if present + if let sizeJson = htmlParserJson.size { + let newSourceSize = SourceSize(context: backgroundContext) + newSourceSize.query = sizeJson.query + newSourceSize.attribute = sizeJson.attribute ?? "text" + newSourceSize.regex = sizeJson.regex + + newSourceHtmlParser.size = newSourceSize + } + + if let seedLeechJson = htmlParserJson.sl { + let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext) + newSourceSeedLeech.seeders = seedLeechJson.seeders + newSourceSeedLeech.leechers = seedLeechJson.leechers + newSourceSeedLeech.combined = seedLeechJson.combined + newSourceSeedLeech.attribute = seedLeechJson.attribute ?? "text" + newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex + newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex + + newSourceHtmlParser.seedLeech = newSourceSeedLeech + } + + // Adds a magnet complex query and its unique properties + let newSourceMagnet = SourceMagnetLink(context: backgroundContext) + newSourceMagnet.externalLinkQuery = htmlParserJson.magnet.externalLinkQuery + newSourceMagnet.query = htmlParserJson.magnet.query + newSourceMagnet.attribute = htmlParserJson.magnet.attribute + newSourceMagnet.regex = htmlParserJson.magnet.regex + + newSourceHtmlParser.magnetLink = newSourceMagnet + + newSource.htmlParser = newSourceHtmlParser + } + @MainActor public func addSourceList(sourceUrl: String) async -> Bool { let backgroundContext = PersistenceController.shared.backgroundContext @@ -134,24 +225,25 @@ public class SourceManager: ObservableObject { return false } - let sourceUrlRequest = SourceList.fetchRequest() - sourceUrlRequest.predicate = NSPredicate(format: "urlString == %@", sourceUrl) - sourceUrlRequest.fetchLimit = 1 - - if let existingSourceUrl = try? backgroundContext.fetch(sourceUrlRequest).first { - print("Existing source URL found") - PersistenceController.shared.delete(existingSourceUrl, context: backgroundContext) - } - - let newSourceUrl = SourceList(context: backgroundContext) - newSourceUrl.urlString = sourceUrl - do { let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!)) - if let rawResponse = try? JSONDecoder().decode(SourceListJson.self, from: data) { - newSourceUrl.repoName = rawResponse.repoName + let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data) + + let sourceListRequest = SourceList.fetchRequest() + sourceListRequest.predicate = NSPredicate(format: "urlString == %@ OR author == %@", sourceUrl, rawResponse.author) + sourceListRequest.fetchLimit = 1 + + if (try? backgroundContext.fetch(sourceListRequest).first) != nil { + urlErrorAlertText = "A source with the same URL or author exists. Please remove it and try again." + showUrlErrorAlert.toggle() + return false } + let newSourceUrl = SourceList(context: backgroundContext) + newSourceUrl.urlString = sourceUrl + newSourceUrl.name = rawResponse.name + newSourceUrl.author = rawResponse.author + try backgroundContext.save() return true diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 72e3226..1abab69 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -73,7 +73,10 @@ struct ContentView: View { .onSubmit(of: .search) { Task { await scrapingModel.scanSources(sources: sources.compactMap { $0 }) - await debridManager.populateDebridHashes(scrapingModel.searchResults) + + if realDebridEnabled { + await debridManager.populateDebridHashes(scrapingModel.searchResults) + } } } .navigationTitle("Search") diff --git a/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift b/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift index dfc34ef..11722d4 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift @@ -20,7 +20,7 @@ struct SettingsSourceListView: View { var body: some View { List { ForEach(sourceUrls, id: \.self) { sourceUrl in - Text(sourceUrl.repoName ?? "Unknown repo") + Text(sourceUrl.name) } .onDelete { offsets in for index in offsets { diff --git a/Ferrite/Views/SourceListView.swift b/Ferrite/Views/SourceListView.swift index 0bad7cc..5cdf60a 100644 --- a/Ferrite/Views/SourceListView.swift +++ b/Ferrite/Views/SourceListView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SourceListView: View { @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var navModel: NavigationViewModel let backgroundContext = PersistenceController.shared.backgroundContext @@ -32,15 +33,36 @@ struct SourceListView: View { PersistenceController.shared.save() } )) { - Text(source.name) - } - } - .onDelete { offsets in - for index in offsets { - if let source = sources[safe: index] { - PersistenceController.shared.delete(source, context: backgroundContext) + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(source.name) + Text("v\(source.version)") + .foregroundColor(.secondary) + } + + Text("by \(source.author ?? "Unknown")") + .foregroundColor(.secondary) } } + .contextMenu { + Button { + navModel.selectedSource = source + navModel.showSourceSettings.toggle() + } label: { + Text("Settings") + Image(systemName: "gear") + } + + Button { + PersistenceController.shared.delete(source, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } + } + .sheet(isPresented: $navModel.showSourceSettings) { + SourceSettingsView() } } } @@ -52,7 +74,16 @@ struct SourceListView: View { ForEach(sourceManager.availableSources, id: \.self) { availableSource in if !sources.contains(where: { availableSource.name == $0.name }) { HStack { - Text(availableSource.name) + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(availableSource.name) + Text("v\(availableSource.version)") + .foregroundColor(.secondary) + } + + Text("by \(availableSource.author ?? "Unknown")") + .foregroundColor(.secondary) + } Spacer() diff --git a/Ferrite/Views/SourceSettingsView.swift b/Ferrite/Views/SourceSettingsView.swift new file mode 100644 index 0000000..fcbf77b --- /dev/null +++ b/Ferrite/Views/SourceSettingsView.swift @@ -0,0 +1,56 @@ +// +// SourceSettingsView.swift +// Ferrite +// +// Created by Brian Dashore on 8/4/22. +// + +import SwiftUI + +struct SourceSettingsView: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + NavView { + Form { + SourceSettingsMethodView() + } + .navigationTitle("Source settings") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + dismiss() + } + } + } + } + } +} + +struct SourceSettingsMethodView: View { + @EnvironmentObject var navModel: NavigationViewModel + + @State private var selectedTempParser: SourcePreferredParser = .none + + var body: some View { + Picker("Fetch method", selection: $selectedTempParser) { + if navModel.selectedSource?.htmlParser != nil { + Text("Web scraping") + .tag(SourcePreferredParser.scraping) + } + + if navModel.selectedSource?.rssParser != nil { + Text("RSS") + .tag(SourcePreferredParser.rss) + } + } + .pickerStyle(.inline) + .onAppear { + selectedTempParser = SourcePreferredParser(rawValue: navModel.selectedSource?.preferredParser ?? 0) ?? .none + } + .onChange(of: selectedTempParser) { newMethod in + navModel.selectedSource?.preferredParser = newMethod.rawValue + PersistenceController.shared.save() + } + } +}