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:
kingbri 2022-08-04 21:23:47 -04:00
parent 9ea7ab7b11
commit efecfa3236
19 changed files with 787 additions and 154 deletions

View file

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

View file

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

View file

@ -15,6 +15,7 @@ public extension SourceComplexQuery {
}
@NSManaged var attribute: String
@NSManaged var lookupAttribute: String?
@NSManaged var query: String
@NSManaged var regex: String?
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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