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 */; };
|
||||
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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -103,6 +111,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
/* 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 */,
|
||||
|
|
|
|||
|
|
@ -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<Source> {
|
||||
NSFetchRequest<Source>(entityName: "Source")
|
||||
|
||||
extension Source {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Source> {
|
||||
return NSFetchRequest<Source>(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 {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public extension SourceComplexQuery {
|
|||
}
|
||||
|
||||
@NSManaged var attribute: String
|
||||
@NSManaged var lookupAttribute: String?
|
||||
@NSManaged var query: String
|
||||
@NSManaged var regex: String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SourceHtmlParser> {
|
||||
NSFetchRequest<SourceHtmlParser>(entityName: "SourceHtmlParser")
|
||||
|
||||
extension SourceHtmlParser {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<SourceHtmlParser> {
|
||||
return NSFetchRequest<SourceHtmlParser>(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 {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ public extension SourceList {
|
|||
NSFetchRequest<SourceList>(entityName: "SourceList")
|
||||
}
|
||||
|
||||
@NSManaged var repoAuthor: String?
|
||||
@NSManaged var repoName: String?
|
||||
@NSManaged var author: String
|
||||
@NSManaged var name: 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 seeders: String?
|
||||
@NSManaged var attribute: String
|
||||
@NSManaged var lookupAttribute: String?
|
||||
@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"?>
|
||||
<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">
|
||||
<attribute name="author" optional="YES" attributeType="String"/>
|
||||
<attribute name="baseUrl" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="preferredParser" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="version" attributeType="String" defaultValueString=""/>
|
||||
<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 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="regex" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="SourceHtmlParser" representedClassName="SourceHtmlParser" syncable="YES">
|
||||
<attribute name="rows" 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="seedLeech" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceSeedLeech" inverseName="parentParser" inverseEntity="SourceSeedLeech"/>
|
||||
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentParser" inverseEntity="SourceSize"/>
|
||||
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentParser" inverseEntity="SourceTitle"/>
|
||||
<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="parentHtmlParser" inverseEntity="SourceSize"/>
|
||||
<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 name="SourceList" representedClassName="SourceList" syncable="YES">
|
||||
<attribute name="repoAuthor" optional="YES" attributeType="String"/>
|
||||
<attribute name="repoName" optional="YES" attributeType="String"/>
|
||||
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="urlString" attributeType="String" defaultValueString=""/>
|
||||
</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"/>
|
||||
<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 name="SourceSeedLeech" representedClassName="SourceSeedLeech" syncable="YES">
|
||||
<attribute name="attribute" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="combined" optional="YES" attributeType="String"/>
|
||||
<attribute name="leecherRegex" 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="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 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 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>
|
||||
</model>
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
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