mirror of
https://github.com/Ferrite-iOS/Ferrite.git
synced 2026-01-11 20:10:27 +00:00
Debrid: Decentralize and add AllDebrid support
AllDebrid is another debrid provider. Add support to Ferrite in addition to RealDebrid. The overall debrid login backend has changed to accomodate for a more agnostic app structure where more services can be added as needed. Also add some cosmetic changes to search so filters can be added while searching for a phrase. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
06d4f8e84e
commit
2322d3af67
36 changed files with 1014 additions and 252 deletions
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; };
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; };
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; };
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; };
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
|
||||
|
|
@ -20,6 +22,8 @@
|
|||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; };
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; };
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; };
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */; };
|
||||
0C42B5982932F6DD008057A0 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Array.swift */; };
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.swift */; };
|
||||
|
|
@ -28,11 +32,13 @@
|
|||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; };
|
||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; };
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
|
||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
|
||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; };
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
|
||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */; };
|
||||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */; };
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; };
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; };
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; };
|
||||
|
|
@ -105,6 +111,8 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
|
||||
0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = "<group>"; };
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -117,6 +125,8 @@
|
|||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; };
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; };
|
||||
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridChoiceView.swift; sourceTree = "<group>"; };
|
||||
0C42B5972932F6DD008057A0 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||
0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -124,9 +134,11 @@
|
|||
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
|
||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
|
||||
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = "<group>"; };
|
||||
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridModels.swift; sourceTree = "<group>"; };
|
||||
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = "<group>"; };
|
||||
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenter.swift; sourceTree = "<group>"; };
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -213,6 +225,36 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0C0755C22934241F00ECA142 /* SheetViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
);
|
||||
path = SheetViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C0755C32934244500ECA142 /* ComponentViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C0755C42934245800ECA142 /* Debrid */,
|
||||
0CA3B23528C265FD00616D3A /* Library */,
|
||||
0C44E2AB28D4E126007711AE /* SearchResult */,
|
||||
0CA0545C288F7CB200850554 /* Settings */,
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */,
|
||||
);
|
||||
path = ComponentViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C0755C42934245800ECA142 /* Debrid */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */,
|
||||
0C0755C5293424A200ECA142 /* DebridLabelView.swift */,
|
||||
);
|
||||
path = Debrid;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -241,7 +283,9 @@
|
|||
0C0D50E3288DFE6E0035ECC8 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */,
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */,
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
|
|
@ -281,25 +325,25 @@
|
|||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2AB28D4E126007711AE /* SearchResultViews */ = {
|
||||
0C44E2AB28D4E126007711AE /* SearchResult */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */,
|
||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */,
|
||||
);
|
||||
path = SearchResultViews;
|
||||
path = SearchResult;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */ = {
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
|
||||
);
|
||||
path = SourceViews;
|
||||
path = Source;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA0545C288F7CB200850554 /* SettingsViews */ = {
|
||||
0CA0545C288F7CB200850554 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
|
||||
|
|
@ -308,7 +352,7 @@
|
|||
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||
);
|
||||
path = SettingsViews;
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA148BA288903F000DE2211 /* Ferrite */ = {
|
||||
|
|
@ -357,6 +401,7 @@
|
|||
0CA148C8288903F000DE2211 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C42B5972932F6DD008057A0 /* Array.swift */,
|
||||
0CA148C9288903F000DE2211 /* Collection.swift */,
|
||||
0CA148CA288903F000DE2211 /* Data.swift */,
|
||||
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
|
||||
|
|
@ -373,12 +418,10 @@
|
|||
0CA148EE2889061200DE2211 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */,
|
||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */,
|
||||
0C0755C32934244500ECA142 /* ComponentViews */,
|
||||
0CA148F02889062700DE2211 /* RepresentableViews */,
|
||||
0CA148C0288903F000DE2211 /* CommonViews */,
|
||||
0C44E2AB28D4E126007711AE /* SearchResultViews */,
|
||||
0CA0545C288F7CB200850554 /* SettingsViews */,
|
||||
0C0755C22934241F00ECA142 /* SheetViews */,
|
||||
0CA148D1288903F000DE2211 /* MainView.swift */,
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */,
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
|
||||
|
|
@ -387,8 +430,6 @@
|
|||
0CA148BB288903F000DE2211 /* SettingsView.swift */,
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -417,13 +458,14 @@
|
|||
0CA148F12889066000DE2211 /* API */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */,
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */ = {
|
||||
0CA3B23528C265FD00616D3A /* Library */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23828C2660D00616D3A /* BookmarksView.swift */,
|
||||
|
|
@ -431,7 +473,7 @@
|
|||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */,
|
||||
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */,
|
||||
);
|
||||
path = LibraryViews;
|
||||
path = Library;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CAF1C5F286F5C0D00296F86 = {
|
||||
|
|
@ -562,11 +604,13 @@
|
|||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */,
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */,
|
||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
|
|
@ -588,6 +632,7 @@
|
|||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */,
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */,
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
|
||||
|
|
@ -596,6 +641,8 @@
|
|||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
||||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
|
||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||
|
|
@ -606,7 +653,8 @@
|
|||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */,
|
||||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */,
|
||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */,
|
||||
0C42B5982932F6DD008057A0 /* Array.swift in Sources */,
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
|
|
|
|||
|
|
@ -6,3 +6,198 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainSwift
|
||||
|
||||
// TODO: Fix errors
|
||||
public class AllDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let keychain = KeychainSwift()
|
||||
|
||||
let baseApiUrl = "https://api.alldebrid.com/v4"
|
||||
let appName = "Ferrite"
|
||||
|
||||
var authTask: Task<Void, Error>?
|
||||
|
||||
// Fetches information for PIN auth
|
||||
public func getPinInfo() async throws -> PinResponse {
|
||||
let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get")
|
||||
print("Auth URL: \(url)")
|
||||
let request = URLRequest(url: url)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<PinResponse>.self, from: data).data
|
||||
|
||||
return rawResponse
|
||||
} catch {
|
||||
print("Couldn't get pin information!")
|
||||
throw ADError.AuthQuery(description: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetches API keys
|
||||
public func getApiKey(checkID: String, pin: String) async throws {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "agent", value: appName),
|
||||
URLQueryItem(name: "check", value: checkID),
|
||||
URLQueryItem(name: "pin", value: pin)
|
||||
]
|
||||
|
||||
let request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/pin/check", queryItems: queryItems))
|
||||
|
||||
// Timer to poll AD API for key
|
||||
authTask = Task {
|
||||
var count = 0
|
||||
|
||||
while count < 20 {
|
||||
if Task.isCancelled {
|
||||
throw ADError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// We don't care if this fails
|
||||
let rawResponse = try? self.jsonDecoder.decode(ADResponse<ApiKeyResponse>.self, from: data).data
|
||||
|
||||
// If there's an API key from the response, end the task successfully
|
||||
if let apiKeyResponse = rawResponse {
|
||||
keychain.set(apiKeyResponse.apikey, forKey: "AllDebrid.ApiKey")
|
||||
|
||||
return
|
||||
} else {
|
||||
try await Task.sleep(seconds: 5)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
throw ADError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
}
|
||||
|
||||
if case let .failure(error) = await authTask?.result {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
keychain.delete("AllDebrid.ApiKey")
|
||||
}
|
||||
|
||||
// Wrapper request function which matches the responses and returns data
|
||||
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
||||
guard let token = keychain.get("AllDebrid.ApiKey") else {
|
||||
throw ADError.InvalidToken
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw ADError.FailedRequest(description: "No HTTP response given")
|
||||
}
|
||||
|
||||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
deleteTokens()
|
||||
throw ADError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.")
|
||||
} else {
|
||||
throw ADError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a URL for further requests
|
||||
private func buildRequestURL(urlString: String, queryItems: [URLQueryItem] = []) throws -> URL {
|
||||
guard var components = URLComponents(string: urlString) else {
|
||||
throw ADError.InvalidUrl
|
||||
}
|
||||
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "agent", value: appName)
|
||||
] + queryItems
|
||||
|
||||
if let url = components.url {
|
||||
return url
|
||||
} else {
|
||||
throw ADError.InvalidUrl
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a magnet link to the user's AD account
|
||||
public func addMagnet(magnetLink: String) async throws -> Int {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload"))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var bodyComponents = URLComponents()
|
||||
bodyComponents.queryItems = [
|
||||
URLQueryItem(name: "magnets[]", value: magnetLink)
|
||||
]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<AddMagnetResponse>.self, from: data).data
|
||||
|
||||
if let magnet = rawResponse.magnets[safe: 0] {
|
||||
return magnet.id
|
||||
} else {
|
||||
throw ADError.InvalidResponse
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchMagnetStatus(magnetId: Int, selectedIndex: Int?) async throws -> String {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "id", value: String(magnetId))
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
|
||||
|
||||
// Better to fetch no link at all than the wrong link
|
||||
if let linkWrapper = rawResponse.magnets.links[safe: selectedIndex ?? -1] {
|
||||
return linkWrapper.link
|
||||
} else {
|
||||
throw ADError.EmptyTorrents
|
||||
}
|
||||
}
|
||||
|
||||
public func unlockLink(lockedLink: String) async throws -> String {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "link", value: lockedLink)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/link/unlock", queryItems: queryItems))
|
||||
print(request)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<UnlockLinkResponse>.self, from: data).data
|
||||
|
||||
return rawResponse.link
|
||||
}
|
||||
|
||||
public func instantAvailability(hashes: [String]) async throws -> [IA] {
|
||||
let queryItems = hashes.map { URLQueryItem(name: "magnets[]", value: $0) }
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<InstantAvailabilityResponse>.self, from: data).data
|
||||
|
||||
let filteredMagnets = rawResponse.magnets.filter { $0.instant == true && $0.files != nil }
|
||||
let availableHashes = filteredMagnets.map { magnetResp in
|
||||
// Force unwrap is OK here since the filter caught any nil values
|
||||
let files = magnetResp.files!.enumerated().map { index, magnetFile in
|
||||
IAFile(id: index, fileName: magnetFile.name)
|
||||
}
|
||||
|
||||
return IA(
|
||||
hash: magnetResp.hash,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
)
|
||||
}
|
||||
|
||||
return availableHashes
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ public class RealDebrid {
|
|||
}
|
||||
|
||||
// Wrapper request function which matches the responses and returns data
|
||||
@discardableResult public func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
||||
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
||||
guard let token = await fetchToken() else {
|
||||
throw RDError.InvalidToken
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ public class RealDebrid {
|
|||
|
||||
// Checks if the magnet is streamable on RD
|
||||
// Currently does not work for batch links
|
||||
public func instantAvailability(magnetHashes: [String]) async throws -> [RealDebrid.IA] {
|
||||
public func instantAvailability(magnetHashes: [String]) async throws -> [IA] {
|
||||
var availableHashes: [RealDebrid.IA] = []
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!)
|
||||
|
||||
|
|
|
|||
26
Ferrite/Extensions/Array.swift
Normal file
26
Ferrite/Extensions/Array.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Array.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/26/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Set: RawRepresentable where Element: Codable {
|
||||
public init?(rawValue: String) {
|
||||
guard let data = rawValue.data(using: .utf8),
|
||||
let result = try? JSONDecoder().decode(Set<Element>.self, from: data)
|
||||
else { return nil }
|
||||
self = result
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
guard let data = try? JSONEncoder().encode(self),
|
||||
let result = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return "[]"
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,16 @@ extension View {
|
|||
))
|
||||
}
|
||||
|
||||
// From https://github.com/siteline/SwiftUI-Introspect/pull/129
|
||||
public func introspectSearchController(customize: @escaping (UISearchController) -> Void) -> some View {
|
||||
introspectNavigationController { navigationController in
|
||||
let navigationBar = navigationController.navigationBar
|
||||
if let searchController = navigationBar.topItem?.searchController {
|
||||
customize(searchController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Modifiers
|
||||
|
||||
func conditionalContextMenu(id: some Hashable,
|
||||
|
|
|
|||
157
Ferrite/Models/AllDebridModels.swift
Normal file
157
Ferrite/Models/AllDebridModels.swift
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//
|
||||
// AllDebridModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/25/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension AllDebrid {
|
||||
// MARK: - Errors
|
||||
|
||||
// TODO: Hybridize debrid errors in one structure
|
||||
enum ADError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
||||
// MARK: - Generic AllDebrid response
|
||||
|
||||
// Uses a generic parametr for whatever underlying response is present
|
||||
struct ADResponse<ADData: Codable>: Codable {
|
||||
let status: String
|
||||
let data: ADData
|
||||
}
|
||||
|
||||
// MARK: - PinResponse
|
||||
|
||||
struct PinResponse: Codable {
|
||||
let pin, check: String
|
||||
let expiresIn: Int
|
||||
let userURL, baseURL, checkURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pin, check
|
||||
case expiresIn = "expires_in"
|
||||
case userURL = "user_url"
|
||||
case baseURL = "base_url"
|
||||
case checkURL = "check_url"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ApiKeyResponse
|
||||
|
||||
struct ApiKeyResponse: Codable {
|
||||
let apikey: String
|
||||
let activated: Bool
|
||||
let expiresIn: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case apikey, activated
|
||||
case expiresIn = "expires_in"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AddMagnetResponse
|
||||
|
||||
struct AddMagnetResponse: Codable {
|
||||
let magnets: [AddMagnetData]
|
||||
}
|
||||
|
||||
// MARK: - AddMagnetData
|
||||
|
||||
internal struct AddMagnetData: Codable {
|
||||
let magnet, hash, name, filenameOriginal: String
|
||||
let size: Int
|
||||
let ready: Bool
|
||||
let id: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case magnet, hash, name
|
||||
case filenameOriginal = "filename_original"
|
||||
case size, ready, id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MagnetStatusResponse
|
||||
|
||||
struct MagnetStatusResponse: Codable {
|
||||
let magnets: MagnetStatusData
|
||||
}
|
||||
|
||||
// MARK: - MagnetStatusData
|
||||
|
||||
internal struct MagnetStatusData: Codable {
|
||||
let id: Int
|
||||
let filename: String
|
||||
let size: Int
|
||||
let hash, status: String
|
||||
let statusCode, downloaded, uploaded, seeders: Int
|
||||
let downloadSpeed, processingPerc, uploadSpeed, uploadDate: Int
|
||||
let completionDate: Int
|
||||
let links: [MagnetStatusLink]
|
||||
let type: String
|
||||
let notified: Bool
|
||||
let version: Int
|
||||
}
|
||||
|
||||
// MARK: - MagnetStatusLink
|
||||
|
||||
// Abridged for required parameters
|
||||
internal struct MagnetStatusLink: Codable {
|
||||
let link: String
|
||||
let filename: String
|
||||
let size: Int
|
||||
}
|
||||
|
||||
// MARK: - UnlockLinkResponse
|
||||
|
||||
// Abridged for required parameters
|
||||
struct UnlockLinkResponse: Codable {
|
||||
let link: String
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailabilityResponse
|
||||
|
||||
struct InstantAvailabilityResponse: Codable {
|
||||
let magnets: [InstantAvailabilityMagnet]
|
||||
}
|
||||
|
||||
// MARK: - IAMagnetResponse
|
||||
|
||||
internal struct InstantAvailabilityMagnet: Codable {
|
||||
let magnet, hash: String
|
||||
let instant: Bool
|
||||
let files: [InstantAvailabilityFile]?
|
||||
}
|
||||
|
||||
// MARK: - IAFileResponse
|
||||
|
||||
internal struct InstantAvailabilityFile: Codable {
|
||||
let name: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name = "n"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailablity client side structures
|
||||
|
||||
struct IA: Codable, Hashable {
|
||||
let hash: String
|
||||
let expiryTimeStamp: Double
|
||||
var files: [IAFile]
|
||||
}
|
||||
|
||||
struct IAFile: Codable, Hashable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
}
|
||||
32
Ferrite/Models/DebridManagerModels.swift
Normal file
32
Ferrite/Models/DebridManagerModels.swift
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// DebridManagerModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Universal IA enum (IA = InstantAvailability)
|
||||
|
||||
public enum IAStatus: Codable, Hashable, Sendable {
|
||||
case full
|
||||
case partial
|
||||
case none
|
||||
}
|
||||
|
||||
// MARK: - Enum for debrid differentiation. 0 is nil
|
||||
|
||||
public enum DebridType: Int, Codable, Hashable, CaseIterable {
|
||||
case realDebrid = 1
|
||||
case allDebrid = 2
|
||||
|
||||
func toString(abbreviated: Bool = false) -> String {
|
||||
switch self {
|
||||
case .realDebrid:
|
||||
return abbreviated ? "RD" : "RealDebrid"
|
||||
case .allDebrid:
|
||||
return abbreviated ? "AD" : "AllDebrid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
extension Github {
|
||||
public struct Release: Codable, Hashable, Sendable {
|
||||
public extension Github {
|
||||
struct Release: Codable, Hashable, Sendable {
|
||||
let htmlUrl: String
|
||||
let tagName: String
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
extension RealDebrid {
|
||||
public extension RealDebrid {
|
||||
// MARK: - Errors
|
||||
public enum RDError: Error {
|
||||
|
||||
// TODO: Hybridize debrid errors in one structure
|
||||
enum RDError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
|
|
@ -23,7 +25,7 @@ extension RealDebrid {
|
|||
|
||||
// MARK: - device code endpoint
|
||||
|
||||
public struct DeviceCodeResponse: Codable, Sendable {
|
||||
struct DeviceCodeResponse: Codable, Sendable {
|
||||
let deviceCode, userCode: String
|
||||
let interval, expiresIn: Int
|
||||
let verificationURL, directVerificationURL: String
|
||||
|
|
@ -40,7 +42,7 @@ extension RealDebrid {
|
|||
|
||||
// MARK: - device credentials endpoint
|
||||
|
||||
public struct DeviceCredentialsResponse: Codable, Sendable {
|
||||
struct DeviceCredentialsResponse: Codable, Sendable {
|
||||
let clientID, clientSecret: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
|
|
@ -51,7 +53,7 @@ extension RealDebrid {
|
|||
|
||||
// MARK: - token endpoint
|
||||
|
||||
public struct TokenResponse: Codable, Sendable {
|
||||
struct TokenResponse: Codable, Sendable {
|
||||
let accessToken: String
|
||||
let expiresIn: Int
|
||||
let refreshToken, tokenType: String
|
||||
|
|
@ -67,7 +69,7 @@ extension RealDebrid {
|
|||
// MARK: - instantAvailability endpoint
|
||||
|
||||
// Thanks Skitty!
|
||||
public struct InstantAvailabilityResponse: Codable, Sendable {
|
||||
struct InstantAvailabilityResponse: Codable, Sendable {
|
||||
var data: InstantAvailabilityData?
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -79,55 +81,49 @@ extension RealDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
struct InstantAvailabilityData: Codable, Sendable {
|
||||
internal struct InstantAvailabilityData: Codable, Sendable {
|
||||
var rd: [[String: InstantAvailabilityInfo]]
|
||||
}
|
||||
|
||||
struct InstantAvailabilityInfo: Codable, Sendable {
|
||||
internal struct InstantAvailabilityInfo: Codable, Sendable {
|
||||
var filename: String
|
||||
var filesize: Int
|
||||
}
|
||||
|
||||
// MARK: - Instant Availability client side structures
|
||||
|
||||
public struct IA: Codable, Hashable, Sendable {
|
||||
struct IA: Codable, Hashable, Sendable {
|
||||
let hash: String
|
||||
let expiryTimeStamp: Double
|
||||
var files: [IAFile] = []
|
||||
var batches: [IABatch] = []
|
||||
}
|
||||
|
||||
public struct IABatch: Codable, Hashable, Sendable {
|
||||
struct IABatch: Codable, Hashable, Sendable {
|
||||
let files: [IABatchFile]
|
||||
}
|
||||
|
||||
public struct IABatchFile: Codable, Hashable, Sendable {
|
||||
struct IABatchFile: Codable, Hashable, Sendable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
|
||||
public struct IAFile: Codable, Hashable, Sendable {
|
||||
struct IAFile: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let batchIndex: Int
|
||||
let batchFileIndex: Int
|
||||
}
|
||||
|
||||
public enum IAStatus: Codable, Hashable, Sendable {
|
||||
case full
|
||||
case partial
|
||||
case none
|
||||
}
|
||||
|
||||
// MARK: - addMagnet endpoint
|
||||
|
||||
public struct AddMagnetResponse: Codable, Sendable {
|
||||
struct AddMagnetResponse: Codable, Sendable {
|
||||
let id: String
|
||||
let uri: String
|
||||
}
|
||||
|
||||
// MARK: - torrentInfo endpoint
|
||||
|
||||
struct TorrentInfoResponse: Codable, Sendable {
|
||||
internal struct TorrentInfoResponse: Codable, Sendable {
|
||||
let id, filename, originalFilename, hash: String
|
||||
let bytes, originalBytes: Int
|
||||
let host: String
|
||||
|
|
@ -148,13 +144,13 @@ extension RealDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
struct TorrentInfoFile: Codable, Sendable {
|
||||
internal struct TorrentInfoFile: Codable, Sendable {
|
||||
let id: Int
|
||||
let path: String
|
||||
let bytes, selected: Int
|
||||
}
|
||||
|
||||
public struct UserTorrentsResponse: Codable, Sendable {
|
||||
struct UserTorrentsResponse: Codable, Sendable {
|
||||
let id, filename, hash: String
|
||||
let bytes: Int
|
||||
let host: String
|
||||
|
|
@ -167,7 +163,7 @@ extension RealDebrid {
|
|||
|
||||
// MARK: - unrestrictLink endpoint
|
||||
|
||||
struct UnrestrictLinkResponse: Codable, Sendable {
|
||||
internal struct UnrestrictLinkResponse: Codable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
|
|
@ -187,7 +183,7 @@ extension RealDebrid {
|
|||
|
||||
// MARK: - User downloads list
|
||||
|
||||
public struct UserDownloadsResponse: Codable, Sendable {
|
||||
struct UserDownloadsResponse: Codable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
|
|
|
|||
|
|
@ -13,39 +13,84 @@ public class DebridManager: ObservableObject {
|
|||
// Linked classes
|
||||
var toastModel: ToastViewModel?
|
||||
let realDebrid: RealDebrid = .init()
|
||||
let allDebrid: AllDebrid = .init()
|
||||
|
||||
// UI Variables
|
||||
@Published var showWebView: Bool = false
|
||||
@Published var showLoadingProgress: Bool = false
|
||||
|
||||
// Service agnostic variables
|
||||
var currentDebridTask: Task<Void, Never>?
|
||||
|
||||
// RealDebrid auth variables
|
||||
@Published var realDebridEnabled: Bool = false {
|
||||
@Published var enabledDebrids: Set<DebridType> = [] {
|
||||
didSet {
|
||||
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
|
||||
UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var selectedDebridType: DebridType? {
|
||||
didSet {
|
||||
UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService")
|
||||
}
|
||||
}
|
||||
|
||||
var currentDebridTask: Task<Void, Never>?
|
||||
var downloadUrl: String = ""
|
||||
var authUrl: String = ""
|
||||
|
||||
// RealDebrid auth variables
|
||||
@Published var realDebridAuthProcessing: Bool = false
|
||||
var realDebridAuthUrl: String = ""
|
||||
|
||||
// RealDebrid fetch variables
|
||||
@Published var realDebridIAValues: [RealDebrid.IA] = []
|
||||
var realDebridDownloadUrl: String = ""
|
||||
|
||||
@Published var showDeleteAlert: Bool = false
|
||||
|
||||
// TODO: Switch to an individual item based sheet system to remove these variables
|
||||
var selectedRealDebridItem: RealDebrid.IA?
|
||||
var selectedRealDebridFile: RealDebrid.IAFile?
|
||||
var selectedRealDebridID: String?
|
||||
|
||||
// AllDebrid auth variables
|
||||
@Published var allDebridAuthProcessing: Bool = false
|
||||
|
||||
// AllDebrid fetch variables
|
||||
@Published var allDebridIAValues: [AllDebrid.IA] = []
|
||||
|
||||
var selectedAllDebridItem: AllDebrid.IA?
|
||||
var selectedAllDebridFile: AllDebrid.IAFile?
|
||||
|
||||
init() {
|
||||
realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
||||
if let rawDebridList = UserDefaults.standard.string(forKey: "Debrid.EnabledArray"),
|
||||
let serializedDebridList = Set<DebridType>(rawValue: rawDebridList)
|
||||
{
|
||||
enabledDebrids = serializedDebridList
|
||||
}
|
||||
|
||||
// If a UserDefaults integer isn't set, it's usually 0
|
||||
let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService")
|
||||
selectedDebridType = DebridType(rawValue: rawPreferredService)
|
||||
|
||||
// If a user has one logged in service, automatically set the preferred service to that one
|
||||
if enabledDebrids.count == 1 {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this after v0.6.0
|
||||
// Login cleanup function that's automatically run to switch to the new login system
|
||||
public func cleanupOldLogins() async {
|
||||
let realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
||||
if realDebridEnabled {
|
||||
enabledDebrids.insert(.realDebrid)
|
||||
UserDefaults.standard.set(false, forKey: "RealDebrid.Enabled")
|
||||
}
|
||||
|
||||
let allDebridEnabled = UserDefaults.standard.bool(forKey: "AllDebrid.Enabled")
|
||||
if allDebridEnabled {
|
||||
enabledDebrids.insert(.allDebrid)
|
||||
UserDefaults.standard.set(false, forKey: "AllDebrid.Enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Common function to populate hashes for debrid services
|
||||
public func populateDebridHashes(_ resultHashes: [String]) async {
|
||||
do {
|
||||
let now = Date()
|
||||
|
|
@ -53,76 +98,135 @@ public class DebridManager: ObservableObject {
|
|||
// If a hash isn't found in the IA, update it
|
||||
// If the hash is expired, remove it and update it
|
||||
let sendHashes = resultHashes.filter { hash in
|
||||
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }) {
|
||||
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }), enabledDebrids.contains(.realDebrid) {
|
||||
if now.timeIntervalSince1970 > realDebridIAValues[IAIndex].expiryTimeStamp {
|
||||
realDebridIAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if let IAIndex = allDebridIAValues.firstIndex(where: { $0.hash == hash }), enabledDebrids.contains(.allDebrid) {
|
||||
if now.timeIntervalSince1970 > allDebridIAValues[IAIndex].expiryTimeStamp {
|
||||
allDebridIAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !sendHashes.isEmpty {
|
||||
let fetchedIAValues = try await realDebrid.instantAvailability(magnetHashes: sendHashes)
|
||||
if enabledDebrids.contains(.realDebrid) {
|
||||
let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnetHashes: sendHashes)
|
||||
realDebridIAValues += fetchedRealDebridIA
|
||||
}
|
||||
|
||||
realDebridIAValues += fetchedIAValues
|
||||
if enabledDebrids.contains(.allDebrid) {
|
||||
let fetchedAllDebridIA = try await allDebrid.instantAvailability(hashes: sendHashes)
|
||||
allDebridIAValues += fetchedAllDebridIA
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
|
||||
if error.code != -999 {
|
||||
toastModel?.updateToastDescription("RealDebrid hash error: \(error)")
|
||||
toastModel?.updateToastDescription("Hash population error: \(error)")
|
||||
}
|
||||
|
||||
print("RealDebrid hash error: \(error)")
|
||||
print("Hash population error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func matchSearchResult(result: SearchResult?) -> RealDebrid.IAStatus {
|
||||
// Common function to match search results with a provided debrid service
|
||||
public func matchSearchResult(result: SearchResult?) -> IAStatus {
|
||||
guard let result else {
|
||||
return .none
|
||||
}
|
||||
|
||||
guard let debridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else {
|
||||
return .none
|
||||
}
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
guard let realDebridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if debridMatch.batches.isEmpty {
|
||||
return .full
|
||||
} else {
|
||||
return .partial
|
||||
if realDebridMatch.batches.isEmpty {
|
||||
return .full
|
||||
} else {
|
||||
return .partial
|
||||
}
|
||||
case .allDebrid:
|
||||
guard let allDebridMatch = allDebridIAValues.first(where: { result.magnetHash == $0.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if allDebridMatch.files.count > 1 {
|
||||
return .partial
|
||||
} else {
|
||||
return .full
|
||||
}
|
||||
case .none:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
public func setSelectedRdResult(result: SearchResult) -> Bool {
|
||||
public func selectDebridResult(result: SearchResult) -> Bool {
|
||||
guard let magnetHash = result.magnetHash else {
|
||||
toastModel?.updateToastDescription("Could not find the torrent magnet hash")
|
||||
return false
|
||||
}
|
||||
|
||||
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) {
|
||||
selectedRealDebridItem = realDebridItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) {
|
||||
selectedRealDebridItem = realDebridItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .allDebrid:
|
||||
if let allDebridItem = allDebridIAValues.first(where: { magnetHash == $0.hash }) {
|
||||
selectedAllDebridItem = allDebridItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not find the associated AllDebrid entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .none:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func authenticateRd() async {
|
||||
// MARK: - Authentication UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to authenticate with
|
||||
public func authenticateDebrid(debridType: DebridType) async {
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
await authenticateRd()
|
||||
enabledDebrids.insert(.realDebrid)
|
||||
case .allDebrid:
|
||||
await authenticateAd()
|
||||
enabledDebrids.insert(.allDebrid)
|
||||
}
|
||||
|
||||
// Automatically sets the preferred debrid service if only one login is provided
|
||||
if enabledDebrids.count == 1 {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticateRd() async {
|
||||
do {
|
||||
realDebridAuthProcessing = true
|
||||
let verificationResponse = try await realDebrid.getVerificationInfo()
|
||||
|
||||
realDebridAuthUrl = verificationResponse.directVerificationURL
|
||||
authUrl = verificationResponse.directVerificationURL
|
||||
showWebView.toggle()
|
||||
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
|
||||
realDebridEnabled = true
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("RealDebrid authentication error: \(error)")
|
||||
realDebrid.authTask?.cancel()
|
||||
|
|
@ -131,10 +235,44 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func logoutRd() async {
|
||||
private func authenticateAd() async {
|
||||
do {
|
||||
allDebridAuthProcessing = true
|
||||
let pinResponse = try await allDebrid.getPinInfo()
|
||||
|
||||
authUrl = pinResponse.userURL
|
||||
showWebView.toggle()
|
||||
|
||||
try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin)
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("AllDebrid authentication error: \(error)")
|
||||
allDebrid.authTask?.cancel()
|
||||
|
||||
print("AllDebrid authentication error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Logout UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to logout of
|
||||
public func logoutDebrid(debridType: DebridType) async {
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
await logoutRd()
|
||||
case .allDebrid:
|
||||
logoutAd()
|
||||
}
|
||||
|
||||
// Automatically resets the preferred debrid service if it was set to the logged out service
|
||||
if selectedDebridType == debridType {
|
||||
selectedDebridType = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func logoutRd() async {
|
||||
do {
|
||||
try await realDebrid.deleteTokens()
|
||||
realDebridEnabled = false
|
||||
enabledDebrids.remove(.realDebrid)
|
||||
realDebridAuthProcessing = false
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("RealDebrid logout error: \(error)")
|
||||
|
|
@ -143,7 +281,18 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func fetchRdDownload(searchResult: SearchResult) async {
|
||||
private func logoutAd() {
|
||||
allDebrid.deleteTokens()
|
||||
enabledDebrids.remove(.allDebrid)
|
||||
allDebridAuthProcessing = false
|
||||
|
||||
toastModel?.updateToastDescription("Please manually delete the AllDebrid API key", newToastType: .info)
|
||||
}
|
||||
|
||||
// MARK: - Debrid fetch UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to fetch from
|
||||
public func fetchDebridDownload(searchResult: SearchResult) async {
|
||||
defer {
|
||||
currentDebridTask = nil
|
||||
showLoadingProgress = false
|
||||
|
|
@ -153,11 +302,24 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
guard let magnetLink = searchResult.magnetLink else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("RealDebrid error: Invalid magnet link")
|
||||
print("Debrid error: Invalid magnet link")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
await fetchRdDownload(magnetLink: magnetLink)
|
||||
case .allDebrid:
|
||||
await fetchAdDownload(magnetLink: magnetLink)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRdDownload(magnetLink: String) async {
|
||||
print("Called RD Download function!")
|
||||
|
||||
do {
|
||||
var fileIds: [Int] = []
|
||||
|
||||
|
|
@ -178,11 +340,11 @@ public class DebridManager: ObservableObject {
|
|||
{
|
||||
let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink }
|
||||
if let existingLink = existingLinks[safe: 0]?.download {
|
||||
realDebridDownloadUrl = existingLink
|
||||
downloadUrl = existingLink
|
||||
} else {
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
realDebridDownloadUrl = downloadLink
|
||||
downloadUrl = downloadLink
|
||||
}
|
||||
|
||||
} else {
|
||||
|
|
@ -192,10 +354,13 @@ public class DebridManager: ObservableObject {
|
|||
if let realDebridId = selectedRealDebridID {
|
||||
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
|
||||
|
||||
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0)
|
||||
let torrentLink = try await realDebrid.torrentInfo(
|
||||
debridID: realDebridId,
|
||||
selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0
|
||||
)
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
realDebridDownloadUrl = downloadLink
|
||||
downloadUrl = downloadLink
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.")
|
||||
}
|
||||
|
|
@ -223,11 +388,32 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func deleteRdTorrent() async {
|
||||
func deleteRdTorrent() async {
|
||||
if let realDebridId = selectedRealDebridID {
|
||||
try? await realDebrid.deleteTorrent(debridID: realDebridId)
|
||||
}
|
||||
|
||||
selectedRealDebridID = nil
|
||||
}
|
||||
|
||||
func fetchAdDownload(magnetLink: String) async {
|
||||
do {
|
||||
let magnetID = try await allDebrid.addMagnet(magnetLink: magnetLink)
|
||||
let lockedLink = try await allDebrid.fetchMagnetStatus(
|
||||
magnetId: magnetID,
|
||||
selectedIndex: selectedAllDebridFile?.id ?? 0
|
||||
)
|
||||
let unlockedLink = try await allDebrid.unlockLink(lockedLink: lockedLink)
|
||||
|
||||
downloadUrl = unlockedLink
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
switch error.code {
|
||||
case -999:
|
||||
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
|
||||
default:
|
||||
toastModel?.updateToastDescription("AllDebrid download error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import SwiftUI
|
|||
import SwiftyJSON
|
||||
|
||||
class ScrapingViewModel: ObservableObject {
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
// Link the toast view model for single-directional communication
|
||||
var toastModel: ToastViewModel?
|
||||
let byteCountFormatter: ByteCountFormatter = .init()
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
//
|
||||
// BatchChoiceView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BatchChoiceView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.name) {
|
||||
debridManager.selectedRealDebridFile = file
|
||||
|
||||
if let searchResult = navModel.selectedSearchResult {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: searchResult)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
// The download may complete before this sheet dismisses
|
||||
try? await Task.sleep(seconds: 1)
|
||||
navModel.selectedBatchTitle = file.name
|
||||
navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
|
||||
}
|
||||
|
||||
debridManager.selectedRealDebridFile = nil
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
navModel.currentChoiceSheet = nil
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("Select a file")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
navModel.currentChoiceSheet = nil
|
||||
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BatchChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BatchChoiceView()
|
||||
}
|
||||
}
|
||||
37
Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift
Normal file
37
Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// DebridChoiceView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/26/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DebridChoiceView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Picker("", selection: $debridManager.selectedDebridType) {
|
||||
Text("None")
|
||||
.tag(nil as DebridType?)
|
||||
|
||||
ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
Text(debridType.toString())
|
||||
.tag(DebridType?.some(debridType))
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid")
|
||||
}
|
||||
.animation(.none)
|
||||
}
|
||||
}
|
||||
|
||||
struct DebridChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DebridChoiceView()
|
||||
}
|
||||
}
|
||||
36
Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift
Normal file
36
Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// DebridLabelView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/27/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DebridLabelView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
let debridAbbreviation: String
|
||||
|
||||
var body: some View {
|
||||
Text(debridAbbreviation)
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background {
|
||||
Group {
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
Color.green
|
||||
case .partial:
|
||||
Color.orange
|
||||
case .none:
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,6 @@ struct BookmarksView: View {
|
|||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var bookmarks: FetchedResults<Bookmark>
|
||||
|
|
@ -54,7 +52,7 @@ struct BookmarksView: View {
|
|||
}
|
||||
}
|
||||
.onAppear {
|
||||
if realDebridEnabled {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
viewTask = Task {
|
||||
let hashes = bookmarks.compactMap(\.magnetHash)
|
||||
await debridManager.populateDebridHashes(hashes)
|
||||
|
|
@ -22,11 +22,11 @@ struct HistoryButtonView: View {
|
|||
if let url = entry.url {
|
||||
if url.starts(with: "https://") {
|
||||
Task {
|
||||
debridManager.realDebridDownloadUrl = url
|
||||
debridManager.downloadUrl = url
|
||||
navModel.runDebridAction(urlString: url)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -13,12 +13,11 @@ struct SearchResultButtonView: View {
|
|||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
@State private var runOnce = false
|
||||
@State var existingBookmark: Bookmark? = nil
|
||||
@State private var showConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
|
|
@ -28,22 +27,22 @@ struct SearchResultButtonView: View {
|
|||
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
if debridManager.selectDebridResult(result: result) {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: result)
|
||||
await debridManager.fetchDebridDownload(searchResult: result)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.downloadUrl)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
if debridManager.selectDebridResult(result: result) {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
|
|
@ -58,7 +57,7 @@ struct SearchResultButtonView: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(4)
|
||||
|
||||
SearchResultRDView(result: result)
|
||||
SearchResultInfoView(result: result)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// SearchResultRDView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/26/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultInfoView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(result.source)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let seeders = result.seeders {
|
||||
Text("S: \(seeders)")
|
||||
}
|
||||
|
||||
if let leechers = result.leechers {
|
||||
Text("L: \(leechers)")
|
||||
}
|
||||
|
||||
if let size = result.size {
|
||||
Text(size)
|
||||
}
|
||||
|
||||
if debridManager.selectedDebridType == .realDebrid {
|
||||
DebridLabelView(result: result, debridAbbreviation: "RD")
|
||||
}
|
||||
|
||||
if debridManager.selectedDebridType == .allDebrid {
|
||||
DebridLabelView(result: result, debridAbbreviation: "AD")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,6 @@ struct ContentView: View {
|
|||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
sortDescriptors: []
|
||||
|
|
@ -64,6 +62,7 @@ struct ContentView: View {
|
|||
Image(systemName: "chevron.down")
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.animation(.none)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
|
@ -73,6 +72,7 @@ struct ContentView: View {
|
|||
SearchResultsView()
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.navigationBarTitleDisplayMode(navModel.isEditingSearch || navModel.isSearching ? .inline : .automatic)
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search",
|
||||
text: $scrapingModel.searchText,
|
||||
|
|
@ -86,8 +86,9 @@ struct ContentView: View {
|
|||
let sources = sourceManager.fetchInstalledSources()
|
||||
await scrapingModel.scanSources(sources: sources)
|
||||
|
||||
if realDebridEnabled, !scrapingModel.searchResults.isEmpty {
|
||||
if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty {
|
||||
debridManager.realDebridIAValues = []
|
||||
debridManager.allDebridIAValues = []
|
||||
|
||||
await debridManager.populateDebridHashes(
|
||||
scrapingModel.searchResults.compactMap(\.magnetHash)
|
||||
|
|
@ -106,6 +107,14 @@ struct ContentView: View {
|
|||
scrapingModel.searchText = ""
|
||||
}
|
||||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
DebridChoiceView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ struct LibraryView: View {
|
|||
HStack {
|
||||
EditButton()
|
||||
|
||||
if selectedSegment == .history {
|
||||
switch selectedSegment {
|
||||
case .bookmarks:
|
||||
DebridChoiceView()
|
||||
case .history:
|
||||
HistoryActionsView()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
//
|
||||
// SearchResultRDView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/26/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultRDView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(result.source)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let seeders = result.seeders {
|
||||
Text("S: \(seeders)")
|
||||
}
|
||||
|
||||
if let leechers = result.leechers {
|
||||
Text("L: \(leechers)")
|
||||
}
|
||||
|
||||
if let size = result.size {
|
||||
Text(size)
|
||||
}
|
||||
|
||||
if realDebridEnabled {
|
||||
Text("RD")
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background {
|
||||
Group {
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
Color.green
|
||||
case .partial:
|
||||
Color.orange
|
||||
case .none:
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
|||
struct SearchResultsView: View {
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
|
|
|||
|
|
@ -24,22 +24,36 @@ struct SettingsView: View {
|
|||
Form {
|
||||
Section(header: InlineHeader("Debrid Services")) {
|
||||
HStack {
|
||||
Text("Real Debrid")
|
||||
Text("RealDebrid")
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.realDebridEnabled {
|
||||
await debridManager.logoutRd()
|
||||
if debridManager.enabledDebrids.contains(.realDebrid) {
|
||||
await debridManager.logoutDebrid(debridType: .realDebrid)
|
||||
} else if !debridManager.realDebridAuthProcessing {
|
||||
await debridManager.authenticateRd()
|
||||
await debridManager.authenticateDebrid(debridType: .realDebrid)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.realDebridEnabled ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.realDebridEnabled ? .red : .blue)
|
||||
.onChange(of: debridManager.realDebridEnabled) { changed in
|
||||
print("Debrid enabled changed to \(changed)")
|
||||
Text(debridManager.enabledDebrids.contains(.realDebrid) ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(.realDebrid) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("AllDebrid")
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.enabledDebrids.contains(.allDebrid) {
|
||||
await debridManager.logoutDebrid(debridType: .allDebrid)
|
||||
} else if !debridManager.realDebridAuthProcessing {
|
||||
await debridManager.authenticateDebrid(debridType: .allDebrid)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.enabledDebrids.contains(.allDebrid) ? "Logout" : (debridManager.allDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(.allDebrid) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +63,7 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
Section(header: Text("Default actions")) {
|
||||
if debridManager.realDebridEnabled {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
NavigationLink(
|
||||
destination: DebridActionPickerView(),
|
||||
label: {
|
||||
|
|
@ -118,7 +132,7 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
.sheet(isPresented: $debridManager.showWebView) {
|
||||
LoginWebView(url: URL(string: debridManager.realDebridAuthUrl)!)
|
||||
LoginWebView(url: URL(string: debridManager.authUrl)!)
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
|
|
|
|||
100
Ferrite/Views/SheetViews/BatchChoiceView.swift
Normal file
100
Ferrite/Views/SheetViews/BatchChoiceView.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// BatchChoiceView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BatchChoiceView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
switch debridManager.selectedDebridType {
|
||||
case .realDebrid:
|
||||
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.name) {
|
||||
debridManager.selectedRealDebridFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.name)
|
||||
}
|
||||
}
|
||||
case .allDebrid:
|
||||
ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.fileName) {
|
||||
debridManager.selectedAllDebridFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.fileName)
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.navigationTitle("Select a file")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
navModel.currentChoiceSheet = nil
|
||||
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common function to communicate betwen VMs and queue/display a download
|
||||
func queueCommonDownload(fileName: String) {
|
||||
if let searchResult = navModel.selectedSearchResult {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchDebridDownload(searchResult: searchResult)
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
navModel.selectedBatchTitle = fileName
|
||||
navModel.addToHistory(
|
||||
name: searchResult.title,
|
||||
source: searchResult.source,
|
||||
url: debridManager.downloadUrl,
|
||||
subName: fileName
|
||||
)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
|
||||
switch debridManager.selectedDebridType {
|
||||
case .realDebrid:
|
||||
debridManager.selectedAllDebridFile = nil
|
||||
debridManager.selectedAllDebridItem = nil
|
||||
case .allDebrid:
|
||||
debridManager.selectedRealDebridFile = nil
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navModel.currentChoiceSheet = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct BatchChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BatchChoiceView()
|
||||
}
|
||||
}
|
||||
|
|
@ -37,22 +37,22 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
Section(header: "Real Debrid options") {
|
||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .outplayer)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .vlc)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .vlc)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .infuse)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .infuse)
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = debridManager.realDebridDownloadUrl
|
||||
UIPasteboard.general.string = debridManager.downloadUrl
|
||||
showLinkCopyAlert.toggle()
|
||||
}
|
||||
.backport.alert(
|
||||
|
|
@ -63,7 +63,7 @@ struct MagnetChoiceView: View {
|
|||
)
|
||||
|
||||
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
|
||||
if let url = URL(string: debridManager.realDebridDownloadUrl) {
|
||||
if let url = URL(string: debridManager.downloadUrl) {
|
||||
navModel.activityItems = [url]
|
||||
navModel.showLocalActivitySheet.toggle()
|
||||
}
|
||||
|
|
@ -108,14 +108,14 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
.onDisappear {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
.navigationTitle("Link actions")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
debridManager.downloadUrl = ""
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
Loading…
Reference in a new issue