Ferrite: Overhaul sources

Sources are now completely changed to use a more flexible API. This
uses a fully native source system, so there will be 0 overhead on
resource usage and performance.

JSON objects specify what is fetched and displayed by Ferrite when
searching torrents.

Sources now include sizes, seeders, and leechers for any site that
specifies them.

The versioning and repo naming framework has been added, but will be
displayed in another update.

API support will be included in another update.

Signed-off-by: kingbri <bdashore3@gmail.com>
This commit is contained in:
kingbri 2022-08-02 16:17:41 -04:00
parent 64798a172a
commit 940f8337a5
27 changed files with 617 additions and 198 deletions

View file

@ -7,16 +7,26 @@
objects = {
/* Begin PBXBuildFile section */
0C0D50E1288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50DF288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift */; };
0C0D50E2288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift */; };
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourceListView.swift */; };
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; };
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; };
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 */; };
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 */; };
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */; };
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */; };
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */; };
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */; };
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */; };
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */; };
0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */ = {isa = PBXBuildFile; productRef = 0C90E32B2888E5D000C0BC89 /* ActivityView */; };
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */; };
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* SourceManager.swift */; };
@ -44,22 +54,28 @@
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
0CBC7702288DE4400054BE44 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */; };
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
0CE37ABA288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */; };
0CE37ABB288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */; };
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
0C0D50DF288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSource+CoreDataClass.swift"; sourceTree = "<group>"; };
0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSource+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
0C0D50E6288DFF850035ECC8 /* SourceListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListView.swift; sourceTree = "<group>"; };
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = "<group>"; };
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.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>"; };
0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataClass.swift"; sourceTree = "<group>"; };
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataClass.swift"; sourceTree = "<group>"; };
0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataProperties.swift"; sourceTree = "<group>"; };
0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSourceUrlView.swift; sourceTree = "<group>"; };
0CA05458288EE9E600850554 /* SourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManager.swift; sourceTree = "<group>"; };
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListEditorView.swift; sourceTree = "<group>"; };
@ -86,10 +102,7 @@
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = "<group>"; };
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSourceUrl+CoreDataClass.swift"; sourceTree = "<group>"; };
0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSourceUrl+CoreDataProperties.swift"; sourceTree = "<group>"; };
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -100,6 +113,7 @@
files = (
0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */,
0C64A4B4288903680079976D /* Base32 in Frameworks */,
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
);
@ -111,10 +125,16 @@
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
isa = PBXGroup;
children = (
0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */,
0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */,
0C0D50DF288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift */,
0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift */,
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */,
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */,
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */,
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */,
0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */,
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */,
0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */,
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */,
0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */,
0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */,
);
path = Classes;
sourceTree = "<group>";
@ -252,7 +272,7 @@
children = (
0C0D50DE288DF72D0035ECC8 /* Classes */,
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */,
0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */,
0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */,
);
path = DataManagement;
sourceTree = "<group>";
@ -278,6 +298,7 @@
0C90E32B2888E5D000C0BC89 /* ActivityView */,
0C64A4B3288903680079976D /* Base32 */,
0C64A4B6288903880079976D /* KeychainSwift */,
0C4CFC452897030D00AD9FAD /* Regex */,
);
productName = Torrenter;
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
@ -312,6 +333,7 @@
0C90E32A2888E5D000C0BC89 /* XCRemoteSwiftPackageReference "ActivityView" */,
0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */,
0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */,
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */,
);
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
projectDirPath = "";
@ -339,41 +361,47 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0CE37ABB288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift in Sources */,
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
0CA148DB288903F000DE2211 /* NavView.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 */,
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */,
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */,
0CE37ABA288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift in Sources */,
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
0C0D50E1288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift in Sources */,
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */,
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
0CBC7702288DE4400054BE44 /* FerriteDB.xcdatamodeld in Sources */,
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
0CA148D9288903F000DE2211 /* CardView.swift in Sources */,
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */,
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */,
0C0D50E2288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift in Sources */,
0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -581,6 +609,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/Regex";
requirement = {
branch = main;
kind = branch;
};
};
0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/norio-nomura/Base32";
@ -616,6 +652,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
0C4CFC452897030D00AD9FAD /* Regex */ = {
isa = XCSwiftPackageProductDependency;
package = 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */;
productName = Regex;
};
0C64A4B3288903680079976D /* Base32 */ = {
isa = XCSwiftPackageProductDependency;
package = 0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */;
@ -639,12 +680,12 @@
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */ = {
0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */,
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */,
);
currentVersion = 0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */;
currentVersion = 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */;
path = FerriteDB.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View file

@ -0,0 +1,13 @@
//
// Source+CoreDataClass.swift
// Ferrite
//
// Created by Brian Dashore on 7/30/22.
//
//
import CoreData
import Foundation
@objc(Source)
public class Source: NSManagedObject {}

View file

@ -0,0 +1,24 @@
//
// Source+CoreDataProperties.swift
// Ferrite
//
// Created by Brian Dashore on 7/30/22.
//
//
import CoreData
import Foundation
public extension Source {
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> {
NSFetchRequest<Source>(entityName: "Source")
}
@NSManaged var name: String
@NSManaged var enabled: Bool
@NSManaged var version: String
@NSManaged var baseUrl: String
@NSManaged var htmlParser: SourceHtmlParser?
}
extension Source: Identifiable {}

View file

@ -0,0 +1,13 @@
//
// SourceComplexQuery+CoreDataClass.swift
// Ferrite
//
// Created by Brian Dashore on 7/31/22.
//
//
import CoreData
import Foundation
@objc(SourceComplexQuery)
public class SourceComplexQuery: NSManagedObject {}

View file

@ -0,0 +1,22 @@
//
// SourceComplexQuery+CoreDataProperties.swift
// Ferrite
//
// Created by Brian Dashore on 7/31/22.
//
//
import CoreData
import Foundation
public extension SourceComplexQuery {
@nonobjc class func fetchRequest() -> NSFetchRequest<SourceComplexQuery> {
NSFetchRequest<SourceComplexQuery>(entityName: "SourceComplexQuery")
}
@NSManaged var attribute: String
@NSManaged var query: String
@NSManaged var regex: String?
}
extension SourceComplexQuery: Identifiable {}

View file

@ -0,0 +1,13 @@
//
// SourceHtmlParser+CoreDataClass.swift
// Ferrite
//
// Created by Brian Dashore on 7/30/22.
//
//
import CoreData
import Foundation
@objc(SourceHtmlParser)
public class SourceHtmlParser: NSManagedObject {}

View file

@ -0,0 +1,26 @@
//
// SourceHtmlParser+CoreDataProperties.swift
// Ferrite
//
// Created by Brian Dashore on 8/2/22.
//
//
import CoreData
import Foundation
public extension SourceHtmlParser {
@nonobjc class func fetchRequest() -> NSFetchRequest<SourceHtmlParser> {
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?
}
extension SourceHtmlParser: Identifiable {}

View file

@ -0,0 +1,13 @@
//
// SourceList+CoreDataClass.swift
// Ferrite
//
// Created by Brian Dashore on 7/30/22.
//
//
import CoreData
import Foundation
@objc(SourceList)
public class SourceList: NSManagedObject {}

View file

@ -0,0 +1,22 @@
//
// SourceList+CoreDataProperties.swift
// Ferrite
//
// Created by Brian Dashore on 7/30/22.
//
//
import CoreData
import Foundation
public extension SourceList {
@nonobjc class func fetchRequest() -> NSFetchRequest<SourceList> {
NSFetchRequest<SourceList>(entityName: "SourceList")
}
@NSManaged var repoAuthor: String?
@NSManaged var repoName: String?
@NSManaged var urlString: String
}
extension SourceList: Identifiable {}

View file

@ -0,0 +1,13 @@
//
// SourceSeedLeech+CoreDataClass.swift
// Ferrite
//
// Created by Brian Dashore on 8/2/22.
//
//
import CoreData
import Foundation
@objc(SourceSeedLeech)
public class SourceSeedLeech: NSManagedObject {}

View file

@ -0,0 +1,26 @@
//
// SourceSeedLeech+CoreDataProperties.swift
// Ferrite
//
// Created by Brian Dashore on 8/2/22.
//
//
import CoreData
import Foundation
public extension SourceSeedLeech {
@nonobjc class func fetchRequest() -> NSFetchRequest<SourceSeedLeech> {
NSFetchRequest<SourceSeedLeech>(entityName: "SourceSeedLeech")
}
@NSManaged var combined: String?
@NSManaged var leecherRegex: String?
@NSManaged var leechers: String?
@NSManaged var seederRegex: String?
@NSManaged var seeders: String?
@NSManaged var attribute: String
@NSManaged var parentParser: SourceHtmlParser?
}
extension SourceSeedLeech: Identifiable {}

View file

@ -1,13 +0,0 @@
//
// TorrentSource+CoreDataClass.swift
// Ferrite
//
// Created by Brian Dashore on 7/24/22.
//
//
import CoreData
import Foundation
@objc(TorrentSource)
public class TorrentSource: NSManagedObject {}

View file

@ -1,26 +0,0 @@
//
// TorrentSource+CoreDataProperties.swift
// Ferrite
//
// Created by Brian Dashore on 7/24/22.
//
//
import CoreData
import Foundation
public extension TorrentSource {
@nonobjc class func fetchRequest() -> NSFetchRequest<TorrentSource> {
NSFetchRequest<TorrentSource>(entityName: "TorrentSource")
}
@NSManaged var enabled: Bool
@NSManaged var linkQuery: String
@NSManaged var name: String?
@NSManaged var rowQuery: String
@NSManaged var sizeQuery: String?
@NSManaged var titleQuery: String?
@NSManaged var url: String
}
extension TorrentSource: Identifiable {}

View file

@ -1,13 +0,0 @@
//
// TorrentSourceUrl+CoreDataClass.swift
// Ferrite
//
// Created by Brian Dashore on 7/24/22.
//
//
import CoreData
import Foundation
@objc(TorrentSourceUrl)
public class TorrentSourceUrl: NSManagedObject {}

View file

@ -1,22 +0,0 @@
//
// TorrentSourceUrl+CoreDataProperties.swift
// Ferrite
//
// Created by Brian Dashore on 7/25/22.
//
//
import CoreData
import Foundation
public extension TorrentSourceUrl {
@nonobjc class func fetchRequest() -> NSFetchRequest<TorrentSourceUrl> {
NSFetchRequest<TorrentSourceUrl>(entityName: "TorrentSourceUrl")
}
@NSManaged var urlString: String
@NSManaged var repoName: String?
@NSManaged var repoAuthor: String?
}
extension TorrentSourceUrl: Identifiable {}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>FerriteDB.xcdatamodel</string>
</dict>
</plist>

View file

@ -1,17 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21256.2" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="TorrentSource" representedClassName="TorrentSource" syncable="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="baseUrl" attributeType="String" defaultValueString=""/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="linkQuery" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="rowQuery" attributeType="String"/>
<attribute name="sizeQuery" optional="YES" attributeType="String"/>
<attribute name="titleQuery" optional="YES" attributeType="String"/>
<attribute name="url" attributeType="String"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="version" attributeType="String" defaultValueString=""/>
<relationship name="htmlParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceHtmlParser" inverseName="parentSource" inverseEntity="SourceHtmlParser"/>
</entity>
<entity name="TorrentSourceUrl" representedClassName="TorrentSourceUrl" syncable="YES">
<entity name="SourceComplexQuery" representedClassName="SourceComplexQuery" isAbstract="YES" syncable="YES">
<attribute name="attribute" attributeType="String" defaultValueString=""/>
<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="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"/>
</entity>
<entity name="SourceList" representedClassName="SourceList" syncable="YES">
<attribute name="repoAuthor" optional="YES" attributeType="String"/>
<attribute name="repoName" optional="YES" attributeType="String"/>
<attribute name="urlString" attributeType="String"/>
<attribute name="urlString" attributeType="String" defaultValueString=""/>
</entity>
<entity name="SourceMagnet" representedClassName="SourceMagnet" 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"/>
</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="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"/>
</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"/>
</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"/>
</entity>
</model>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21256.2" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="TorrentSource" representedClassName="TorrentSource" syncable="YES">
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="linkQuery" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="rowQuery" attributeType="String"/>
<attribute name="sizeQuery" optional="YES" attributeType="String"/>
<attribute name="titleQuery" optional="YES" attributeType="String"/>
<attribute name="url" attributeType="String"/>
</entity>
<entity name="TorrentSourceUrl" representedClassName="TorrentSourceUrl" syncable="YES">
<attribute name="repoAuthor" optional="YES" attributeType="String"/>
<attribute name="repoName" optional="YES" attributeType="String"/>
<attribute name="urlString" attributeType="String"/>
</entity>
</model>

View file

@ -9,7 +9,7 @@ import CoreData
// No iCloud until finalized sources
struct PersistenceController {
static let shared = PersistenceController()
static var shared = PersistenceController()
// Coredata storage
let container: NSPersistentContainer
@ -40,10 +40,9 @@ struct PersistenceController {
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
try? backgroundContext.setQueryGenerationFrom(.current)
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Error: \(error.localizedDescription)")
fatalError("CoreData init error: \(error)")
}
}
}

View file

@ -7,10 +7,10 @@
import Foundation
public struct SourceJson: Codable {
public struct SourceListJson: Codable {
let repoName: String?
let repoAuthor: String?
let sources: [TorrentSourceJson]
let sources: [SourceJson]
enum CodingKeys: String, CodingKey {
case repoName = "name"
@ -19,11 +19,40 @@ public struct SourceJson: Codable {
}
}
public struct TorrentSourceJson: Codable, Hashable {
let name: String?
let url: String
let rowQuery: String
let linkQuery: String
let titleQuery: String?
let sizeQuery: String?
public struct SourceJson: Codable, Hashable {
let name: String
let version: String
let baseUrl: String
let htmlParser: SourceHtmlParserJson?
}
public struct SourceHtmlParserJson: Codable, Hashable {
let searchUrl: String
let rows: String
let magnet: SourceMagnetJson
let title: SouceComplexQuery?
let size: SouceComplexQuery?
let sl: SourceSLJson?
}
public struct SouceComplexQuery: Codable, Hashable {
let query: String
let attribute: String
let regex: String?
}
public struct SourceMagnetJson: Codable, Hashable {
let query: String
let attribute: String
let regex: String?
let externalLinkQuery: String?
}
public struct SourceSLJson: Codable, Hashable {
let seeders: String?
let leechers: String?
let combined: String?
let attribute: String
let seederRegex: String?
let leecherRegex: String?
}

View file

@ -6,6 +6,7 @@
//
import Base32
import Regex
import SwiftSoup
import SwiftUI
@ -15,6 +16,8 @@ public struct SearchResult: Hashable, Codable {
let size: String
let magnetLink: String
let magnetHash: String?
let seeders: String?
let leechers: String?
}
class ScrapingViewModel: ObservableObject {
@ -24,44 +27,48 @@ class ScrapingViewModel: ObservableObject {
var toastModel: ToastViewModel?
@Published var searchResults: [SearchResult] = []
@Published var debridHashes: [String] = []
@Published var searchText: String = ""
@Published var selectedSearchResult: SearchResult?
@Published var filteredSource: TorrentSource?
@Published var filteredSource: Source?
@MainActor
public func scanSources(sources: [TorrentSource]) async {
public func scanSources(sources: [Source]) async {
if sources.isEmpty {
print("Sources empty")
return
}
var tempResults: [SearchResult] = []
for source in sources {
if source.enabled {
guard let html = await fetchWebsiteHtml(source: source) else {
continue
}
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")
let sourceResults = await scrapeWebsite(source: source, html: html)
tempResults += sourceResults
continue
}
let urlString = source.baseUrl + htmlParser.searchUrl.replacingOccurrences(of: "{query}", with: encodedQuery)
guard let html = await fetchWebsiteHtml(urlString: urlString) else {
continue
}
let sourceResults = await scrapeWebsite(source: source, html: html)
tempResults += sourceResults
}
}
}
searchResults = tempResults
}
// Fetches the HTML body for the source website
// Fetches the HTML for a URL
@MainActor
public func fetchWebsiteHtml(source: TorrentSource) async -> String? {
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")
return nil
}
guard let url = URL(string: source.url + encodedQuery) else {
public func fetchWebsiteHtml(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!")
@ -83,58 +90,179 @@ class ScrapingViewModel: ObservableObject {
// Returns results to UI
// Results must have a link and title, but other parameters aren't required
@MainActor
public func scrapeWebsite(source: TorrentSource, html: String) async -> [SearchResult] {
var tempResults: [SearchResult] = []
var hashes: [String] = []
public func scrapeWebsite(source: Source, html: String) async -> [SearchResult] {
guard let htmlParser = source.htmlParser else {
return []
}
var rows = Elements()
do {
let document = try SwiftSoup.parse(html)
rows = try document.select(htmlParser.rows)
} catch {
toastModel?.toastDescription = "Scraping error, couldn't fetch rows: \(error)"
print("Scraping error, couldn't fetch rows: \(error)")
let rows = try document.select(source.rowQuery)
return []
}
for row in rows {
guard let link = try row.select(source.linkQuery).first() else {
var tempResults: [SearchResult] = []
// If there's an error, continue instead of returning with nothing
for row in rows {
do {
// 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 {
continue
}
let href = try link.attr("href")
var href: String
if let externalMagnetQuery = magnetParser.externalLinkQuery, !externalMagnetQuery.isEmpty {
guard let externalMagnetLink = try row.select(externalMagnetQuery).first()?.attr("href") else {
continue
}
guard let magnetHtml = await fetchWebsiteHtml(urlString: source.baseUrl + externalMagnetLink) else {
continue
}
let magnetDocument = try SwiftSoup.parse(magnetHtml)
guard let linkResult = try magnetDocument.select(magnetParser.query).first() else {
continue
}
if magnetParser.attribute == "text" {
href = try linkResult.text()
} else {
href = try linkResult.attr(magnetParser.attribute)
}
} else {
guard let link = try runComplexQuery(
row: row,
query: magnetParser.query,
attribute: magnetParser.attribute,
regexString: magnetParser.regex
) else {
continue
}
href = link
}
if !href.starts(with: "magnet:") {
continue
}
// Fetches the magnet hash
let magnetHash = fetchMagnetHash(magnetLink: href)
// Fetches the episode/movie title
var title: String?
if let titleQuery = source.titleQuery {
title = try row.select(titleQuery).first()?.text()
if let titleParser = htmlParser.title {
title = try? runComplexQuery(
row: row,
query: titleParser.query,
attribute: titleParser.attribute,
regexString: titleParser.regex
)
}
let size = try row.select(source.sizeQuery ?? "").first()
let sizeText = try size?.text()
// Fetches the torrent's size
var size: String?
if let sizeParser = htmlParser.size {
size = try? runComplexQuery(
row: row,
query: sizeParser.query,
attribute: sizeParser.attribute,
regexString: sizeParser.regex
)
}
// Fetches seeders and leechers if there are any
var seeders: String?
var leechers: String?
if let seederLeecher = htmlParser.seedLeech {
if let combinedQuery = seederLeecher.combined {
if let combinedString = try? runComplexQuery(
row: row,
query: combinedQuery,
attribute: seederLeecher.attribute,
regexString: nil
) {
if let seederRegex = seederLeecher.seederRegex, let leecherRegex = seederLeecher.leecherRegex {
// Seeder regex matching
seeders = try? Regex(seederRegex).firstMatch(in: combinedString)?.groups[safe: 0]?.value
// Leecher regex matching
leechers = try? Regex(leecherRegex).firstMatch(in: combinedString)?.groups[safe: 0]?.value
}
}
} else {
if let seederQuery = seederLeecher.seeders {
seeders = try? runComplexQuery(
row: row,
query: seederQuery,
attribute: seederLeecher.attribute,
regexString: seederLeecher.seederRegex
)
}
if let leecherQuery = seederLeecher.seeders {
leechers = try? runComplexQuery(
row: row,
query: leecherQuery,
attribute: seederLeecher.attribute,
regexString: seederLeecher.leecherRegex
)
}
}
}
let result = SearchResult(
title: title ?? "No title",
source: source.name ?? "N/A",
size: sizeText ?? "?B",
source: source.name,
size: size ?? "",
magnetLink: href,
magnetHash: magnetHash
magnetHash: magnetHash,
seeders: seeders,
leechers: leechers
)
// Change to bulk request to speed up UI
if let hash = magnetHash {
hashes.append(hash)
}
tempResults.append(result)
} catch {
toastModel?.toastDescription = "Scraping error: \(error)"
print("Scraping error: \(error)")
continue
}
}
return tempResults
} catch {
toastModel?.toastDescription = "Error while scraping: \(error)"
print("Error while scraping: \(error)")
return tempResults
}
return []
func runComplexQuery(row: Element, query: String, attribute: String, regexString: String?) throws -> String? {
var parsedValue: String?
let result = try row.select(query).first()
switch attribute {
case "text":
parsedValue = try result?.text()
default:
parsedValue = try result?.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 {
return parsedValue
}
}
@ -153,7 +281,7 @@ class ScrapingViewModel: ObservableObject {
let decryptedMagnetHash = base32DecodeToData(String(magnetHash))
return decryptedMagnetHash?.hexEncodedString()
} else {
return String(magnetHash)
return String(magnetHash).lowercased()
}
}
}

View file

@ -5,22 +5,23 @@
// Created by Brian Dashore on 7/25/22.
//
import CoreData
import Foundation
public class SourceManager: ObservableObject {
var toastModel: ToastViewModel?
@Published var availableSources: [TorrentSourceJson] = []
@Published var availableSources: [SourceJson] = []
@Published var urlErrorAlertText = ""
@Published var showUrlErrorAlert = false
@MainActor
public func fetchSourcesFromUrl() async {
let sourceUrlRequest = TorrentSourceUrl.fetchRequest()
let sourceUrlRequest = SourceList.fetchRequest()
do {
let sourceUrls = try PersistenceController.shared.backgroundContext.fetch(sourceUrlRequest)
var tempSourceUrls: [TorrentSourceJson] = []
var tempSourceUrls: [SourceJson] = []
for sourceUrl in sourceUrls {
guard let url = URL(string: sourceUrl.urlString) else {
@ -28,7 +29,7 @@ public class SourceManager: ObservableObject {
}
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
let sourceResponse = try JSONDecoder().decode(SourceJson.self, from: data)
let sourceResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
tempSourceUrls += sourceResponse.sources
}
@ -39,34 +40,79 @@ public class SourceManager: ObservableObject {
}
}
public func installSource(sourceJson: TorrentSourceJson) {
public func installSource(sourceJson: SourceJson) {
let backgroundContext = PersistenceController.shared.backgroundContext
// If a source exists, don't add the new one
if let name = sourceJson.name {
let existingSourceRequest = TorrentSource.fetchRequest()
existingSourceRequest.predicate = NSPredicate(format: "name == %@", name)
existingSourceRequest.fetchLimit = 1
let existingSourceRequest = Source.fetchRequest()
existingSourceRequest.predicate = NSPredicate(format: "name == %@", sourceJson.name)
existingSourceRequest.fetchLimit = 1
let existingSource = try? backgroundContext.fetch(existingSourceRequest).first
if existingSource != nil {
Task { @MainActor in
toastModel?.toastDescription = "Could not install source \(sourceJson.name ?? "Unknown source") because it is already installed."
}
return
let existingSource = try? backgroundContext.fetch(existingSourceRequest).first
if existingSource != nil {
Task { @MainActor in
toastModel?.toastDescription = "Could not install source with name \(sourceJson.name) because it is already installed."
}
return
}
let newTorrentSource = TorrentSource(context: backgroundContext)
newTorrentSource.name = sourceJson.name
newTorrentSource.url = sourceJson.url
newTorrentSource.rowQuery = sourceJson.rowQuery
newTorrentSource.linkQuery = sourceJson.linkQuery
newTorrentSource.titleQuery = sourceJson.titleQuery
newTorrentSource.sizeQuery = sourceJson.sizeQuery
let newSource = Source(context: backgroundContext)
newSource.name = sourceJson.name
newSource.version = sourceJson.version
newSource.baseUrl = sourceJson.baseUrl
newTorrentSource.enabled = true
// Adds an HTML parser if present
if let htmlParserJson = sourceJson.htmlParser {
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
newSourceTitle.regex = titleJson.regex
newSourceHtmlParser.title = newSourceTitle
}
// Adds a size complex query if present
if let sizeJson = htmlParserJson.size {
let newSourceSize = SourceSize(context: backgroundContext)
newSourceSize.query = sizeJson.query
newSourceSize.attribute = sizeJson.attribute
newSourceSize.regex = sizeJson.regex
newSourceHtmlParser.size = newSourceSize
}
if let seedLeechJson = htmlParserJson.sl {
let newSourceSeedLeech = SourceSeedLeech(context: backgroundContext)
newSourceSeedLeech.seeders = seedLeechJson.seeders
newSourceSeedLeech.leechers = seedLeechJson.leechers
newSourceSeedLeech.combined = seedLeechJson.combined
newSourceSeedLeech.attribute = seedLeechJson.attribute
newSourceSeedLeech.seederRegex = seedLeechJson.seederRegex
newSourceSeedLeech.leecherRegex = seedLeechJson.leecherRegex
newSourceHtmlParser.seedLeech = newSourceSeedLeech
}
// Adds a magnet complex query and its unique properties
let newSourceMagnet = SourceMagnet(context: backgroundContext)
newSourceMagnet.externalLinkQuery = htmlParserJson.magnet.externalLinkQuery
newSourceMagnet.query = htmlParserJson.magnet.query
newSourceMagnet.attribute = htmlParserJson.magnet.attribute
newSourceMagnet.regex = htmlParserJson.magnet.regex
newSourceHtmlParser.magnet = newSourceMagnet
newSource.htmlParser = newSourceHtmlParser
}
newSource.enabled = true
do {
try backgroundContext.save()
@ -88,7 +134,7 @@ public class SourceManager: ObservableObject {
return false
}
let sourceUrlRequest = TorrentSourceUrl.fetchRequest()
let sourceUrlRequest = SourceList.fetchRequest()
sourceUrlRequest.predicate = NSPredicate(format: "urlString == %@", sourceUrl)
sourceUrlRequest.fetchLimit = 1
@ -97,12 +143,12 @@ public class SourceManager: ObservableObject {
PersistenceController.shared.delete(existingSourceUrl, context: backgroundContext)
}
let newSourceUrl = TorrentSourceUrl(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(SourceJson.self, from: data) {
if let rawResponse = try? JSONDecoder().decode(SourceListJson.self, from: data) {
newSourceUrl.repoName = rawResponse.repoName
}

View file

@ -15,11 +15,11 @@ struct ContentView: View {
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
@FetchRequest(
entity: TorrentSource.entity(),
entity: Source.entity(),
sortDescriptors: []
) var sources: FetchedResults<TorrentSource>
) var sources: FetchedResults<Source>
@State private var selectedSource: TorrentSource? {
@State private var selectedSource: Source? {
didSet {
scrapingModel.filteredSource = selectedSource
}

View file

@ -20,6 +20,14 @@ struct SearchResultRDView: View {
Spacer()
if let seeders = result.seeders {
Text("S: \(seeders)")
}
if let leechers = result.leechers {
Text("L: \(leechers)")
}
Text(result.size)
if realDebridEnabled {

View file

@ -9,6 +9,7 @@ import SwiftUI
struct SettingsView: View {
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var sourceManager: SourceManager
let backgroundContext = PersistenceController.shared.backgroundContext

View file

@ -11,9 +11,9 @@ struct SettingsSourceListView: View {
let backgroundContext = PersistenceController.shared.backgroundContext
@FetchRequest(
entity: TorrentSourceUrl.entity(),
entity: SourceList.entity(),
sortDescriptors: []
) var sourceUrls: FetchedResults<TorrentSourceUrl>
) var sourceUrls: FetchedResults<SourceList>
@State private var presentSourceSheet = false

View file

@ -13,9 +13,9 @@ struct SourceListView: View {
let backgroundContext = PersistenceController.shared.backgroundContext
@FetchRequest(
entity: TorrentSource.entity(),
entity: Source.entity(),
sortDescriptors: []
) var sources: FetchedResults<TorrentSource>
) var sources: FetchedResults<Source>
@State private var availableSourceLength = 0
@ -32,7 +32,7 @@ struct SourceListView: View {
PersistenceController.shared.save()
}
)) {
Text(source.name ?? "Unknown Source")
Text(source.name)
}
}
.onDelete { offsets in
@ -52,7 +52,7 @@ struct SourceListView: View {
ForEach(sourceManager.availableSources, id: \.self) { availableSource in
if !sources.contains(where: { availableSource.name == $0.name }) {
HStack {
Text(availableSource.name ?? "Unnamed source")
Text(availableSource.name)
Spacer()