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 = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourceListView.swift */; }; 0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourceListView.swift */; };
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; }; 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; };
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.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 */; }; 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; };
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
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 */; }; 0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */ = {isa = PBXBuildFile; productRef = 0C90E32B2888E5D000C0BC89 /* ActivityView */; };
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */; }; 0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */; };
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* SourceManager.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 */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
0CBC7702288DE4400054BE44 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */; };
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; 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 */; }; 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
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>"; }; 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>"; }; 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -100,6 +113,7 @@
files = ( files = (
0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */, 0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */,
0C64A4B4288903680079976D /* Base32 in Frameworks */, 0C64A4B4288903680079976D /* Base32 in Frameworks */,
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
); );
@ -111,10 +125,16 @@
0C0D50DE288DF72D0035ECC8 /* Classes */ = { 0C0D50DE288DF72D0035ECC8 /* Classes */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */, 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */,
0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */, 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */,
0C0D50DF288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift */, 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */,
0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.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; path = Classes;
sourceTree = "<group>"; sourceTree = "<group>";
@ -252,7 +272,7 @@
children = ( children = (
0C0D50DE288DF72D0035ECC8 /* Classes */, 0C0D50DE288DF72D0035ECC8 /* Classes */,
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */, 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */,
0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */, 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */,
); );
path = DataManagement; path = DataManagement;
sourceTree = "<group>"; sourceTree = "<group>";
@ -278,6 +298,7 @@
0C90E32B2888E5D000C0BC89 /* ActivityView */, 0C90E32B2888E5D000C0BC89 /* ActivityView */,
0C64A4B3288903680079976D /* Base32 */, 0C64A4B3288903680079976D /* Base32 */,
0C64A4B6288903880079976D /* KeychainSwift */, 0C64A4B6288903880079976D /* KeychainSwift */,
0C4CFC452897030D00AD9FAD /* Regex */,
); );
productName = Torrenter; productName = Torrenter;
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
@ -312,6 +333,7 @@
0C90E32A2888E5D000C0BC89 /* XCRemoteSwiftPackageReference "ActivityView" */, 0C90E32A2888E5D000C0BC89 /* XCRemoteSwiftPackageReference "ActivityView" */,
0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */, 0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */,
0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */, 0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */,
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */,
); );
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -339,41 +361,47 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0CE37ABB288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift in Sources */,
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
0CA148DB288903F000DE2211 /* NavView.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */, 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */, 0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */,
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */,
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.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 */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
0CA148E2288903F000DE2211 /* Data.swift in Sources */, 0CA148E2288903F000DE2211 /* Data.swift in Sources */,
0C0D50E1288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift in Sources */,
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */, 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */, 0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */,
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */, 0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
0CBC7702288DE4400054BE44 /* FerriteDB.xcdatamodeld in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
0CA148D9288903F000DE2211 /* CardView.swift in Sources */, 0CA148D9288903F000DE2211 /* CardView.swift in Sources */,
0C32FB572890D1F2002BD219 /* ListRowViews.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 */, 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */,
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */, 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */,
0C0D50E2288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift in Sources */,
0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */, 0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -581,6 +609,14 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/Regex";
requirement = {
branch = main;
kind = branch;
};
};
0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */ = { 0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/norio-nomura/Base32"; repositoryURL = "https://github.com/norio-nomura/Base32";
@ -616,6 +652,11 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
0C4CFC452897030D00AD9FAD /* Regex */ = {
isa = XCSwiftPackageProductDependency;
package = 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */;
productName = Regex;
};
0C64A4B3288903680079976D /* Base32 */ = { 0C64A4B3288903680079976D /* Base32 */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */; package = 0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */;
@ -639,12 +680,12 @@
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */ /* Begin XCVersionGroup section */
0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */ = { 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;
children = ( children = (
0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */, 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */,
); );
currentVersion = 0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */; currentVersion = 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */;
path = FerriteDB.xcdatamodeld; path = FerriteDB.xcdatamodeld;
sourceTree = "<group>"; sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel; 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"?> <?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=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21271" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="TorrentSource" representedClassName="TorrentSource" syncable="YES"> <entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="baseUrl" attributeType="String" defaultValueString=""/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/> <attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="linkQuery" attributeType="String"/> <attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="name" optional="YES" attributeType="String"/> <attribute name="version" attributeType="String" defaultValueString=""/>
<attribute name="rowQuery" attributeType="String"/> <relationship name="htmlParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceHtmlParser" inverseName="parentSource" inverseEntity="SourceHtmlParser"/>
<attribute name="sizeQuery" optional="YES" attributeType="String"/>
<attribute name="titleQuery" optional="YES" attributeType="String"/>
<attribute name="url" attributeType="String"/>
</entity> </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="repoAuthor" optional="YES" attributeType="String"/>
<attribute name="repoName" 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> </entity>
</model> </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 // No iCloud until finalized sources
struct PersistenceController { struct PersistenceController {
static let shared = PersistenceController() static var shared = PersistenceController()
// Coredata storage // Coredata storage
let container: NSPersistentContainer let container: NSPersistentContainer
@ -40,10 +40,9 @@ struct PersistenceController {
backgroundContext.automaticallyMergesChangesFromParent = true backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
try? backgroundContext.setQueryGenerationFrom(.current) try? backgroundContext.setQueryGenerationFrom(.current)
container.loadPersistentStores { _, error in container.loadPersistentStores { _, error in
if let error = error { if let error = error {
fatalError("Error: \(error.localizedDescription)") fatalError("CoreData init error: \(error)")
} }
} }
} }

View file

@ -7,10 +7,10 @@
import Foundation import Foundation
public struct SourceJson: Codable { public struct SourceListJson: Codable {
let repoName: String? let repoName: String?
let repoAuthor: String? let repoAuthor: String?
let sources: [TorrentSourceJson] let sources: [SourceJson]
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case repoName = "name" case repoName = "name"
@ -19,11 +19,40 @@ public struct SourceJson: Codable {
} }
} }
public struct TorrentSourceJson: Codable, Hashable { public struct SourceJson: Codable, Hashable {
let name: String? let name: String
let url: String let version: String
let rowQuery: String let baseUrl: String
let linkQuery: String let htmlParser: SourceHtmlParserJson?
let titleQuery: String? }
let sizeQuery: String?
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 Base32
import Regex
import SwiftSoup import SwiftSoup
import SwiftUI import SwiftUI
@ -15,6 +16,8 @@ public struct SearchResult: Hashable, Codable {
let size: String let size: String
let magnetLink: String let magnetLink: String
let magnetHash: String? let magnetHash: String?
let seeders: String?
let leechers: String?
} }
class ScrapingViewModel: ObservableObject { class ScrapingViewModel: ObservableObject {
@ -24,44 +27,48 @@ class ScrapingViewModel: ObservableObject {
var toastModel: ToastViewModel? var toastModel: ToastViewModel?
@Published var searchResults: [SearchResult] = [] @Published var searchResults: [SearchResult] = []
@Published var debridHashes: [String] = []
@Published var searchText: String = "" @Published var searchText: String = ""
@Published var selectedSearchResult: SearchResult? @Published var selectedSearchResult: SearchResult?
@Published var filteredSource: TorrentSource? @Published var filteredSource: Source?
@MainActor @MainActor
public func scanSources(sources: [TorrentSource]) async { public func scanSources(sources: [Source]) async {
if sources.isEmpty { if sources.isEmpty {
print("Sources empty") print("Sources empty")
return
} }
var tempResults: [SearchResult] = [] var tempResults: [SearchResult] = []
for source in sources { for source in sources {
if source.enabled { if source.enabled {
guard let html = await fetchWebsiteHtml(source: source) else { if let htmlParser = source.htmlParser {
continue 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) continue
tempResults += sourceResults }
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 searchResults = tempResults
} }
// Fetches the HTML body for the source website // Fetches the HTML for a URL
@MainActor @MainActor
public func fetchWebsiteHtml(source: TorrentSource) async -> String? { public func fetchWebsiteHtml(urlString: String) async -> String? {
guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { guard let url = URL(string: urlString) 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 {
toastModel?.toastDescription = "Source doesn't contain a valid URL, contact the source dev!" toastModel?.toastDescription = "Source doesn't contain a valid URL, contact the source dev!"
print("Source doesn't contain a valid URL, contact the source dev!") print("Source doesn't contain a valid URL, contact the source dev!")
@ -83,58 +90,179 @@ class ScrapingViewModel: ObservableObject {
// Returns results to UI // Returns results to UI
// Results must have a link and title, but other parameters aren't required // Results must have a link and title, but other parameters aren't required
@MainActor @MainActor
public func scrapeWebsite(source: TorrentSource, html: String) async -> [SearchResult] { public func scrapeWebsite(source: Source, html: String) async -> [SearchResult] {
var tempResults: [SearchResult] = [] guard let htmlParser = source.htmlParser else {
var hashes: [String] = [] return []
}
var rows = Elements()
do { do {
let document = try SwiftSoup.parse(html) 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 { var tempResults: [SearchResult] = []
guard let link = try row.select(source.linkQuery).first() else {
// 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 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:") { if !href.starts(with: "magnet:") {
continue continue
} }
// Fetches the magnet hash
let magnetHash = fetchMagnetHash(magnetLink: href) let magnetHash = fetchMagnetHash(magnetLink: href)
// Fetches the episode/movie title
var title: String? var title: String?
if let titleQuery = source.titleQuery { if let titleParser = htmlParser.title {
title = try row.select(titleQuery).first()?.text() title = try? runComplexQuery(
row: row,
query: titleParser.query,
attribute: titleParser.attribute,
regexString: titleParser.regex
)
} }
let size = try row.select(source.sizeQuery ?? "").first() // Fetches the torrent's size
let sizeText = try size?.text() 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( let result = SearchResult(
title: title ?? "No title", title: title ?? "No title",
source: source.name ?? "N/A", source: source.name,
size: sizeText ?? "?B", size: size ?? "",
magnetLink: href, 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) tempResults.append(result)
} catch {
toastModel?.toastDescription = "Scraping error: \(error)"
print("Scraping error: \(error)")
continue
} }
}
return tempResults return tempResults
} catch { }
toastModel?.toastDescription = "Error while scraping: \(error)"
print("Error while scraping: \(error)")
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)) let decryptedMagnetHash = base32DecodeToData(String(magnetHash))
return decryptedMagnetHash?.hexEncodedString() return decryptedMagnetHash?.hexEncodedString()
} else { } else {
return String(magnetHash) return String(magnetHash).lowercased()
} }
} }
} }

View file

@ -5,22 +5,23 @@
// Created by Brian Dashore on 7/25/22. // Created by Brian Dashore on 7/25/22.
// //
import CoreData
import Foundation import Foundation
public class SourceManager: ObservableObject { public class SourceManager: ObservableObject {
var toastModel: ToastViewModel? var toastModel: ToastViewModel?
@Published var availableSources: [TorrentSourceJson] = [] @Published var availableSources: [SourceJson] = []
@Published var urlErrorAlertText = "" @Published var urlErrorAlertText = ""
@Published var showUrlErrorAlert = false @Published var showUrlErrorAlert = false
@MainActor @MainActor
public func fetchSourcesFromUrl() async { public func fetchSourcesFromUrl() async {
let sourceUrlRequest = TorrentSourceUrl.fetchRequest() let sourceUrlRequest = SourceList.fetchRequest()
do { do {
let sourceUrls = try PersistenceController.shared.backgroundContext.fetch(sourceUrlRequest) let sourceUrls = try PersistenceController.shared.backgroundContext.fetch(sourceUrlRequest)
var tempSourceUrls: [TorrentSourceJson] = [] var tempSourceUrls: [SourceJson] = []
for sourceUrl in sourceUrls { for sourceUrl in sourceUrls {
guard let url = URL(string: sourceUrl.urlString) else { 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 (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 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 let backgroundContext = PersistenceController.shared.backgroundContext
// If a source exists, don't add the new one // If a source exists, don't add the new one
if let name = sourceJson.name { let existingSourceRequest = Source.fetchRequest()
let existingSourceRequest = TorrentSource.fetchRequest() existingSourceRequest.predicate = NSPredicate(format: "name == %@", sourceJson.name)
existingSourceRequest.predicate = NSPredicate(format: "name == %@", name) existingSourceRequest.fetchLimit = 1
existingSourceRequest.fetchLimit = 1
let existingSource = try? backgroundContext.fetch(existingSourceRequest).first let existingSource = try? backgroundContext.fetch(existingSourceRequest).first
if existingSource != nil { if existingSource != nil {
Task { @MainActor in Task { @MainActor in
toastModel?.toastDescription = "Could not install source \(sourceJson.name ?? "Unknown source") because it is already installed." toastModel?.toastDescription = "Could not install source with name \(sourceJson.name) because it is already installed."
}
return
} }
return
} }
let newTorrentSource = TorrentSource(context: backgroundContext) let newSource = Source(context: backgroundContext)
newTorrentSource.name = sourceJson.name newSource.name = sourceJson.name
newTorrentSource.url = sourceJson.url newSource.version = sourceJson.version
newTorrentSource.rowQuery = sourceJson.rowQuery newSource.baseUrl = sourceJson.baseUrl
newTorrentSource.linkQuery = sourceJson.linkQuery
newTorrentSource.titleQuery = sourceJson.titleQuery
newTorrentSource.sizeQuery = sourceJson.sizeQuery
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 { do {
try backgroundContext.save() try backgroundContext.save()
@ -88,7 +134,7 @@ public class SourceManager: ObservableObject {
return false return false
} }
let sourceUrlRequest = TorrentSourceUrl.fetchRequest() let sourceUrlRequest = SourceList.fetchRequest()
sourceUrlRequest.predicate = NSPredicate(format: "urlString == %@", sourceUrl) sourceUrlRequest.predicate = NSPredicate(format: "urlString == %@", sourceUrl)
sourceUrlRequest.fetchLimit = 1 sourceUrlRequest.fetchLimit = 1
@ -97,12 +143,12 @@ public class SourceManager: ObservableObject {
PersistenceController.shared.delete(existingSourceUrl, context: backgroundContext) PersistenceController.shared.delete(existingSourceUrl, context: backgroundContext)
} }
let newSourceUrl = TorrentSourceUrl(context: backgroundContext) let newSourceUrl = SourceList(context: backgroundContext)
newSourceUrl.urlString = sourceUrl newSourceUrl.urlString = sourceUrl
do { do {
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!)) let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
if let rawResponse = try? JSONDecoder().decode(SourceJson.self, from: data) { if let rawResponse = try? JSONDecoder().decode(SourceListJson.self, from: data) {
newSourceUrl.repoName = rawResponse.repoName newSourceUrl.repoName = rawResponse.repoName
} }

View file

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

View file

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

View file

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

View file

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

View file

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