From 940f8337a5a41a15ab67c81e9e662a54d3cfb0fe Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 2 Aug 2022 16:17:41 -0400 Subject: [PATCH] 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 --- Ferrite.xcodeproj/project.pbxproj | 87 +++++-- .../Classes/Source+CoreDataClass.swift | 13 ++ .../Classes/Source+CoreDataProperties.swift | 24 ++ .../SourceComplexQuery+CoreDataClass.swift | 13 ++ ...ourceComplexQuery+CoreDataProperties.swift | 22 ++ .../SourceHtmlParser+CoreDataClass.swift | 13 ++ .../SourceHtmlParser+CoreDataProperties.swift | 26 +++ .../Classes/SourceList+CoreDataClass.swift | 13 ++ .../SourceList+CoreDataProperties.swift | 22 ++ .../SourceSeedLeech+CoreDataClass.swift | 13 ++ .../SourceSeedLeech+CoreDataProperties.swift | 26 +++ .../Classes/TorrentSource+CoreDataClass.swift | 13 -- .../TorrentSource+CoreDataProperties.swift | 26 --- .../TorrentSourceUrl+CoreDataClass.swift | 13 -- .../TorrentSourceUrl+CoreDataProperties.swift | 22 -- .../FerriteDB.xcdatamodeld/.xccurrentversion | 8 + .../FerriteDB.xcdatamodel/contents | 51 ++++- .../FerriteDB_v2.xcdatamodel/contents | 17 ++ .../PersistenceController.swift | 5 +- Ferrite/Models/SourceModels.swift | 47 +++- Ferrite/ViewModels/ScrapingViewModel.swift | 214 ++++++++++++++---- Ferrite/ViewModels/SourceManager.swift | 100 +++++--- Ferrite/Views/ContentView.swift | 6 +- Ferrite/Views/SearchResultRDView.swift | 8 + Ferrite/Views/SettingsView.swift | 1 + .../SettingsViews/SettingsSourceUrlView.swift | 4 +- Ferrite/Views/SourceListView.swift | 8 +- 27 files changed, 617 insertions(+), 198 deletions(-) create mode 100644 Ferrite/DataManagement/Classes/Source+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift delete mode 100644 Ferrite/DataManagement/Classes/TorrentSource+CoreDataClass.swift delete mode 100644 Ferrite/DataManagement/Classes/TorrentSource+CoreDataProperties.swift delete mode 100644 Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataClass.swift delete mode 100644 Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion create mode 100644 Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 601c669..4afcce9 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -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 = ""; }; - 0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSource+CoreDataProperties.swift"; sourceTree = ""; }; 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = ""; }; 0C0D50E6288DFF850035ECC8 /* SourceListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListView.swift; sourceTree = ""; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; 0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = ""; }; + 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = ""; }; + 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; + 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = ""; }; + 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = ""; }; + 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = ""; }; + 0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataClass.swift"; sourceTree = ""; }; + 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = ""; }; + 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = ""; }; + 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = ""; }; + 0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataClass.swift"; sourceTree = ""; }; + 0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataProperties.swift"; sourceTree = ""; }; 0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSourceUrlView.swift; sourceTree = ""; }; 0CA05458288EE9E600850554 /* SourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManager.swift; sourceTree = ""; }; 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListEditorView.swift; sourceTree = ""; }; @@ -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 = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; - 0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; - 0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSourceUrl+CoreDataClass.swift"; sourceTree = ""; }; - 0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSourceUrl+CoreDataProperties.swift"; sourceTree = ""; }; 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = ""; }; /* 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 = ""; @@ -252,7 +272,7 @@ children = ( 0C0D50DE288DF72D0035ECC8 /* Classes */, 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */, - 0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */, + 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */, ); path = DataManagement; sourceTree = ""; @@ -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 = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Ferrite/DataManagement/Classes/Source+CoreDataClass.swift b/Ferrite/DataManagement/Classes/Source+CoreDataClass.swift new file mode 100644 index 0000000..1196354 --- /dev/null +++ b/Ferrite/DataManagement/Classes/Source+CoreDataClass.swift @@ -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 {} diff --git a/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift new file mode 100644 index 0000000..1213858 --- /dev/null +++ b/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift @@ -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 { + NSFetchRequest(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 {} diff --git a/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataClass.swift b/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataClass.swift new file mode 100644 index 0000000..3362732 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataClass.swift @@ -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 {} diff --git a/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift new file mode 100644 index 0000000..eaa9c56 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceComplexQuery+CoreDataProperties.swift @@ -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 { + NSFetchRequest(entityName: "SourceComplexQuery") + } + + @NSManaged var attribute: String + @NSManaged var query: String + @NSManaged var regex: String? +} + +extension SourceComplexQuery: Identifiable {} diff --git a/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataClass.swift b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataClass.swift new file mode 100644 index 0000000..ff712ba --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataClass.swift @@ -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 {} diff --git a/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift new file mode 100644 index 0000000..e7e6268 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift @@ -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 { + NSFetchRequest(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 {} diff --git a/Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift b/Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift new file mode 100644 index 0000000..c6f26e5 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift @@ -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 {} diff --git a/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift new file mode 100644 index 0000000..aa85143 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift @@ -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 { + NSFetchRequest(entityName: "SourceList") + } + + @NSManaged var repoAuthor: String? + @NSManaged var repoName: String? + @NSManaged var urlString: String +} + +extension SourceList: Identifiable {} diff --git a/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataClass.swift b/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataClass.swift new file mode 100644 index 0000000..236706a --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataClass.swift @@ -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 {} diff --git a/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift new file mode 100644 index 0000000..365bb59 --- /dev/null +++ b/Ferrite/DataManagement/Classes/SourceSeedLeech+CoreDataProperties.swift @@ -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 { + NSFetchRequest(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 {} diff --git a/Ferrite/DataManagement/Classes/TorrentSource+CoreDataClass.swift b/Ferrite/DataManagement/Classes/TorrentSource+CoreDataClass.swift deleted file mode 100644 index bc789b4..0000000 --- a/Ferrite/DataManagement/Classes/TorrentSource+CoreDataClass.swift +++ /dev/null @@ -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 {} diff --git a/Ferrite/DataManagement/Classes/TorrentSource+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/TorrentSource+CoreDataProperties.swift deleted file mode 100644 index 9040ec7..0000000 --- a/Ferrite/DataManagement/Classes/TorrentSource+CoreDataProperties.swift +++ /dev/null @@ -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 { - NSFetchRequest(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 {} diff --git a/Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataClass.swift b/Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataClass.swift deleted file mode 100644 index f6ec04c..0000000 --- a/Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataClass.swift +++ /dev/null @@ -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 {} diff --git a/Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataProperties.swift deleted file mode 100644 index 265ce24..0000000 --- a/Ferrite/DataManagement/Classes/TorrentSourceUrl+CoreDataProperties.swift +++ /dev/null @@ -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 { - NSFetchRequest(entityName: "TorrentSourceUrl") - } - - @NSManaged var urlString: String - @NSManaged var repoName: String? - @NSManaged var repoAuthor: String? -} - -extension TorrentSourceUrl: Identifiable {} diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..c089bb1 --- /dev/null +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + FerriteDB.xcdatamodel + + diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents index 84c1bc5..7696a03 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents @@ -1,17 +1,48 @@ - - + + + - - - - - - + + + - + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents new file mode 100644 index 0000000..84c1bc5 --- /dev/null +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index dd7bcb7..2dacbe7 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -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)") } } } diff --git a/Ferrite/Models/SourceModels.swift b/Ferrite/Models/SourceModels.swift index 6110ec4..e2d99ef 100644 --- a/Ferrite/Models/SourceModels.swift +++ b/Ferrite/Models/SourceModels.swift @@ -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? } diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 8e8cf1a..38b4c73 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -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() } } } diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index d8e148d..dd99f4b 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -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 } diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 5e0010f..72e3226 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -15,11 +15,11 @@ struct ContentView: View { @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false @FetchRequest( - entity: TorrentSource.entity(), + entity: Source.entity(), sortDescriptors: [] - ) var sources: FetchedResults + ) var sources: FetchedResults - @State private var selectedSource: TorrentSource? { + @State private var selectedSource: Source? { didSet { scrapingModel.filteredSource = selectedSource } diff --git a/Ferrite/Views/SearchResultRDView.swift b/Ferrite/Views/SearchResultRDView.swift index 0183285..f422a7c 100644 --- a/Ferrite/Views/SearchResultRDView.swift +++ b/Ferrite/Views/SearchResultRDView.swift @@ -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 { diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 19adb2a..dae6a4c 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var sourceManager: SourceManager let backgroundContext = PersistenceController.shared.backgroundContext diff --git a/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift b/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift index b1619da..dfc34ef 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift @@ -11,9 +11,9 @@ struct SettingsSourceListView: View { let backgroundContext = PersistenceController.shared.backgroundContext @FetchRequest( - entity: TorrentSourceUrl.entity(), + entity: SourceList.entity(), sortDescriptors: [] - ) var sourceUrls: FetchedResults + ) var sourceUrls: FetchedResults @State private var presentSourceSheet = false diff --git a/Ferrite/Views/SourceListView.swift b/Ferrite/Views/SourceListView.swift index bd7b1d9..0bad7cc 100644 --- a/Ferrite/Views/SourceListView.swift +++ b/Ferrite/Views/SourceListView.swift @@ -13,9 +13,9 @@ struct SourceListView: View { let backgroundContext = PersistenceController.shared.backgroundContext @FetchRequest( - entity: TorrentSource.entity(), + entity: Source.entity(), sortDescriptors: [] - ) var sources: FetchedResults + ) var sources: FetchedResults @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()