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 <bdashore3@gmail.com>
This commit is contained in:
parent
9ea7ab7b11
commit
efecfa3236
19 changed files with 787 additions and 154 deletions
|
|
@ -18,6 +18,9 @@
|
||||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; };
|
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; };
|
||||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
|
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 */; };
|
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 */; };
|
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; };
|
||||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */; };
|
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 */; };
|
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.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 */; };
|
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
|
@ -67,6 +72,9 @@
|
||||||
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
|
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
|
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
|
||||||
|
0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
|
0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = "<group>"; };
|
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = "<group>"; };
|
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = "<group>"; };
|
||||||
|
|
@ -103,6 +111,8 @@
|
||||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
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>"; };
|
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
|
0CF501F0289AE06A0099C785 /* SourceTracker+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceTracker+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
|
0CF501F1289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceTracker+CoreDataProperties.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 */
|
||||||
|
|
||||||
|
|
@ -125,6 +135,10 @@
|
||||||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */,
|
||||||
|
0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */,
|
||||||
|
0CF501F0289AE06A0099C785 /* SourceTracker+CoreDataClass.swift */,
|
||||||
|
0CF501F1289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift */,
|
||||||
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */,
|
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */,
|
||||||
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */,
|
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */,
|
||||||
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */,
|
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */,
|
||||||
|
|
@ -218,6 +232,7 @@
|
||||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||||
0C0D50E6288DFF850035ECC8 /* SourceListView.swift */,
|
0C0D50E6288DFF850035ECC8 /* SourceListView.swift */,
|
||||||
|
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
|
||||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
|
|
@ -365,15 +380,18 @@
|
||||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
||||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||||
|
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
|
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
|
||||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||||
|
0CF501F2289AE06A0099C785 /* SourceTracker+CoreDataClass.swift in Sources */,
|
||||||
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
|
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
|
||||||
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */,
|
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */,
|
||||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||||
|
0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */,
|
||||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
||||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
|
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
|
||||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
||||||
|
|
@ -388,7 +406,9 @@
|
||||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
||||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||||
|
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */,
|
||||||
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */,
|
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */,
|
||||||
|
0CF501F3289AE06A0099C785 /* SourceTracker+CoreDataProperties.swift in Sources */,
|
||||||
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
|
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
|
||||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
|
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,31 @@
|
||||||
// Source+CoreDataProperties.swift
|
// Source+CoreDataProperties.swift
|
||||||
// Ferrite
|
// Ferrite
|
||||||
//
|
//
|
||||||
// Created by Brian Dashore on 7/30/22.
|
// Created by Brian Dashore on 8/3/22.
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
|
||||||
import CoreData
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
public extension Source {
|
|
||||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> {
|
extension Source {
|
||||||
NSFetchRequest<Source>(entityName: "Source")
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<Source> {
|
||||||
|
return NSFetchRequest<Source>(entityName: "Source")
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged var name: String
|
@NSManaged public var baseUrl: String
|
||||||
@NSManaged var enabled: Bool
|
@NSManaged public var enabled: Bool
|
||||||
@NSManaged var version: String
|
@NSManaged public var name: String
|
||||||
@NSManaged var baseUrl: String
|
@NSManaged public var author: String?
|
||||||
@NSManaged var htmlParser: SourceHtmlParser?
|
@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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public extension SourceComplexQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged var attribute: String
|
@NSManaged var attribute: String
|
||||||
|
@NSManaged var lookupAttribute: String?
|
||||||
@NSManaged var query: String
|
@NSManaged var query: String
|
||||||
@NSManaged var regex: String?
|
@NSManaged var regex: String?
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,49 @@
|
||||||
// SourceHtmlParser+CoreDataProperties.swift
|
// SourceHtmlParser+CoreDataProperties.swift
|
||||||
// Ferrite
|
// Ferrite
|
||||||
//
|
//
|
||||||
// Created by Brian Dashore on 8/2/22.
|
// Created by Brian Dashore on 8/3/22.
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
|
||||||
import CoreData
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
public extension SourceHtmlParser {
|
|
||||||
@nonobjc class func fetchRequest() -> NSFetchRequest<SourceHtmlParser> {
|
extension SourceHtmlParser {
|
||||||
NSFetchRequest<SourceHtmlParser>(entityName: "SourceHtmlParser")
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<SourceHtmlParser> {
|
||||||
|
return NSFetchRequest<SourceHtmlParser>(entityName: "SourceHtmlParser")
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged var rows: String
|
@NSManaged public var rows: String
|
||||||
@NSManaged var searchUrl: String
|
@NSManaged public var searchUrl: String
|
||||||
@NSManaged var magnet: SourceMagnet?
|
@NSManaged public var magnetLink: SourceMagnetLink?
|
||||||
@NSManaged var parentSource: Source?
|
@NSManaged public var parentSource: Source?
|
||||||
@NSManaged var size: SourceSize?
|
@NSManaged public var seedLeech: SourceSeedLeech?
|
||||||
@NSManaged var title: SourceTitle?
|
@NSManaged public var size: SourceSize?
|
||||||
@NSManaged var seedLeech: SourceSeedLeech?
|
@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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ public extension SourceList {
|
||||||
NSFetchRequest<SourceList>(entityName: "SourceList")
|
NSFetchRequest<SourceList>(entityName: "SourceList")
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged var repoAuthor: String?
|
@NSManaged var author: String
|
||||||
@NSManaged var repoName: String?
|
@NSManaged var name: String
|
||||||
@NSManaged var urlString: String
|
@NSManaged var urlString: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<SourceRssParser> {
|
||||||
|
return NSFetchRequest<SourceRssParser>(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<SourceTracker> ?? []
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ public extension SourceSeedLeech {
|
||||||
@NSManaged var seederRegex: String?
|
@NSManaged var seederRegex: String?
|
||||||
@NSManaged var seeders: String?
|
@NSManaged var seeders: String?
|
||||||
@NSManaged var attribute: String
|
@NSManaged var attribute: String
|
||||||
|
@NSManaged var lookupAttribute: String?
|
||||||
@NSManaged var parentParser: SourceHtmlParser?
|
@NSManaged var parentParser: SourceHtmlParser?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<SourceTracker> {
|
||||||
|
return NSFetchRequest<SourceTracker>(entityName: "SourceTracker")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var urlString: String
|
||||||
|
@NSManaged public var parentRssParser: SourceRssParser?
|
||||||
|
@NSManaged public var parentHtmlParser: SourceHtmlParser?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SourceTracker : Identifiable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,48 +1,80 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21271" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21271" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
|
<attribute name="author" optional="YES" attributeType="String"/>
|
||||||
<attribute name="baseUrl" attributeType="String" defaultValueString=""/>
|
<attribute name="baseUrl" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||||
|
<attribute name="preferredParser" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="version" attributeType="String" defaultValueString=""/>
|
<attribute name="version" attributeType="String" defaultValueString=""/>
|
||||||
<relationship name="htmlParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceHtmlParser" inverseName="parentSource" inverseEntity="SourceHtmlParser"/>
|
<relationship name="htmlParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceHtmlParser" inverseName="parentSource" inverseEntity="SourceHtmlParser"/>
|
||||||
|
<relationship name="rssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="parentSource" inverseEntity="SourceRssParser"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SourceComplexQuery" representedClassName="SourceComplexQuery" isAbstract="YES" syncable="YES">
|
<entity name="SourceComplexQuery" representedClassName="SourceComplexQuery" isAbstract="YES" syncable="YES">
|
||||||
<attribute name="attribute" attributeType="String" defaultValueString=""/>
|
<attribute name="attribute" attributeType="String" defaultValueString="text"/>
|
||||||
|
<attribute name="lookupAttribute" optional="YES" attributeType="String"/>
|
||||||
<attribute name="query" attributeType="String" defaultValueString=""/>
|
<attribute name="query" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="regex" optional="YES" attributeType="String"/>
|
<attribute name="regex" optional="YES" attributeType="String"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SourceHtmlParser" representedClassName="SourceHtmlParser" syncable="YES">
|
<entity name="SourceHtmlParser" representedClassName="SourceHtmlParser" syncable="YES">
|
||||||
<attribute name="rows" attributeType="String" defaultValueString=""/>
|
<attribute name="rows" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="searchUrl" attributeType="String" defaultValueString=""/>
|
<attribute name="searchUrl" attributeType="String" defaultValueString=""/>
|
||||||
<relationship name="magnet" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnet" inverseName="parentParser" inverseEntity="SourceMagnet"/>
|
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentHtmlParser" inverseEntity="SourceMagnetHash"/>
|
||||||
|
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentHtmlParser" inverseEntity="SourceMagnetLink"/>
|
||||||
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="htmlParser" inverseEntity="Source"/>
|
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="htmlParser" inverseEntity="Source"/>
|
||||||
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceSeedLeech" inverseName="parentParser" inverseEntity="SourceSeedLeech"/>
|
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceSeedLeech" inverseName="parentHtmlParser" inverseEntity="SourceSeedLeech"/>
|
||||||
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentParser" inverseEntity="SourceSize"/>
|
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentHtmlParser" inverseEntity="SourceSize"/>
|
||||||
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentParser" inverseEntity="SourceTitle"/>
|
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentHtmlParser" inverseEntity="SourceTitle"/>
|
||||||
|
<relationship name="trackers" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="SourceTracker" inverseName="parentHtmlParser" inverseEntity="SourceTracker"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SourceList" representedClassName="SourceList" syncable="YES">
|
<entity name="SourceList" representedClassName="SourceList" syncable="YES">
|
||||||
<attribute name="repoAuthor" optional="YES" attributeType="String"/>
|
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="repoName" optional="YES" attributeType="String"/>
|
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="urlString" attributeType="String" defaultValueString=""/>
|
<attribute name="urlString" attributeType="String" defaultValueString=""/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SourceMagnet" representedClassName="SourceMagnet" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
<entity name="SourceMagnetHash" representedClassName="SourceMagnetHash" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||||
|
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="magnetHash" inverseEntity="SourceHtmlParser"/>
|
||||||
|
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="magnetHash" inverseEntity="SourceRssParser"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="SourceMagnetLink" representedClassName="SourceMagnetLink" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||||
<attribute name="externalLinkQuery" optional="YES" attributeType="String"/>
|
<attribute name="externalLinkQuery" optional="YES" attributeType="String"/>
|
||||||
<relationship name="parentParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="magnet" inverseEntity="SourceHtmlParser"/>
|
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="magnetLink" inverseEntity="SourceHtmlParser"/>
|
||||||
|
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="magnetLink" inverseEntity="SourceRssParser"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="SourceRssParser" representedClassName="SourceRssParser" syncable="YES">
|
||||||
|
<attribute name="items" attributeType="String" defaultValueString=""/>
|
||||||
|
<attribute name="rssUrl" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="searchUrl" attributeType="String" defaultValueString=""/>
|
||||||
|
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentRssParser" inverseEntity="SourceMagnetHash"/>
|
||||||
|
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentRssParser" inverseEntity="SourceMagnetLink"/>
|
||||||
|
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="rssParser" inverseEntity="Source"/>
|
||||||
|
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSeedLeech" inverseName="parentRssParser" inverseEntity="SourceSeedLeech"/>
|
||||||
|
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentRssParser" inverseEntity="SourceSize"/>
|
||||||
|
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentRssParser" inverseEntity="SourceTitle"/>
|
||||||
|
<relationship name="trackers" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="SourceTracker" inverseName="parentRssParser" inverseEntity="SourceTracker"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SourceSeedLeech" representedClassName="SourceSeedLeech" syncable="YES">
|
<entity name="SourceSeedLeech" representedClassName="SourceSeedLeech" syncable="YES">
|
||||||
<attribute name="attribute" attributeType="String" defaultValueString=""/>
|
<attribute name="attribute" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="combined" optional="YES" attributeType="String"/>
|
<attribute name="combined" optional="YES" attributeType="String"/>
|
||||||
<attribute name="leecherRegex" optional="YES" attributeType="String"/>
|
<attribute name="leecherRegex" optional="YES" attributeType="String"/>
|
||||||
<attribute name="leechers" optional="YES" attributeType="String"/>
|
<attribute name="leechers" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="lookupAttribute" optional="YES" attributeType="String"/>
|
||||||
<attribute name="seederRegex" optional="YES" attributeType="String"/>
|
<attribute name="seederRegex" optional="YES" attributeType="String"/>
|
||||||
<attribute name="seeders" optional="YES" attributeType="String"/>
|
<attribute name="seeders" optional="YES" attributeType="String"/>
|
||||||
<relationship name="parentParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="seedLeech" inverseEntity="SourceHtmlParser"/>
|
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="seedLeech" inverseEntity="SourceHtmlParser"/>
|
||||||
|
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="seedLeech" inverseEntity="SourceRssParser"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SourceSize" representedClassName="SourceSize" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
<entity name="SourceSize" representedClassName="SourceSize" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||||
<relationship name="parentParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="size" inverseEntity="SourceHtmlParser"/>
|
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="size" inverseEntity="SourceHtmlParser"/>
|
||||||
|
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="size" inverseEntity="SourceRssParser"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="SourceTitle" representedClassName="SourceTitle" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
<entity name="SourceTitle" representedClassName="SourceTitle" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
|
||||||
<relationship name="parentParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="title" inverseEntity="SourceHtmlParser"/>
|
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="title" inverseEntity="SourceHtmlParser"/>
|
||||||
|
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="title" inverseEntity="SourceRssParser"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="SourceTracker" representedClassName="SourceTracker" syncable="YES">
|
||||||
|
<attribute name="urlString" attributeType="String" defaultValueString=""/>
|
||||||
|
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="trackers" inverseEntity="SourceHtmlParser"/>
|
||||||
|
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="trackers" inverseEntity="SourceRssParser"/>
|
||||||
</entity>
|
</entity>
|
||||||
</model>
|
</model>
|
||||||
|
|
@ -8,36 +8,52 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct SourceListJson: Codable {
|
public struct SourceListJson: Codable {
|
||||||
let repoName: String?
|
let name: String
|
||||||
let repoAuthor: String?
|
let author: String
|
||||||
let sources: [SourceJson]
|
var sources: [SourceJson]
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case repoName = "name"
|
|
||||||
case repoAuthor = "author"
|
|
||||||
case sources
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SourceJson: Codable, Hashable {
|
public struct SourceJson: Codable, Hashable {
|
||||||
let name: String
|
let name: String
|
||||||
let version: String
|
let version: String
|
||||||
let baseUrl: String
|
let baseUrl: String
|
||||||
|
var author: String?
|
||||||
|
let rssParser: SourceRssParserJson?
|
||||||
let htmlParser: SourceHtmlParserJson?
|
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 {
|
public struct SourceHtmlParserJson: Codable, Hashable {
|
||||||
let searchUrl: String
|
let searchUrl: String
|
||||||
let rows: String
|
let rows: String
|
||||||
let magnet: SourceMagnetJson
|
let magnet: SourceMagnetJson
|
||||||
let title: SouceComplexQuery?
|
let title: SouceComplexQueryJson?
|
||||||
let size: SouceComplexQuery?
|
let size: SouceComplexQueryJson?
|
||||||
let sl: SourceSLJson?
|
let sl: SourceSLJson?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SouceComplexQuery: Codable, Hashable {
|
public struct SouceComplexQueryJson: Codable, Hashable {
|
||||||
let query: String
|
let query: String
|
||||||
let attribute: String
|
let lookupAttribute: String?
|
||||||
|
let attribute: String?
|
||||||
let regex: String?
|
let regex: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +68,8 @@ public struct SourceSLJson: Codable, Hashable {
|
||||||
let seeders: String?
|
let seeders: String?
|
||||||
let leechers: String?
|
let leechers: String?
|
||||||
let combined: String?
|
let combined: String?
|
||||||
let attribute: String
|
let attribute: String?
|
||||||
|
let lookupAttribute: String?
|
||||||
let seederRegex: String?
|
let seederRegex: String?
|
||||||
let leecherRegex: String?
|
let leecherRegex: String?
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class NavigationViewModel: ObservableObject {
|
class NavigationViewModel: ObservableObject {
|
||||||
|
// Used between SearchResultsView and MagnetChoiceView
|
||||||
enum ChoiceSheetType: Identifiable {
|
enum ChoiceSheetType: Identifiable {
|
||||||
var id: Int {
|
var id: Int {
|
||||||
hashValue
|
hashValue
|
||||||
|
|
@ -18,4 +19,8 @@ class NavigationViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var currentChoiceSheet: ChoiceSheetType?
|
@Published var currentChoiceSheet: ChoiceSheetType?
|
||||||
|
|
||||||
|
// Used between SourceListView and SourceSettingsView
|
||||||
|
@Published var showSourceSettings: Bool = false
|
||||||
|
@Published var selectedSource: Source?
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
|
|
||||||
// Link the toast view model for single-directional communication
|
// Link the toast view model for single-directional communication
|
||||||
var toastModel: ToastViewModel?
|
var toastModel: ToastViewModel?
|
||||||
|
let byteCountFormatter: ByteCountFormatter = .init()
|
||||||
|
|
||||||
@Published var searchResults: [SearchResult] = []
|
@Published var searchResults: [SearchResult] = []
|
||||||
@Published var searchText: String = ""
|
@Published var searchText: String = ""
|
||||||
|
|
@ -42,22 +43,56 @@ class ScrapingViewModel: ObservableObject {
|
||||||
|
|
||||||
for source in sources {
|
for source in sources {
|
||||||
if source.enabled {
|
if source.enabled {
|
||||||
if let htmlParser = source.htmlParser {
|
// Default to HTML scraping
|
||||||
guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
let preferredParser = SourcePreferredParser(rawValue: source.preferredParser) ?? .none
|
||||||
toastModel?.toastDescription = "Could not process search query, invalid characters present."
|
|
||||||
print("Could not process search query, invalid characters present")
|
|
||||||
|
|
||||||
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 {
|
let replacedSearchUrl = rssParser.searchUrl.replacingOccurrences(of: "{query}", with: encodedQuery)
|
||||||
continue
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
case .siteApi, .none:
|
||||||
let sourceResults = await scrapeWebsite(source: source, html: html)
|
continue
|
||||||
tempResults += sourceResults
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,9 +100,9 @@ class ScrapingViewModel: ObservableObject {
|
||||||
searchResults = tempResults
|
searchResults = tempResults
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the HTML for a URL
|
// Fetches the data for a URL
|
||||||
@MainActor
|
@MainActor
|
||||||
public func fetchWebsiteHtml(urlString: String) async -> String? {
|
public func fetchWebsiteData(urlString: String) async -> String? {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
toastModel?.toastDescription = "Source doesn't contain a valid URL, contact the source dev!"
|
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!")
|
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)
|
let html = String(data: data, encoding: .ascii)
|
||||||
return html
|
return html
|
||||||
} catch {
|
} catch {
|
||||||
toastModel?.toastDescription = "Error in fetching HTML \(error)"
|
toastModel?.toastDescription = "Error in fetching data \(error)"
|
||||||
print("Error in fetching HTML \(error)")
|
print("Error in fetching data \(error)")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns results to UI
|
// RSS feed scraper
|
||||||
// Results must have a link and title, but other parameters aren't required
|
|
||||||
@MainActor
|
@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 {
|
guard let htmlParser = source.htmlParser else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +272,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
// Fetches the magnet link
|
// Fetches the magnet link
|
||||||
// If the magnet is located on an external page, fetch the external page and grab 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
|
// External page fetching affects source performance
|
||||||
guard let magnetParser = htmlParser.magnet else {
|
guard let magnetParser = htmlParser.magnetLink else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,7 +282,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let magnetHtml = await fetchWebsiteHtml(urlString: source.baseUrl + externalMagnetLink) else {
|
guard let magnetHtml = await fetchWebsiteData(urlString: source.baseUrl + externalMagnetLink) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +297,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
href = try linkResult.attr(magnetParser.attribute)
|
href = try linkResult.attr(magnetParser.attribute)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
guard let link = try runComplexQuery(
|
guard let link = try runHtmlComplexQuery(
|
||||||
row: row,
|
row: row,
|
||||||
query: magnetParser.query,
|
query: magnetParser.query,
|
||||||
attribute: magnetParser.attribute,
|
attribute: magnetParser.attribute,
|
||||||
|
|
@ -162,7 +319,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
// Fetches the episode/movie title
|
// Fetches the episode/movie title
|
||||||
var title: String?
|
var title: String?
|
||||||
if let titleParser = htmlParser.title {
|
if let titleParser = htmlParser.title {
|
||||||
title = try? runComplexQuery(
|
title = try? runHtmlComplexQuery(
|
||||||
row: row,
|
row: row,
|
||||||
query: titleParser.query,
|
query: titleParser.query,
|
||||||
attribute: titleParser.attribute,
|
attribute: titleParser.attribute,
|
||||||
|
|
@ -171,9 +328,10 @@ class ScrapingViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the torrent's size
|
// Fetches the torrent's size
|
||||||
|
// TODO: Add int translation
|
||||||
var size: String?
|
var size: String?
|
||||||
if let sizeParser = htmlParser.size {
|
if let sizeParser = htmlParser.size {
|
||||||
size = try? runComplexQuery(
|
size = try? runHtmlComplexQuery(
|
||||||
row: row,
|
row: row,
|
||||||
query: sizeParser.query,
|
query: sizeParser.query,
|
||||||
attribute: sizeParser.attribute,
|
attribute: sizeParser.attribute,
|
||||||
|
|
@ -186,7 +344,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
var leechers: String?
|
var leechers: String?
|
||||||
if let seederLeecher = htmlParser.seedLeech {
|
if let seederLeecher = htmlParser.seedLeech {
|
||||||
if let combinedQuery = seederLeecher.combined {
|
if let combinedQuery = seederLeecher.combined {
|
||||||
if let combinedString = try? runComplexQuery(
|
if let combinedString = try? runHtmlComplexQuery(
|
||||||
row: row,
|
row: row,
|
||||||
query: combinedQuery,
|
query: combinedQuery,
|
||||||
attribute: seederLeecher.attribute,
|
attribute: seederLeecher.attribute,
|
||||||
|
|
@ -202,7 +360,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let seederQuery = seederLeecher.seeders {
|
if let seederQuery = seederLeecher.seeders {
|
||||||
seeders = try? runComplexQuery(
|
seeders = try? runHtmlComplexQuery(
|
||||||
row: row,
|
row: row,
|
||||||
query: seederQuery,
|
query: seederQuery,
|
||||||
attribute: seederLeecher.attribute,
|
attribute: seederLeecher.attribute,
|
||||||
|
|
@ -211,7 +369,7 @@ class ScrapingViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let leecherQuery = seederLeecher.seeders {
|
if let leecherQuery = seederLeecher.seeders {
|
||||||
leechers = try? runComplexQuery(
|
leechers = try? runHtmlComplexQuery(
|
||||||
row: row,
|
row: row,
|
||||||
query: leecherQuery,
|
query: leecherQuery,
|
||||||
attribute: seederLeecher.attribute,
|
attribute: seederLeecher.attribute,
|
||||||
|
|
@ -243,7 +401,8 @@ class ScrapingViewModel: ObservableObject {
|
||||||
return tempResults
|
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?
|
var parsedValue: String?
|
||||||
|
|
||||||
let result = try row.select(query).first()
|
let result = try row.select(query).first()
|
||||||
|
|
@ -258,7 +417,36 @@ class ScrapingViewModel: ObservableObject {
|
||||||
// A capture group must be used in the provided regex
|
// A capture group must be used in the provided regex
|
||||||
if let regexString = regexString,
|
if let regexString = regexString,
|
||||||
let parsedValue = parsedValue,
|
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
|
return regexValue
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -284,4 +472,46 @@ class ScrapingViewModel: ObservableObject {
|
||||||
return String(magnetHash).lowercased()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,22 @@ public class SourceManager: ObservableObject {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public func fetchSourcesFromUrl() async {
|
public func fetchSourcesFromUrl() async {
|
||||||
let sourceUrlRequest = SourceList.fetchRequest()
|
let sourceListRequest = SourceList.fetchRequest()
|
||||||
do {
|
do {
|
||||||
let sourceUrls = try PersistenceController.shared.backgroundContext.fetch(sourceUrlRequest)
|
let sourceLists = try PersistenceController.shared.backgroundContext.fetch(sourceListRequest)
|
||||||
var tempSourceUrls: [SourceJson] = []
|
var tempSourceUrls: [SourceJson] = []
|
||||||
|
|
||||||
for sourceUrl in sourceUrls {
|
for sourceList in sourceLists {
|
||||||
guard let url = URL(string: sourceUrl.urlString) else {
|
guard let url = URL(string: sourceList.urlString) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
|
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
|
tempSourceUrls += sourceResponse.sources
|
||||||
}
|
}
|
||||||
|
|
@ -61,55 +65,23 @@ public class SourceManager: ObservableObject {
|
||||||
newSource.name = sourceJson.name
|
newSource.name = sourceJson.name
|
||||||
newSource.version = sourceJson.version
|
newSource.version = sourceJson.version
|
||||||
newSource.baseUrl = sourceJson.baseUrl
|
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
|
// Adds an HTML parser if present
|
||||||
if let htmlParserJson = sourceJson.htmlParser {
|
if let htmlParserJson = sourceJson.htmlParser {
|
||||||
let newSourceHtmlParser = SourceHtmlParser(context: backgroundContext)
|
addHtmlParser(newSource: newSource, htmlParserJson: htmlParserJson)
|
||||||
newSourceHtmlParser.searchUrl = htmlParserJson.searchUrl
|
}
|
||||||
newSourceHtmlParser.rows = htmlParserJson.rows
|
|
||||||
|
|
||||||
// Adds a title complex query if present
|
// Add an API condition as well
|
||||||
if let titleJson = htmlParserJson.title {
|
if newSource.rssParser != nil {
|
||||||
let newSourceTitle = SourceTitle(context: backgroundContext)
|
newSource.preferredParser = Int16(SourcePreferredParser.rss.rawValue)
|
||||||
newSourceTitle.query = titleJson.query
|
} else {
|
||||||
newSourceTitle.attribute = titleJson.attribute
|
newSource.preferredParser = Int16(SourcePreferredParser.scraping.rawValue)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newSource.enabled = true
|
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
|
@MainActor
|
||||||
public func addSourceList(sourceUrl: String) async -> Bool {
|
public func addSourceList(sourceUrl: String) async -> Bool {
|
||||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||||
|
|
@ -134,24 +225,25 @@ public class SourceManager: ObservableObject {
|
||||||
return false
|
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 {
|
do {
|
||||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
|
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
|
||||||
if let rawResponse = try? JSONDecoder().decode(SourceListJson.self, from: data) {
|
let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
||||||
newSourceUrl.repoName = rawResponse.repoName
|
|
||||||
|
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()
|
try backgroundContext.save()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,10 @@ struct ContentView: View {
|
||||||
.onSubmit(of: .search) {
|
.onSubmit(of: .search) {
|
||||||
Task {
|
Task {
|
||||||
await scrapingModel.scanSources(sources: sources.compactMap { $0 })
|
await scrapingModel.scanSources(sources: sources.compactMap { $0 })
|
||||||
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
|
||||||
|
if realDebridEnabled {
|
||||||
|
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Search")
|
.navigationTitle("Search")
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ struct SettingsSourceListView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
ForEach(sourceUrls, id: \.self) { sourceUrl in
|
ForEach(sourceUrls, id: \.self) { sourceUrl in
|
||||||
Text(sourceUrl.repoName ?? "Unknown repo")
|
Text(sourceUrl.name)
|
||||||
}
|
}
|
||||||
.onDelete { offsets in
|
.onDelete { offsets in
|
||||||
for index in offsets {
|
for index in offsets {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import SwiftUI
|
||||||
|
|
||||||
struct SourceListView: View {
|
struct SourceListView: View {
|
||||||
@EnvironmentObject var sourceManager: SourceManager
|
@EnvironmentObject var sourceManager: SourceManager
|
||||||
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
|
|
||||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||||
|
|
||||||
|
|
@ -32,15 +33,36 @@ struct SourceListView: View {
|
||||||
PersistenceController.shared.save()
|
PersistenceController.shared.save()
|
||||||
}
|
}
|
||||||
)) {
|
)) {
|
||||||
Text(source.name)
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
}
|
HStack {
|
||||||
}
|
Text(source.name)
|
||||||
.onDelete { offsets in
|
Text("v\(source.version)")
|
||||||
for index in offsets {
|
.foregroundColor(.secondary)
|
||||||
if let source = sources[safe: index] {
|
}
|
||||||
PersistenceController.shared.delete(source, context: backgroundContext)
|
|
||||||
|
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
|
ForEach(sourceManager.availableSources, id: \.self) { availableSource in
|
||||||
if !sources.contains(where: { availableSource.name == $0.name }) {
|
if !sources.contains(where: { availableSource.name == $0.name }) {
|
||||||
HStack {
|
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()
|
Spacer()
|
||||||
|
|
||||||
|
|
|
||||||
56
Ferrite/Views/SourceSettingsView.swift
Normal file
56
Ferrite/Views/SourceSettingsView.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue