Debrid rework #35
31 changed files with 1277 additions and 1419 deletions
|
|
@ -20,7 +20,6 @@
|
|||
0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */; };
|
||||
0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */; };
|
||||
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
|
||||
0C2B028F29E9E61E00DCF127 /* SortFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */; };
|
||||
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; };
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
|
||||
|
|
@ -54,7 +53,6 @@
|
|||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
|
||||
0C5708EB29B8F89300BE07F9 /* SettingsLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
|
||||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
|
||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
|
||||
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */; };
|
||||
|
|
@ -96,6 +94,7 @@
|
|||
0C84FCE729E4B61A00B0DFE4 /* FilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */; };
|
||||
0C84FCE929E5ADEF00B0DFE4 /* FilterAmountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */; };
|
||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.swift */; };
|
||||
0C8AE2482C0FFB6600701675 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8AE2472C0FFB6600701675 /* Store.swift */; };
|
||||
0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */; };
|
||||
0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; };
|
||||
0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */; };
|
||||
|
|
@ -129,12 +128,13 @@
|
|||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; };
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; };
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; };
|
||||
0CB0115B29D36D9E009AFEDE /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */; };
|
||||
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0AB5E29BD2A200015422C /* KodiServerView.swift */; };
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; };
|
||||
0CB725322C123E6F0047FC0B /* CloudDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */; };
|
||||
0CB725342C123E760047FC0B /* CloudTorrentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB725332C123E760047FC0B /* CloudTorrentView.swift */; };
|
||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; };
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||
|
|
@ -154,6 +154,8 @@
|
|||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; };
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; };
|
||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */; };
|
||||
0CF1ABDC2C0C04B2009F6C26 /* Debrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */; };
|
||||
0CF1ABE22C0C3D2F009F6C26 /* DebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */; };
|
||||
0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF2C0A429D1EBD400E716DD /* UIApplication.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
|
@ -171,7 +173,6 @@
|
|||
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
|
||||
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableWrapper.swift; sourceTree = "<group>"; };
|
||||
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; };
|
||||
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = "<group>"; };
|
||||
0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortFilterView.swift; sourceTree = "<group>"; };
|
||||
0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -204,7 +205,6 @@
|
|||
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLogView.swift; sourceTree = "<group>"; };
|
||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
|
||||
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
|
||||
0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiWrapper.swift; sourceTree = "<group>"; };
|
||||
0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKodiView.swift; sourceTree = "<group>"; };
|
||||
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiModels.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -242,6 +242,7 @@
|
|||
0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterModels.swift; sourceTree = "<group>"; };
|
||||
0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterAmountLabelView.swift; sourceTree = "<group>"; };
|
||||
0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = "<group>"; };
|
||||
0C8AE2472C0FFB6600701675 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
||||
0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = "<group>"; };
|
||||
0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = "<group>"; };
|
||||
0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -275,12 +276,13 @@
|
|||
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = "<group>"; };
|
||||
0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
|
||||
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0CAF9318296399190050812A /* PremiumizeCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeCloudView.swift; sourceTree = "<group>"; };
|
||||
0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||
0CB0AB5E29BD2A200015422C /* KodiServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiServerView.swift; sourceTree = "<group>"; };
|
||||
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
|
||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
|
||||
0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudDownloadView.swift; sourceTree = "<group>"; };
|
||||
0CB725332C123E760047FC0B /* CloudTorrentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTorrentView.swift; sourceTree = "<group>"; };
|
||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableInteraction.swift; sourceTree = "<group>"; };
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -300,6 +302,8 @@
|
|||
0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = "<group>"; };
|
||||
0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterHeaderView.swift; sourceTree = "<group>"; };
|
||||
0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPickerView.swift; sourceTree = "<group>"; };
|
||||
0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debrid.swift; sourceTree = "<group>"; };
|
||||
0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridModels.swift; sourceTree = "<group>"; };
|
||||
0CF2C0A429D1EBD400E716DD /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -390,6 +394,7 @@
|
|||
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */,
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
|
||||
0CF1ABE12C0C3D2F009F6C26 /* DebridModels.swift */,
|
||||
0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */,
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||
0C422E7F293542F300486D65 /* PremiumizeModels.swift */,
|
||||
|
|
@ -406,9 +411,8 @@
|
|||
0C2886D52960C4F800D6FC16 /* Cloud */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */,
|
||||
0CAF9318296399190050812A /* PremiumizeCloudView.swift */,
|
||||
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */,
|
||||
0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */,
|
||||
0CB725332C123E760047FC0B /* CloudTorrentView.swift */,
|
||||
);
|
||||
path = Cloud;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -452,6 +456,7 @@
|
|||
0C44E2A728D4DDDC007711AE /* Application.swift */,
|
||||
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */,
|
||||
0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */,
|
||||
0C8AE2472C0FFB6600701675 /* Store.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -492,6 +497,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0CE1C4172981E8D700418F20 /* Plugin.swift */,
|
||||
0CF1ABDB2C0C04B2009F6C26 /* Debrid.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -847,11 +853,11 @@
|
|||
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
|
||||
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */,
|
||||
0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */,
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
|
||||
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||
0C8AE2482C0FFB6600701675 /* Store.swift in Sources */,
|
||||
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */,
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
|
||||
|
|
@ -865,10 +871,10 @@
|
|||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
||||
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */,
|
||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
||||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */,
|
||||
0C84FCE529E4B43200B0DFE4 /* SelectedDebridFilterView.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
|
||||
0CB725342C123E760047FC0B /* CloudTorrentView.swift in Sources */,
|
||||
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */,
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
|
||||
|
|
@ -895,7 +901,6 @@
|
|||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
|
||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
||||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */,
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
||||
|
|
@ -923,6 +928,7 @@
|
|||
0C7075E629D3845D0093DB2D /* ShareSheet.swift in Sources */,
|
||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
||||
0CC2CA7429E24F63000A8585 /* ExpandedSearchable.swift in Sources */,
|
||||
0CF1ABE22C0C3D2F009F6C26 /* DebridModels.swift in Sources */,
|
||||
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
|
||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||
|
|
@ -942,11 +948,13 @@
|
|||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
|
||||
0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */,
|
||||
0CF1ABDC2C0C04B2009F6C26 /* Debrid.swift in Sources */,
|
||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
||||
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */,
|
||||
0CB725322C123E6F0047FC0B /* CloudDownloadView.swift in Sources */,
|
||||
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */,
|
||||
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
|
||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -8,27 +8,60 @@
|
|||
import Foundation
|
||||
|
||||
// TODO: Fix errors
|
||||
public class AllDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
public class AllDebrid: PollingDebridSource, ObservableObject {
|
||||
public let id = "AllDebrid"
|
||||
public let abbreviation = "AD"
|
||||
public let website = "https://alldebrid.com"
|
||||
public var authTask: Task<Void, Error>?
|
||||
|
||||
public var authProcessing: Bool = false
|
||||
public var isLoggedIn: Bool {
|
||||
getToken() != nil
|
||||
}
|
||||
|
||||
public var manualToken: String? {
|
||||
if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") {
|
||||
return getToken()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@Published public var IAValues: [DebridIA] = []
|
||||
@Published public var cloudDownloads: [DebridCloudDownload] = []
|
||||
@Published public var cloudTorrents: [DebridCloudTorrent] = []
|
||||
public var cloudTTL: Double = 0.0
|
||||
|
||||
let baseApiUrl = "https://api.alldebrid.com/v4"
|
||||
let appName = "Ferrite"
|
||||
|
||||
var authTask: Task<Void, Error>?
|
||||
let jsonDecoder = JSONDecoder()
|
||||
|
||||
// MARK: - Auth
|
||||
|
||||
// Fetches information for PIN auth
|
||||
public func getPinInfo() async throws -> PinResponse {
|
||||
public func getAuthUrl() async throws -> URL {
|
||||
let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get")
|
||||
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
|
||||
// Validate the URL before doing anything else
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<PinResponse>.self, from: data).data
|
||||
guard let userUrl = URL(string: rawResponse.userURL) else {
|
||||
throw DebridError.AuthQuery(description: "The login URL is invalid")
|
||||
}
|
||||
|
||||
// Spawn the polling task separately
|
||||
authTask = Task {
|
||||
try await getApiKey(checkID: rawResponse.check, pin: rawResponse.pin)
|
||||
}
|
||||
|
||||
return userUrl
|
||||
} catch {
|
||||
print("Couldn't get pin information!")
|
||||
throw ADError.AuthQuery(description: error.localizedDescription)
|
||||
throw DebridError.AuthQuery(description: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +81,7 @@ public class AllDebrid {
|
|||
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw ADError.AuthQuery(description: "Token request cancelled.")
|
||||
throw DebridError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
|
@ -67,7 +100,7 @@ public class AllDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
throw ADError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
throw DebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
}
|
||||
|
||||
if case let .failure(error) = await authTask?.result {
|
||||
|
|
@ -76,27 +109,27 @@ public class AllDebrid {
|
|||
}
|
||||
|
||||
// Adds a manual API key instead of web auth
|
||||
public func setApiKey(_ key: String) -> Bool {
|
||||
public func setApiKey(_ key: String) {
|
||||
FerriteKeychain.shared.set(key, forKey: "AllDebrid.ApiKey")
|
||||
UserDefaults.standard.set(true, forKey: "AllDebrid.UseManualKey")
|
||||
|
||||
return FerriteKeychain.shared.get("AllDebrid.ApiKey") == key
|
||||
}
|
||||
|
||||
public func getToken() -> String? {
|
||||
return FerriteKeychain.shared.get("AllDebrid.ApiKey")
|
||||
FerriteKeychain.shared.get("AllDebrid.ApiKey")
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
public func logout() {
|
||||
FerriteKeychain.shared.delete("AllDebrid.ApiKey")
|
||||
UserDefaults.standard.removeObject(forKey: "AllDebrid.UseManualKey")
|
||||
}
|
||||
|
||||
// MARK: - Common request
|
||||
|
||||
// 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 = getToken() else {
|
||||
throw ADError.InvalidToken
|
||||
throw DebridError.InvalidToken
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
|
@ -104,23 +137,22 @@ public class AllDebrid {
|
|||
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")
|
||||
throw DebridError.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.")
|
||||
throw DebridError.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).")
|
||||
throw DebridError.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
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
|
||||
components.queryItems = [
|
||||
|
|
@ -130,14 +162,84 @@ public class AllDebrid {
|
|||
if let url = components.url {
|
||||
return url
|
||||
} else {
|
||||
throw ADError.InvalidUrl
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Instant availability
|
||||
|
||||
public func instantAvailability(magnets: [Magnet]) async throws {
|
||||
let now = Date().timeIntervalSince1970
|
||||
|
||||
let sendMagnets = magnets.filter { magnet in
|
||||
if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) {
|
||||
if now > IAValues[IAIndex].expiryTimeStamp {
|
||||
IAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if sendMagnets.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let queryItems = sendMagnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
|
||||
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
|
||||
DebridIAFile(fileId: index, name: magnetFile.name)
|
||||
}
|
||||
|
||||
return DebridIA(
|
||||
magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet),
|
||||
source: self.id,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
)
|
||||
}
|
||||
|
||||
IAValues += availableHashes
|
||||
}
|
||||
|
||||
// MARK: - Downloading
|
||||
|
||||
// Wrapper function to fetch a download link from the API
|
||||
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
|
||||
let selectedMagnetId: String
|
||||
|
||||
if let existingMagnet = cloudTorrents.first(where: { $0.hash == magnet.hash && $0.status == "Ready" }) {
|
||||
selectedMagnetId = existingMagnet.torrentId
|
||||
} else {
|
||||
let magnetId = try await addMagnet(magnet: magnet)
|
||||
selectedMagnetId = String(magnetId)
|
||||
}
|
||||
|
||||
let lockedLink = try await fetchMagnetStatus(
|
||||
magnetId: selectedMagnetId,
|
||||
selectedIndex: iaFile?.fileId ?? 0
|
||||
)
|
||||
|
||||
try await saveLink(link: lockedLink)
|
||||
let downloadUrl = try await unlockLink(lockedLink: lockedLink)
|
||||
|
||||
return downloadUrl
|
||||
}
|
||||
|
||||
// Adds a magnet link to the user's AD account
|
||||
public func addMagnet(magnet: Magnet) async throws -> Int {
|
||||
guard let magnetLink = magnet.link else {
|
||||
throw ADError.FailedRequest(description: "The magnet link is invalid")
|
||||
throw DebridError.FailedRequest(description: "The magnet link is invalid")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload"))
|
||||
|
|
@ -157,13 +259,13 @@ public class AllDebrid {
|
|||
if let magnet = rawResponse.magnets[safe: 0] {
|
||||
return magnet.id
|
||||
} else {
|
||||
throw ADError.InvalidResponse
|
||||
throw DebridError.InvalidResponse
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchMagnetStatus(magnetId: Int, selectedIndex: Int?) async throws -> String {
|
||||
public func fetchMagnetStatus(magnetId: String, selectedIndex: Int?) async throws -> String {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "id", value: String(magnetId))
|
||||
URLQueryItem(name: "id", value: magnetId)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems))
|
||||
|
||||
|
|
@ -174,32 +276,10 @@ public class AllDebrid {
|
|||
if let linkWrapper = rawResponse.magnets[safe: 0]?.links[safe: selectedIndex ?? -1] {
|
||||
return linkWrapper.link
|
||||
} else {
|
||||
throw ADError.EmptyTorrents
|
||||
throw DebridError.EmptyTorrents
|
||||
}
|
||||
}
|
||||
|
||||
public func userMagnets() async throws -> [MagnetStatusData] {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status"))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
|
||||
|
||||
if rawResponse.magnets.isEmpty {
|
||||
throw ADError.EmptyData
|
||||
} else {
|
||||
return rawResponse.magnets
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteMagnet(magnetId: Int) async throws {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "id", value: String(magnetId))
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
public func unlockLink(lockedLink: String) async throws -> String {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "link", value: lockedLink)
|
||||
|
|
@ -221,49 +301,74 @@ public class AllDebrid {
|
|||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
public func savedLinks() async throws -> [SavedLink] {
|
||||
// MARK: - Cloud methods
|
||||
|
||||
// Referred to as "User magnets" in AllDebrid's API
|
||||
public func getUserTorrents() async throws {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status"))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
|
||||
|
||||
if rawResponse.magnets.isEmpty {
|
||||
throw DebridError.EmptyData
|
||||
}
|
||||
|
||||
cloudTorrents = rawResponse.magnets.map { magnetResponse in
|
||||
DebridCloudTorrent(
|
||||
torrentId: String(magnetResponse.id),
|
||||
source: self.id,
|
||||
fileName: magnetResponse.filename,
|
||||
status: magnetResponse.status,
|
||||
hash: magnetResponse.hash,
|
||||
links: magnetResponse.links.map(\.link)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteTorrent(torrentId: String?) async throws {
|
||||
guard let torrentId else {
|
||||
throw DebridError.FailedRequest(description: "The torrentID \(String(describing: torrentId)) is invalid")
|
||||
}
|
||||
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "id", value: torrentId)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
public func getUserDownloads() async throws {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links"))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<SavedLinksResponse>.self, from: data).data
|
||||
|
||||
if rawResponse.links.isEmpty {
|
||||
throw ADError.EmptyData
|
||||
} else {
|
||||
return rawResponse.links
|
||||
throw DebridError.EmptyData
|
||||
}
|
||||
|
||||
// The link is also the ID
|
||||
cloudDownloads = rawResponse.links.map { link in
|
||||
DebridCloudDownload(
|
||||
downloadId: link.link, source: self.id, fileName: link.filename, link: link.link
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteLink(link: String) async throws {
|
||||
// Not used
|
||||
public func checkUserDownloads(link: String) async throws -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
// The downloadId is actually the download link
|
||||
public func deleteDownload(downloadId: String) async throws {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "link", value: link)
|
||||
URLQueryItem(name: "link", value: downloadId)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links/delete", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
public func instantAvailability(magnets: [Magnet]) async throws -> [IA] {
|
||||
let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
|
||||
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(
|
||||
magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
)
|
||||
}
|
||||
|
||||
return availableHashes
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,37 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class Premiumize {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
public class Premiumize: OAuthDebridSource, ObservableObject {
|
||||
public let id = "Premiumize"
|
||||
public let abbreviation = "PM"
|
||||
public let website = "https://premiumize.me"
|
||||
@Published public var authProcessing: Bool = false
|
||||
public var isLoggedIn: Bool {
|
||||
getToken() != nil
|
||||
}
|
||||
|
||||
public var manualToken: String? {
|
||||
if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") {
|
||||
return getToken()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@Published public var IAValues: [DebridIA] = []
|
||||
@Published public var cloudDownloads: [DebridCloudDownload] = []
|
||||
@Published public var cloudTorrents: [DebridCloudTorrent] = []
|
||||
public var cloudTTL: Double = 0.0
|
||||
|
||||
let baseAuthUrl = "https://www.premiumize.me/authorize"
|
||||
let baseApiUrl = "https://www.premiumize.me/api"
|
||||
let clientId = "791565696"
|
||||
|
||||
public func buildAuthUrl() throws -> URL {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
|
||||
// MARK: - Auth
|
||||
|
||||
public func getAuthUrl() throws -> URL {
|
||||
var urlComponents = URLComponents(string: baseAuthUrl)!
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: clientId),
|
||||
|
|
@ -25,7 +48,7 @@ public class Premiumize {
|
|||
if let url = urlComponents.url {
|
||||
return url
|
||||
} else {
|
||||
throw PMError.InvalidUrl
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,41 +56,41 @@ public class Premiumize {
|
|||
let callbackComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
|
||||
guard let callbackFragment = callbackComponents?.fragment else {
|
||||
throw PMError.InvalidResponse
|
||||
throw DebridError.InvalidResponse
|
||||
}
|
||||
|
||||
var fragmentComponents = URLComponents()
|
||||
fragmentComponents.query = callbackFragment
|
||||
|
||||
guard let accessToken = fragmentComponents.queryItems?.first(where: { $0.name == "access_token" })?.value else {
|
||||
throw PMError.InvalidToken
|
||||
throw DebridError.InvalidToken
|
||||
}
|
||||
|
||||
FerriteKeychain.shared.set(accessToken, forKey: "Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// Adds a manual API key instead of web auth
|
||||
public func setApiKey(_ key: String) -> Bool {
|
||||
public func setApiKey(_ key: String) {
|
||||
FerriteKeychain.shared.set(key, forKey: "Premiumize.AccessToken")
|
||||
UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey")
|
||||
|
||||
return FerriteKeychain.shared.get("Premiumize.AccessToken") == key
|
||||
}
|
||||
|
||||
public func getToken() -> String? {
|
||||
return FerriteKeychain.shared.get("Premiumize.AccessToken")
|
||||
FerriteKeychain.shared.get("Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
public func logout() {
|
||||
FerriteKeychain.shared.delete("Premiumize.AccessToken")
|
||||
UserDefaults.standard.removeObject(forKey: "Premiumize.UseManualKey")
|
||||
}
|
||||
|
||||
// MARK: - Common request
|
||||
|
||||
// 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 = getToken() else {
|
||||
throw PMError.InvalidToken
|
||||
throw DebridError.InvalidToken
|
||||
}
|
||||
|
||||
// Use the API query parameter if a manual API key is present
|
||||
|
|
@ -76,7 +99,7 @@ public class Premiumize {
|
|||
let requestUrl = request.url,
|
||||
var components = URLComponents(url: requestUrl, resolvingAgainstBaseURL: false)
|
||||
else {
|
||||
throw PMError.InvalidUrl
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
|
||||
let apiTokenItem = URLQueryItem(name: "apikey", value: token)
|
||||
|
|
@ -95,16 +118,110 @@ public class Premiumize {
|
|||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw PMError.FailedRequest(description: "No HTTP response given")
|
||||
throw DebridError.FailedRequest(description: "No HTTP response given")
|
||||
}
|
||||
|
||||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
deleteTokens()
|
||||
throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
|
||||
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
|
||||
} else {
|
||||
throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Instant availability
|
||||
|
||||
public func instantAvailability(magnets: [Magnet]) async throws {
|
||||
let now = Date().timeIntervalSince1970
|
||||
|
||||
// Remove magnets that don't have an associated link for PM along with existing TTL logic
|
||||
let sendMagnets = magnets.filter { magnet in
|
||||
if magnet.link == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) {
|
||||
if now > IAValues[IAIndex].expiryTimeStamp {
|
||||
IAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if sendMagnets.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let availableMagnets = try await divideCacheRequests(magnets: sendMagnets)
|
||||
|
||||
// Split DDL requests into chunks of 10
|
||||
for chunk in availableMagnets.chunked(into: 10) {
|
||||
let tempIA = try await divideDDLRequests(magnetChunk: chunk)
|
||||
IAValues += tempIA
|
||||
}
|
||||
}
|
||||
|
||||
// Function to divide and execute DDL endpoint requests in parallel
|
||||
// Calls this for 10 requests at a time to not overwhelm API servers
|
||||
public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [DebridIA] {
|
||||
let tempIA = try await withThrowingTaskGroup(of: DebridIA.self) { group in
|
||||
for magnet in magnetChunk {
|
||||
group.addTask {
|
||||
try await self.fetchDDL(magnet: magnet)
|
||||
}
|
||||
}
|
||||
|
||||
var chunkedIA: [DebridIA] = []
|
||||
for try await ia in group {
|
||||
chunkedIA.append(ia)
|
||||
}
|
||||
return chunkedIA
|
||||
}
|
||||
|
||||
return tempIA
|
||||
}
|
||||
|
||||
// Grabs DDL links
|
||||
func fetchDDL(magnet: Magnet) async throws -> DebridIA {
|
||||
if magnet.hash == nil {
|
||||
throw DebridError.EmptyData
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/directdl")!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var bodyComponents = URLComponents()
|
||||
bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnet.link)]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(DDLResponse.self, from: data)
|
||||
let content = rawResponse.content ?? []
|
||||
|
||||
if !content.isEmpty {
|
||||
let files = content.map { file in
|
||||
DebridIAFile(
|
||||
fileId: 0,
|
||||
name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path,
|
||||
streamUrlString: file.link
|
||||
)
|
||||
}
|
||||
|
||||
return DebridIA(
|
||||
magnet: magnet,
|
||||
source: id,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
)
|
||||
} else {
|
||||
throw DebridError.EmptyData
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +251,7 @@ public class Premiumize {
|
|||
var urlComponents = URLComponents(string: "\(baseApiUrl)/cache/check")!
|
||||
urlComponents.queryItems = magnets.map { URLQueryItem(name: "items[]", value: $0.hash) }
|
||||
guard let url = urlComponents.url else {
|
||||
throw PMError.InvalidUrl
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
|
@ -143,7 +260,7 @@ public class Premiumize {
|
|||
let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data)
|
||||
|
||||
if rawResponse.response.isEmpty {
|
||||
throw PMError.EmptyData
|
||||
throw DebridError.EmptyData
|
||||
} else {
|
||||
let availableMagnets = magnets.enumerated().compactMap { index, magnet in
|
||||
if rawResponse.response[safe: index] == true {
|
||||
|
|
@ -157,65 +274,25 @@ public class Premiumize {
|
|||
}
|
||||
}
|
||||
|
||||
// Function to divide and execute DDL endpoint requests in parallel
|
||||
// Calls this for 10 requests at a time to not overwhelm API servers
|
||||
public func divideDDLRequests(magnetChunk: [Magnet]) async throws -> [IA] {
|
||||
let tempIA = try await withThrowingTaskGroup(of: Premiumize.IA.self) { group in
|
||||
for magnet in magnetChunk {
|
||||
group.addTask {
|
||||
try await self.fetchDDL(magnet: magnet)
|
||||
}
|
||||
}
|
||||
// MARK: - Downloading
|
||||
|
||||
var chunkedIA: [Premiumize.IA] = []
|
||||
for try await ia in group {
|
||||
chunkedIA.append(ia)
|
||||
}
|
||||
return chunkedIA
|
||||
}
|
||||
// Wrapper function to fetch a DDL link from the API
|
||||
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
|
||||
// Store the item in PM cloud for later use
|
||||
try await createTransfer(magnet: magnet)
|
||||
|
||||
return tempIA
|
||||
}
|
||||
|
||||
// Grabs DDL links
|
||||
func fetchDDL(magnet: Magnet) async throws -> IA {
|
||||
if magnet.hash == nil {
|
||||
throw PMError.EmptyData
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/directdl")!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var bodyComponents = URLComponents()
|
||||
bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnet.link)]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(DDLResponse.self, from: data)
|
||||
|
||||
if !rawResponse.content.isEmpty {
|
||||
let files = rawResponse.content.map { file in
|
||||
IAFile(
|
||||
name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path,
|
||||
streamUrlString: file.link
|
||||
)
|
||||
}
|
||||
|
||||
return IA(
|
||||
magnet: magnet,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
)
|
||||
if let iaFile, let streamUrlString = iaFile.streamUrlString {
|
||||
return streamUrlString
|
||||
} else if let premiumizeItem = ia, let firstFile = premiumizeItem.files[safe: 0], let streamUrlString = firstFile.streamUrlString {
|
||||
return streamUrlString
|
||||
} else {
|
||||
throw PMError.EmptyData
|
||||
throw DebridError.FailedRequest(description: "Could not fetch your file from the Premiumize API")
|
||||
}
|
||||
}
|
||||
|
||||
func createTransfer(magnet: Magnet) async throws {
|
||||
guard let magnetLink = magnet.link else {
|
||||
throw PMError.FailedRequest(description: "The magnet link is invalid")
|
||||
throw DebridError.FailedRequest(description: "The magnet link is invalid")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/create")!)
|
||||
|
|
@ -230,24 +307,29 @@ public class Premiumize {
|
|||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
func userItems() async throws -> [UserItem] {
|
||||
// MARK: - Cloud methods
|
||||
|
||||
public func getUserDownloads() async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(AllItemsResponse.self, from: data)
|
||||
|
||||
if rawResponse.files.isEmpty {
|
||||
throw PMError.EmptyData
|
||||
throw DebridError.EmptyData
|
||||
}
|
||||
|
||||
return rawResponse.files
|
||||
// The "link" is the ID for Premiumize
|
||||
cloudDownloads = rawResponse.files.map { file in
|
||||
DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id)
|
||||
}
|
||||
}
|
||||
|
||||
func itemDetails(itemID: String) async throws -> ItemDetailsResponse {
|
||||
var urlComponents = URLComponents(string: "\(baseApiUrl)/item/details")!
|
||||
urlComponents.queryItems = [URLQueryItem(name: "id", value: itemID)]
|
||||
guard let url = urlComponents.url else {
|
||||
throw PMError.InvalidUrl
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
|
@ -258,16 +340,26 @@ public class Premiumize {
|
|||
return rawResponse
|
||||
}
|
||||
|
||||
func deleteItem(itemID: String) async throws {
|
||||
public func checkUserDownloads(link: String) async throws -> String? {
|
||||
// Link is the cloud item ID
|
||||
try await itemDetails(itemID: link).link
|
||||
}
|
||||
|
||||
public func deleteDownload(downloadId: String) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/delete")!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var bodyComponents = URLComponents()
|
||||
bodyComponents.queryItems = [URLQueryItem(name: "id", value: itemID)]
|
||||
bodyComponents.queryItems = [URLQueryItem(name: "id", value: downloadId)]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
// No user torrents for Premiumize
|
||||
public func getUserTorrents() async throws {}
|
||||
|
||||
public func deleteTorrent(torrentId: String?) async throws {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,37 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class RealDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
public class RealDebrid: PollingDebridSource, ObservableObject {
|
||||
public let id = "RealDebrid"
|
||||
public let abbreviation = "RD"
|
||||
public let website = "https://real-debrid.com"
|
||||
public var authTask: Task<Void, Error>?
|
||||
|
||||
@Published public var authProcessing: Bool = false
|
||||
|
||||
// Check the manual token since getTokens() is async
|
||||
public var isLoggedIn: Bool {
|
||||
FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil
|
||||
}
|
||||
|
||||
public var manualToken: String? {
|
||||
if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") {
|
||||
return FerriteKeychain.shared.get("RealDebrid.AccessToken")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@Published public var IAValues: [DebridIA] = []
|
||||
@Published public var cloudDownloads: [DebridCloudDownload] = []
|
||||
@Published public var cloudTorrents: [DebridCloudTorrent] = []
|
||||
public var cloudTTL: Double = 0.0
|
||||
|
||||
let baseAuthUrl = "https://api.real-debrid.com/oauth/v2"
|
||||
let baseApiUrl = "https://api.real-debrid.com/rest/1.0"
|
||||
let openSourceClientId = "X245A4XAIBGVM"
|
||||
|
||||
var authTask: Task<Void, Error>?
|
||||
let jsonDecoder = JSONDecoder()
|
||||
|
||||
@MainActor
|
||||
func setUserDefaultsValue(_ value: Any, forKey: String) {
|
||||
|
|
@ -26,8 +49,10 @@ public class RealDebrid {
|
|||
UserDefaults.standard.removeObject(forKey: forKey)
|
||||
}
|
||||
|
||||
// MARK: - Auth
|
||||
|
||||
// Fetches the device code from RD
|
||||
public func getVerificationInfo() async throws -> DeviceCodeResponse {
|
||||
public func getAuthUrl() async throws -> URL {
|
||||
var urlComponents = URLComponents(string: "\(baseAuthUrl)/device/code")!
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: openSourceClientId),
|
||||
|
|
@ -35,18 +60,28 @@ public class RealDebrid {
|
|||
]
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
throw RDError.InvalidUrl
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
|
||||
let request = URLRequest(url: url)
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// Validate the URL before doing anything else
|
||||
let rawResponse = try jsonDecoder.decode(DeviceCodeResponse.self, from: data)
|
||||
return rawResponse
|
||||
guard let directVerificationUrl = URL(string: rawResponse.directVerificationURL) else {
|
||||
throw DebridError.AuthQuery(description: "The verification URL is invalid")
|
||||
}
|
||||
|
||||
// Spawn the polling task separately
|
||||
authTask = Task {
|
||||
try await getDeviceCredentials(deviceCode: rawResponse.deviceCode)
|
||||
}
|
||||
|
||||
return directVerificationUrl
|
||||
} catch {
|
||||
print("Couldn't get the new client creds!")
|
||||
throw RDError.AuthQuery(description: error.localizedDescription)
|
||||
throw DebridError.AuthQuery(description: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,55 +94,49 @@ public class RealDebrid {
|
|||
]
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
throw RDError.InvalidUrl
|
||||
throw DebridError.InvalidUrl
|
||||
}
|
||||
|
||||
let request = URLRequest(url: url)
|
||||
|
||||
// Timer to poll RD API for credentials
|
||||
authTask = Task {
|
||||
var count = 0
|
||||
var count = 0
|
||||
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw RDError.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(DeviceCredentialsResponse.self, from: data)
|
||||
|
||||
// If there's a client ID from the response, end the task successfully
|
||||
if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret {
|
||||
await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId")
|
||||
FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
|
||||
try await getTokens(deviceCode: deviceCode)
|
||||
|
||||
return
|
||||
} else {
|
||||
try await Task.sleep(seconds: 5)
|
||||
count += 1
|
||||
}
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw DebridError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// We don't care if this fails
|
||||
let rawResponse = try? jsonDecoder.decode(DeviceCredentialsResponse.self, from: data)
|
||||
|
||||
// If there's a client ID from the response, end the task successfully
|
||||
if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret {
|
||||
await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId")
|
||||
FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
|
||||
try await getApiTokens(deviceCode: deviceCode)
|
||||
|
||||
return
|
||||
} else {
|
||||
try await Task.sleep(seconds: 5)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
if case let .failure(error) = await authTask?.result {
|
||||
throw error
|
||||
}
|
||||
throw DebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
}
|
||||
|
||||
// Fetch all tokens for the user and store in FerriteKeychain.shared
|
||||
public func getTokens(deviceCode: String) async throws {
|
||||
public func getApiTokens(deviceCode: String) async throws {
|
||||
guard let clientId = UserDefaults.standard.string(forKey: "RealDebrid.ClientId") else {
|
||||
throw RDError.EmptyData
|
||||
throw DebridError.EmptyData
|
||||
}
|
||||
|
||||
guard let clientSecret = FerriteKeychain.shared.get("RealDebrid.ClientSecret") else {
|
||||
throw RDError.EmptyData
|
||||
throw DebridError.EmptyData
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseAuthUrl)/token")!)
|
||||
|
|
@ -135,13 +164,13 @@ public class RealDebrid {
|
|||
await setUserDefaultsValue(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp")
|
||||
}
|
||||
|
||||
public func fetchToken() async -> String? {
|
||||
public func getToken() async -> String? {
|
||||
let accessTokenStamp = UserDefaults.standard.double(forKey: "RealDebrid.AccessTokenStamp")
|
||||
|
||||
if Date().timeIntervalSince1970 > accessTokenStamp {
|
||||
do {
|
||||
if let refreshToken = FerriteKeychain.shared.get("RealDebrid.RefreshToken") {
|
||||
try await getTokens(deviceCode: refreshToken)
|
||||
try await getApiTokens(deviceCode: refreshToken)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
|
|
@ -154,17 +183,16 @@ public class RealDebrid {
|
|||
|
||||
// Adds a manual API key instead of web auth
|
||||
// Clear out existing refresh tokens and timestamps
|
||||
public func setApiKey(_ key: String) -> Bool {
|
||||
public func setApiKey(_ key: String) {
|
||||
FerriteKeychain.shared.set(key, forKey: "RealDebrid.AccessToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.AccessTokenStamp")
|
||||
|
||||
UserDefaults.standard.set(true, forKey: "RealDebrid.UseManualKey")
|
||||
|
||||
return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key
|
||||
}
|
||||
|
||||
public func deleteTokens() async throws {
|
||||
// Deletes tokens from device and RD's servers
|
||||
public func logout() async {
|
||||
FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
|
||||
FerriteKeychain.shared.delete("RealDebrid.ClientSecret")
|
||||
await removeUserDefaultsValue(forKey: "RealDebrid.ClientId")
|
||||
|
|
@ -181,10 +209,12 @@ public class RealDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Common request
|
||||
|
||||
// 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 = await fetchToken() else {
|
||||
throw RDError.InvalidToken
|
||||
guard let token = await getToken() else {
|
||||
throw DebridError.InvalidToken
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
|
@ -192,24 +222,42 @@ public class RealDebrid {
|
|||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw RDError.FailedRequest(description: "No HTTP response given")
|
||||
throw DebridError.FailedRequest(description: "No HTTP response given")
|
||||
}
|
||||
|
||||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
try await deleteTokens()
|
||||
throw RDError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
|
||||
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
|
||||
} else {
|
||||
throw RDError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Instant availability
|
||||
|
||||
// Checks if the magnet is streamable on RD
|
||||
// Currently does not work for batch links
|
||||
public func instantAvailability(magnets: [Magnet]) async throws -> [IA] {
|
||||
var availableHashes: [RealDebrid.IA] = []
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.compactMap(\.hash).joined(separator: "/"))")!)
|
||||
public func instantAvailability(magnets: [Magnet]) async throws {
|
||||
let now = Date().timeIntervalSince1970
|
||||
|
||||
let sendMagnets = magnets.filter { magnet in
|
||||
if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) {
|
||||
if now > IAValues[IAIndex].expiryTimeStamp {
|
||||
IAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if sendMagnets.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(sendMagnets.compactMap(\.hash).joined(separator: "/"))")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
|
||||
|
|
@ -225,7 +273,7 @@ public class RealDebrid {
|
|||
continue
|
||||
}
|
||||
|
||||
// Is this a batch
|
||||
// Is this a batch?
|
||||
if data.rd.count > 1 || data.rd[0].count > 1 {
|
||||
// Batch array
|
||||
let batches = data.rd.map { fileDict in
|
||||
|
|
@ -237,22 +285,18 @@ public class RealDebrid {
|
|||
return RealDebrid.IABatch(files: batchFiles)
|
||||
}
|
||||
|
||||
// RD files array
|
||||
// Possibly sort this in the future, but not sure how at the moment
|
||||
var files: [RealDebrid.IAFile] = []
|
||||
var files: [DebridIAFile] = []
|
||||
|
||||
for index in batches.indices {
|
||||
let batchFiles = batches[index].files
|
||||
for batch in batches {
|
||||
let batchFileIds = batch.files.map(\.id)
|
||||
|
||||
for batchFileIndex in batchFiles.indices {
|
||||
let batchFile = batchFiles[batchFileIndex]
|
||||
|
||||
if !files.contains(where: { $0.name == batchFile.fileName }) {
|
||||
for batchFile in batch.files {
|
||||
if !files.contains(where: { $0.fileId == batchFile.id }) {
|
||||
files.append(
|
||||
RealDebrid.IAFile(
|
||||
DebridIAFile(
|
||||
fileId: batchFile.id,
|
||||
name: batchFile.fileName,
|
||||
batchIndex: index,
|
||||
batchFileIndex: batchFileIndex
|
||||
batchIds: batchFileIds
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -260,31 +304,65 @@ public class RealDebrid {
|
|||
}
|
||||
|
||||
// TTL: 5 minutes
|
||||
availableHashes.append(
|
||||
RealDebrid.IA(
|
||||
IAValues.append(
|
||||
DebridIA(
|
||||
magnet: Magnet(hash: hash, link: nil),
|
||||
source: id,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files,
|
||||
batches: batches
|
||||
files: files
|
||||
)
|
||||
)
|
||||
} else {
|
||||
availableHashes.append(
|
||||
RealDebrid.IA(
|
||||
IAValues.append(
|
||||
DebridIA(
|
||||
magnet: Magnet(hash: hash, link: nil),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300
|
||||
source: id,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: []
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableHashes
|
||||
// MARK: - Downloading
|
||||
|
||||
// Wrapper function to fetch a download link from the API
|
||||
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
|
||||
var selectedMagnetId = ""
|
||||
|
||||
do {
|
||||
// Don't queue a new job if the torrent already exists
|
||||
if let existingTorrent = cloudTorrents.first(where: { $0.hash == magnet.hash && $0.status == "downloaded" }) {
|
||||
selectedMagnetId = existingTorrent.torrentId
|
||||
} else {
|
||||
selectedMagnetId = try await addMagnet(magnet: magnet)
|
||||
|
||||
try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? [])
|
||||
}
|
||||
|
||||
// RealDebrid has 1 as the first ID for a file
|
||||
let torrentLink = try await torrentInfo(
|
||||
debridID: selectedMagnetId,
|
||||
selectedFileId: iaFile?.fileId ?? 1
|
||||
)
|
||||
let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
return downloadLink
|
||||
} catch {
|
||||
if case DebridError.EmptyTorrents = error, !selectedMagnetId.isEmpty {
|
||||
try? await deleteTorrent(torrentId: selectedMagnetId)
|
||||
}
|
||||
|
||||
// Re-raise the error to the calling function
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a magnet link to the user's RD account
|
||||
public func addMagnet(magnet: Magnet) async throws -> String {
|
||||
guard let magnetLink = magnet.link else {
|
||||
throw RDError.FailedRequest(description: "The magnet link is invalid")
|
||||
throw DebridError.FailedRequest(description: "The magnet link is invalid")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/addMagnet")!)
|
||||
|
|
@ -323,40 +401,24 @@ public class RealDebrid {
|
|||
}
|
||||
|
||||
// Gets the info of a torrent from a given ID
|
||||
public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String {
|
||||
public func torrentInfo(debridID: String, selectedFileId: Int?) async throws -> String {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
|
||||
let filteredFiles = rawResponse.files.filter { $0.selected == 1 }
|
||||
let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedFileId })
|
||||
|
||||
// Let the user know if a torrent is downloading
|
||||
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" {
|
||||
if let torrentLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" {
|
||||
return torrentLink
|
||||
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
|
||||
throw RDError.EmptyTorrents
|
||||
throw DebridError.IsCaching
|
||||
} else {
|
||||
throw RDError.EmptyData
|
||||
throw DebridError.EmptyTorrents
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the user's torrent library
|
||||
public func userTorrents() async throws -> [UserTorrentsResponse] {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data)
|
||||
|
||||
return rawResponse
|
||||
}
|
||||
|
||||
// Deletes a torrent download from RD
|
||||
public func deleteTorrent(debridID: String) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
// Downloads link from selectFiles for playback
|
||||
public func unrestrictLink(debridDownloadLink: String) async throws -> String {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/unrestrict/link")!)
|
||||
|
|
@ -374,18 +436,67 @@ public class RealDebrid {
|
|||
return rawResponse.download
|
||||
}
|
||||
|
||||
// MARK: - Cloud methods
|
||||
|
||||
// Gets the user's torrent library
|
||||
public func getUserTorrents() async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data)
|
||||
cloudTorrents = rawResponse.map { response in
|
||||
DebridCloudTorrent(
|
||||
torrentId: response.id,
|
||||
source: self.id,
|
||||
fileName: response.filename,
|
||||
status: response.status,
|
||||
hash: response.hash,
|
||||
links: response.links
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Deletes a torrent download from RD
|
||||
public func deleteTorrent(torrentId: String?) async throws {
|
||||
let deleteId: String
|
||||
|
||||
if let torrentId {
|
||||
deleteId = torrentId
|
||||
} else {
|
||||
// Refresh the torrent cloud
|
||||
// The first file is the currently caching one
|
||||
let _ = try await getUserTorrents()
|
||||
guard let firstTorrent = cloudTorrents[safe: -1] else {
|
||||
throw DebridError.EmptyTorrents
|
||||
}
|
||||
|
||||
deleteId = firstTorrent.torrentId
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(deleteId)")!)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
// Gets the user's downloads
|
||||
public func userDownloads() async throws -> [UserDownloadsResponse] {
|
||||
public func getUserDownloads() async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data)
|
||||
|
||||
return rawResponse
|
||||
cloudDownloads = rawResponse.map { response in
|
||||
DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download)
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteDownload(debridID: String) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(debridID)")!)
|
||||
// Not used
|
||||
public func checkUserDownloads(link: String) -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
public func deleteDownload(downloadId: String) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(downloadId)")!)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
|
|
|
|||
|
|
@ -8,20 +8,6 @@
|
|||
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
|
||||
|
|
@ -166,17 +152,4 @@ public extension AllDebrid {
|
|||
case name = "n"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailablity client side structures
|
||||
|
||||
struct IA: Codable, Hashable {
|
||||
let magnet: Magnet
|
||||
let expiryTimeStamp: Double
|
||||
var files: [IAFile]
|
||||
}
|
||||
|
||||
struct IAFile: Codable, Hashable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
Ferrite/Models/DebridModels.swift
Normal file
57
Ferrite/Models/DebridModels.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// DebridModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 6/2/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct DebridIA: Hashable, Sendable {
|
||||
let magnet: Magnet
|
||||
let source: String
|
||||
let expiryTimeStamp: Double
|
||||
var files: [DebridIAFile]
|
||||
}
|
||||
|
||||
public struct DebridIAFile: Hashable, Sendable {
|
||||
let fileId: Int
|
||||
let name: String
|
||||
let streamUrlString: String?
|
||||
let batchIds: [Int]
|
||||
|
||||
init(fileId: Int, name: String, streamUrlString: String? = nil, batchIds: [Int] = []) {
|
||||
self.fileId = fileId
|
||||
self.name = name
|
||||
self.streamUrlString = streamUrlString
|
||||
self.batchIds = batchIds
|
||||
}
|
||||
}
|
||||
|
||||
public struct DebridCloudDownload: Hashable, Sendable {
|
||||
let downloadId: String
|
||||
let source: String
|
||||
let fileName: String
|
||||
let link: String
|
||||
}
|
||||
|
||||
public struct DebridCloudTorrent: Hashable, Sendable {
|
||||
let torrentId: String
|
||||
let source: String
|
||||
let fileName: String
|
||||
let status: String
|
||||
let hash: String
|
||||
let links: [String]
|
||||
}
|
||||
|
||||
public enum DebridError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case IsCaching
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
|
@ -8,20 +8,6 @@
|
|||
import Foundation
|
||||
|
||||
public extension Premiumize {
|
||||
// MARK: - Errors
|
||||
|
||||
// TODO: Hybridize debrid errors in one structure
|
||||
enum PMError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
||||
// MARK: - CacheCheckResponse
|
||||
|
||||
struct CacheCheckResponse: Codable {
|
||||
|
|
@ -33,7 +19,7 @@ public extension Premiumize {
|
|||
|
||||
struct DDLResponse: Codable {
|
||||
let status: String
|
||||
let content: [DDLData]
|
||||
let content: [DDLData]?
|
||||
let location: String
|
||||
let filename: String
|
||||
let filesize: Int
|
||||
|
|
@ -51,19 +37,6 @@ public extension Premiumize {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailability client side structures
|
||||
|
||||
struct IA: Codable, Hashable {
|
||||
let magnet: Magnet
|
||||
let expiryTimeStamp: Double
|
||||
let files: [IAFile]
|
||||
}
|
||||
|
||||
struct IAFile: Codable, Hashable {
|
||||
let name: String
|
||||
let streamUrlString: String
|
||||
}
|
||||
|
||||
// MARK: - AllItemsResponse (listall endpoint)
|
||||
|
||||
struct AllItemsResponse: Codable {
|
||||
|
|
|
|||
|
|
@ -9,20 +9,6 @@
|
|||
import Foundation
|
||||
|
||||
public extension RealDebrid {
|
||||
// MARK: - Errors
|
||||
|
||||
// TODO: Hybridize debrid errors in one structure
|
||||
enum RDError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
||||
// MARK: - device code endpoint
|
||||
|
||||
struct DeviceCodeResponse: Codable, Sendable {
|
||||
|
|
@ -90,14 +76,7 @@ public extension RealDebrid {
|
|||
var filesize: Int
|
||||
}
|
||||
|
||||
// MARK: - Instant Availability client side structures
|
||||
|
||||
struct IA: Codable, Hashable, Sendable {
|
||||
let magnet: Magnet
|
||||
let expiryTimeStamp: Double
|
||||
var files: [IAFile] = []
|
||||
var batches: [IABatch] = []
|
||||
}
|
||||
// MARK: - Instant Availability batch structures (used for client-side conversion)
|
||||
|
||||
struct IABatch: Codable, Hashable, Sendable {
|
||||
let files: [IABatchFile]
|
||||
|
|
@ -108,12 +87,6 @@ public extension RealDebrid {
|
|||
let fileName: String
|
||||
}
|
||||
|
||||
struct IAFile: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let batchIndex: Int
|
||||
let batchFileIndex: Int
|
||||
}
|
||||
|
||||
// MARK: - addMagnet endpoint
|
||||
|
||||
struct AddMagnetResponse: Codable, Sendable {
|
||||
|
|
|
|||
68
Ferrite/Protocols/Debrid.swift
Normal file
68
Ferrite/Protocols/Debrid.swift
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// Debrid.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 6/1/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol DebridSource: AnyObservableObject {
|
||||
// ID of the service
|
||||
// var id: DebridInfo { get }
|
||||
var id: String { get }
|
||||
var abbreviation: String { get }
|
||||
var website: String { get }
|
||||
|
||||
// Auth variables
|
||||
var authProcessing: Bool { get set }
|
||||
var isLoggedIn: Bool { get }
|
||||
|
||||
// Manual API key
|
||||
var manualToken: String? { get }
|
||||
|
||||
// Common authentication functions
|
||||
func setApiKey(_ key: String)
|
||||
func logout() async
|
||||
|
||||
// Instant availability variables
|
||||
var IAValues: [DebridIA] { get set }
|
||||
|
||||
// Instant availability functions
|
||||
func instantAvailability(magnets: [Magnet]) async throws
|
||||
|
||||
// Fetches a download link from a source
|
||||
// Include the instant availability information with the args
|
||||
// Torrents also checked here
|
||||
func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String
|
||||
|
||||
// Cloud variables
|
||||
var cloudDownloads: [DebridCloudDownload] { get set }
|
||||
var cloudTorrents: [DebridCloudTorrent] { get set }
|
||||
var cloudTTL: Double { get set }
|
||||
|
||||
// User downloads functions
|
||||
func getUserDownloads() async throws
|
||||
func checkUserDownloads(link: String) async throws -> String?
|
||||
func deleteDownload(downloadId: String) async throws
|
||||
|
||||
// User torrent functions
|
||||
func getUserTorrents() async throws
|
||||
func deleteTorrent(torrentId: String?) async throws
|
||||
}
|
||||
|
||||
public protocol PollingDebridSource: DebridSource {
|
||||
// Task reference for polling
|
||||
var authTask: Task<Void, Error>? { get set }
|
||||
|
||||
// Fetches the Auth URL
|
||||
func getAuthUrl() async throws -> URL
|
||||
}
|
||||
|
||||
public protocol OAuthDebridSource: DebridSource {
|
||||
// Fetches the auth URL
|
||||
func getAuthUrl() throws -> URL
|
||||
|
||||
// Handles an OAuth callback
|
||||
func handleAuthCallback(url: URL) throws
|
||||
}
|
||||
147
Ferrite/Utils/Store.swift
Normal file
147
Ferrite/Utils/Store.swift
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
//
|
||||
// Store.swift
|
||||
// Ferrite
|
||||
//
|
||||
//
|
||||
// Originally created by William Baker on 09/06/2022.
|
||||
// https://github.com/Tiny-Home-Consulting/Dependiject/blob/master/Dependiject/Store.swift
|
||||
// Copyright (c) 2022 Tiny Home Consulting LLC. All rights reserved.
|
||||
//
|
||||
// Combined together by Brian Dashore
|
||||
//
|
||||
// TODO: Replace with Observable when minVersion >= iOS 17
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
class ErasedObservableObject: ObservableObject {
|
||||
let objectWillChange: AnyPublisher<Void, Never>
|
||||
|
||||
init(objectWillChange: AnyPublisher<Void, Never>) {
|
||||
self.objectWillChange = objectWillChange
|
||||
}
|
||||
|
||||
static func empty() -> ErasedObservableObject {
|
||||
.init(objectWillChange: Empty().eraseToAnyPublisher())
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AnyObservableObject: AnyObject {
|
||||
var objectWillChange: ObservableObjectPublisher { get }
|
||||
}
|
||||
|
||||
// The generic type names were chosen to match the SwiftUI equivalents:
|
||||
// - ObjectType from StateObject<ObjectType> and ObservedObject<ObjectType>
|
||||
// - Subject from ObservedObject.Wrapper.subscript<Subject>(dynamicMember:)
|
||||
// - S from Publisher.receive<S>(on:options:)
|
||||
|
||||
/// A property wrapper used to wrap injected observable objects.
|
||||
///
|
||||
/// This is similar to SwiftUI's
|
||||
/// [`StateObject`](https://developer.apple.com/documentation/swiftui/stateobject), but without
|
||||
/// compile-time type restrictions. The lack of compile-time restrictions means that `ObjectType`
|
||||
/// may be a protocol rather than a class.
|
||||
///
|
||||
/// - Important: At runtime, the wrapped value must conform to ``AnyObservableObject``.
|
||||
///
|
||||
/// To pass properties of the observable object down the view hierarchy as bindings, use the
|
||||
/// projected value:
|
||||
/// ```swift
|
||||
/// struct ExampleView: View {
|
||||
/// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self)
|
||||
///
|
||||
/// var body: some View {
|
||||
/// TextField("username", text: $viewModel.username)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// Not all injected objects need this property wrapper. See the example projects for examples each
|
||||
/// way.
|
||||
@propertyWrapper
|
||||
public struct Store<ObjectType> {
|
||||
/// The underlying object being stored.
|
||||
public let wrappedValue: ObjectType
|
||||
|
||||
// See https://github.com/Tiny-Home-Consulting/Dependiject/issues/38
|
||||
fileprivate var _observableObject: ObservedObject<ErasedObservableObject>
|
||||
|
||||
@MainActor internal var observableObject: ErasedObservableObject {
|
||||
_observableObject.wrappedValue
|
||||
}
|
||||
|
||||
/// A projected value which has the same properties as the wrapped value, but presented as
|
||||
/// bindings.
|
||||
///
|
||||
/// Use this to pass bindings down the view hierarchy:
|
||||
/// ```swift
|
||||
/// struct ExampleView: View {
|
||||
/// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self)
|
||||
///
|
||||
/// var body: some View {
|
||||
/// TextField("username", text: $viewModel.username)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
public var projectedValue: Wrapper {
|
||||
Wrapper(self)
|
||||
}
|
||||
|
||||
/// Create a stored value on a custom scheduler.
|
||||
///
|
||||
/// Use this init to schedule updates on a specific scheduler other than `DispatchQueue.main`.
|
||||
public init<S: Scheduler>(wrappedValue: ObjectType,
|
||||
on scheduler: S,
|
||||
schedulerOptions: S.SchedulerOptions? = nil)
|
||||
{
|
||||
self.wrappedValue = wrappedValue
|
||||
|
||||
if let observable = wrappedValue as? AnyObservableObject {
|
||||
let objectWillChange = observable.objectWillChange
|
||||
.receive(on: scheduler, options: schedulerOptions)
|
||||
.eraseToAnyPublisher()
|
||||
_observableObject = .init(initialValue: .init(objectWillChange: objectWillChange))
|
||||
} else {
|
||||
assertionFailure(
|
||||
"Only use the Store property wrapper with objects conforming to AnyObservableObject."
|
||||
)
|
||||
_observableObject = .init(initialValue: .empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a stored value which publishes on the main thread.
|
||||
///
|
||||
/// To control when updates are published, see ``init(wrappedValue:on:schedulerOptions:)``.
|
||||
public init(wrappedValue: ObjectType) {
|
||||
self.init(wrappedValue: wrappedValue, on: DispatchQueue.main)
|
||||
}
|
||||
|
||||
/// An equivalent to SwiftUI's
|
||||
/// [`ObservedObject.Wrapper`](https://developer.apple.com/documentation/swiftui/observedobject/wrapper)
|
||||
/// type.
|
||||
@dynamicMemberLookup
|
||||
public struct Wrapper {
|
||||
private var store: Store
|
||||
|
||||
internal init(_ store: Store<ObjectType>) {
|
||||
self.store = store
|
||||
}
|
||||
|
||||
/// Returns a binding to the resulting value of a given key path.
|
||||
public subscript<Subject>(
|
||||
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
|
||||
) -> Binding<Subject> {
|
||||
Binding {
|
||||
self.store.wrappedValue[keyPath: keyPath]
|
||||
} set: {
|
||||
self.store.wrappedValue[keyPath: keyPath] = $0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Store: DynamicProperty {
|
||||
public nonisolated mutating func update() {
|
||||
_observableObject.update()
|
||||
}
|
||||
}
|
||||
|
|
@ -12,26 +12,35 @@ import SwiftUI
|
|||
public class DebridManager: ObservableObject {
|
||||
// Linked classes
|
||||
var logManager: LoggingManager?
|
||||
let realDebrid: RealDebrid = .init()
|
||||
let allDebrid: AllDebrid = .init()
|
||||
let premiumize: Premiumize = .init()
|
||||
@Published var realDebrid: RealDebrid = .init()
|
||||
@Published var allDebrid: AllDebrid = .init()
|
||||
@Published var premiumize: Premiumize = .init()
|
||||
|
||||
lazy var debridSources: [DebridSource] = [realDebrid, allDebrid, premiumize]
|
||||
|
||||
// UI Variables
|
||||
@Published var showWebView: Bool = false
|
||||
@Published var showAuthSession: Bool = false
|
||||
|
||||
// Service agnostic variables
|
||||
@Published var enabledDebrids: Set<DebridType> = [] {
|
||||
var hasEnabledDebrids: Bool {
|
||||
debridSources.contains { $0.isLoggedIn }
|
||||
}
|
||||
|
||||
var enabledDebridCount: Int {
|
||||
debridSources.filter(\.isLoggedIn).count
|
||||
}
|
||||
|
||||
@Published var selectedDebridSource: DebridSource? {
|
||||
didSet {
|
||||
UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray")
|
||||
UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var selectedDebridType: DebridType? {
|
||||
didSet {
|
||||
UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService")
|
||||
}
|
||||
}
|
||||
var selectedDebridItem: DebridIA?
|
||||
var selectedDebridFile: DebridIAFile?
|
||||
|
||||
// TODO: Figure out a way to remove this var
|
||||
var selectedOAuthDebridSource: OAuthDebridSource?
|
||||
|
||||
@Published var filteredIAStatus: Set<IAStatus> = []
|
||||
|
||||
|
|
@ -39,104 +48,46 @@ public class DebridManager: ObservableObject {
|
|||
var downloadUrl: String = ""
|
||||
var authUrl: URL?
|
||||
|
||||
// Is the current debrid type processing an auth request
|
||||
func authProcessing(_ passedDebridType: DebridType?) -> Bool {
|
||||
guard let debridType = passedDebridType ?? selectedDebridType else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
return realDebridAuthProcessing
|
||||
case .allDebrid:
|
||||
return allDebridAuthProcessing
|
||||
case .premiumize:
|
||||
return premiumizeAuthProcessing
|
||||
}
|
||||
}
|
||||
|
||||
// RealDebrid auth variables
|
||||
var realDebridAuthProcessing: Bool = false
|
||||
|
||||
// RealDebrid fetch variables
|
||||
@Published var realDebridIAValues: [RealDebrid.IA] = []
|
||||
|
||||
@Published var showDeleteAlert: Bool = false
|
||||
|
||||
var selectedRealDebridItem: RealDebrid.IA?
|
||||
var selectedRealDebridFile: RealDebrid.IAFile?
|
||||
var selectedRealDebridID: String?
|
||||
|
||||
// TODO: Maybe make these generic?
|
||||
// RealDebrid cloud variables
|
||||
@Published var realDebridCloudTorrents: [RealDebrid.UserTorrentsResponse] = []
|
||||
@Published var realDebridCloudDownloads: [RealDebrid.UserDownloadsResponse] = []
|
||||
var realDebridCloudTTL: Double = 0.0
|
||||
|
||||
// AllDebrid auth variables
|
||||
var allDebridAuthProcessing: Bool = false
|
||||
|
||||
// AllDebrid fetch variables
|
||||
@Published var allDebridIAValues: [AllDebrid.IA] = []
|
||||
|
||||
var selectedAllDebridItem: AllDebrid.IA?
|
||||
var selectedAllDebridFile: AllDebrid.IAFile?
|
||||
|
||||
// AllDebrid cloud variables
|
||||
@Published var allDebridCloudMagnets: [AllDebrid.MagnetStatusData] = []
|
||||
@Published var allDebridCloudLinks: [AllDebrid.SavedLink] = []
|
||||
var allDebridCloudTTL: Double = 0.0
|
||||
|
||||
// Premiumize auth variables
|
||||
var premiumizeAuthProcessing: Bool = false
|
||||
|
||||
// Premiumize fetch variables
|
||||
@Published var premiumizeIAValues: [Premiumize.IA] = []
|
||||
|
||||
var selectedPremiumizeItem: Premiumize.IA?
|
||||
var selectedPremiumizeFile: Premiumize.IAFile?
|
||||
|
||||
// Premiumize cloud variables
|
||||
@Published var premiumizeCloudItems: [Premiumize.UserItem] = []
|
||||
var premiumizeCloudTTL: Double = 0.0
|
||||
|
||||
init() {
|
||||
if let rawDebridList = UserDefaults.standard.string(forKey: "Debrid.EnabledArray"),
|
||||
let serializedDebridList = Set<DebridType>(rawValue: rawDebridList)
|
||||
{
|
||||
enabledDebrids = serializedDebridList
|
||||
}
|
||||
// Set the preferred service. Contains migration logic for earlier versions
|
||||
if let rawPreferredService = UserDefaults.standard.string(forKey: "Debrid.PreferredService") {
|
||||
let debridServiceId: String?
|
||||
|
||||
// If a UserDefaults integer isn't set, it's usually 0
|
||||
let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService")
|
||||
selectedDebridType = DebridType(rawValue: rawPreferredService)
|
||||
if let preferredServiceInt = Int(rawPreferredService) {
|
||||
debridServiceId = migratePreferredService(preferredServiceInt)
|
||||
} else {
|
||||
debridServiceId = rawPreferredService
|
||||
}
|
||||
|
||||
// If a user has one logged in service, automatically set the preferred service to that one
|
||||
if enabledDebrids.count == 1 {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
// Only set the debrid source if it's logged in
|
||||
// Otherwise remove the key
|
||||
let tempDebridSource = debridSources.first { $0.id == debridServiceId }
|
||||
if tempDebridSource?.isLoggedIn ?? false {
|
||||
selectedDebridSource = tempDebridSource
|
||||
} else {
|
||||
UserDefaults.standard.removeObject(forKey: "Debrid.PreferredService")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
// TODO: Remove after v0.8.0
|
||||
// Function to migrate the preferred service to the new string ID format
|
||||
public func migratePreferredService(_ idInt: Int) -> String? {
|
||||
// Undo the EnabledDebrids key
|
||||
UserDefaults.standard.removeObject(forKey: "Debrid.EnabledArray")
|
||||
|
||||
let allDebridEnabled = UserDefaults.standard.bool(forKey: "AllDebrid.Enabled")
|
||||
if allDebridEnabled {
|
||||
enabledDebrids.insert(.allDebrid)
|
||||
UserDefaults.standard.set(false, forKey: "AllDebrid.Enabled")
|
||||
}
|
||||
|
||||
let premiumizeEnabled = UserDefaults.standard.bool(forKey: "Premiumize.Enabled")
|
||||
if premiumizeEnabled {
|
||||
enabledDebrids.insert(.premiumize)
|
||||
UserDefaults.standard.set(false, forKey: "Premiumize.Enabled")
|
||||
}
|
||||
return DebridType(rawValue: idInt)?.toString()
|
||||
}
|
||||
|
||||
// Wrapper function to match error descriptions
|
||||
|
|
@ -169,103 +120,29 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
// Cleans all cached IA values in the event of a full IA refresh
|
||||
public func clearIAValues() {
|
||||
realDebridIAValues = []
|
||||
allDebridIAValues = []
|
||||
premiumizeIAValues = []
|
||||
for debridSource in debridSources {
|
||||
debridSource.IAValues = []
|
||||
}
|
||||
}
|
||||
|
||||
// Clears all selected files and items
|
||||
public func clearSelectedDebridItems() {
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
selectedRealDebridFile = nil
|
||||
selectedRealDebridItem = nil
|
||||
case .allDebrid:
|
||||
selectedAllDebridFile = nil
|
||||
selectedAllDebridItem = nil
|
||||
case .premiumize:
|
||||
selectedPremiumizeFile = nil
|
||||
selectedPremiumizeItem = nil
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
selectedDebridItem = nil
|
||||
selectedDebridFile = nil
|
||||
}
|
||||
|
||||
// Common function to populate hashes for debrid services
|
||||
public func populateDebridIA(_ resultMagnets: [Magnet]) async {
|
||||
let now = Date()
|
||||
|
||||
// If a hash isn't found in the IA, update it
|
||||
// If the hash is expired, remove it and update it
|
||||
let sendMagnets = resultMagnets.filter { magnet in
|
||||
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.magnet.hash == magnet.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.magnet.hash == magnet.hash }), enabledDebrids.contains(.allDebrid) {
|
||||
if now.timeIntervalSince1970 > allDebridIAValues[IAIndex].expiryTimeStamp {
|
||||
allDebridIAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if let IAIndex = premiumizeIAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.premiumize) {
|
||||
if now.timeIntervalSince1970 > premiumizeIAValues[IAIndex].expiryTimeStamp {
|
||||
premiumizeIAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Don't exit the function if the API fetch errors
|
||||
if !sendMagnets.isEmpty {
|
||||
if enabledDebrids.contains(.realDebrid) {
|
||||
do {
|
||||
let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnets: sendMagnets)
|
||||
realDebridIAValues += fetchedRealDebridIA
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid IA fetch error")
|
||||
}
|
||||
for debridSource in debridSources {
|
||||
if !debridSource.isLoggedIn {
|
||||
continue
|
||||
}
|
||||
|
||||
if enabledDebrids.contains(.allDebrid) {
|
||||
do {
|
||||
let fetchedAllDebridIA = try await allDebrid.instantAvailability(magnets: sendMagnets)
|
||||
allDebridIAValues += fetchedAllDebridIA
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid IA fetch error")
|
||||
}
|
||||
}
|
||||
|
||||
if enabledDebrids.contains(.premiumize) {
|
||||
do {
|
||||
// Only strip magnets that don't have an associated link for PM
|
||||
let strippedResultMagnets: [Magnet] = resultMagnets.compactMap {
|
||||
if let magnetLink = $0.link {
|
||||
return Magnet(hash: $0.hash, link: magnetLink)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let availableMagnets = try await premiumize.divideCacheRequests(magnets: strippedResultMagnets)
|
||||
|
||||
// Split DDL requests into chunks of 10
|
||||
for chunk in availableMagnets.chunked(into: 10) {
|
||||
let tempIA = try await premiumize.divideDDLRequests(magnetChunk: chunk)
|
||||
|
||||
premiumizeIAValues += tempIA
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "Premiumize IA fetch error")
|
||||
}
|
||||
// Don't exit the function if the API fetch errors
|
||||
do {
|
||||
try await debridSource.instantAvailability(magnets: resultMagnets)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "\(debridSource.id) IA fetch error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -276,38 +153,11 @@ public class DebridManager: ObservableObject {
|
|||
return .none
|
||||
}
|
||||
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
guard let realDebridMatch = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if realDebridMatch.batches.isEmpty {
|
||||
return .full
|
||||
} else {
|
||||
return .partial
|
||||
}
|
||||
case .allDebrid:
|
||||
guard let allDebridMatch = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if allDebridMatch.files.count > 1 {
|
||||
return .partial
|
||||
} else {
|
||||
return .full
|
||||
}
|
||||
case .premiumize:
|
||||
guard let premiumizeMatch = premiumizeIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if premiumizeMatch.files.count > 1 {
|
||||
return .partial
|
||||
} else {
|
||||
return .full
|
||||
}
|
||||
case .none:
|
||||
if let selectedDebridSource,
|
||||
let match = selectedDebridSource.IAValues.first(where: { magnetHash == $0.magnet.hash })
|
||||
{
|
||||
return match.files.count > 1 ? .partial : .full
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
|
@ -318,32 +168,15 @@ public class DebridManager: ObservableObject {
|
|||
return false
|
||||
}
|
||||
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) {
|
||||
selectedRealDebridItem = realDebridItem
|
||||
return true
|
||||
} else {
|
||||
logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .allDebrid:
|
||||
if let allDebridItem = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) {
|
||||
selectedAllDebridItem = allDebridItem
|
||||
return true
|
||||
} else {
|
||||
logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .premiumize:
|
||||
if let premiumizeItem = premiumizeIAValues.first(where: { magnetHash == $0.magnet.hash }) {
|
||||
selectedPremiumizeItem = premiumizeItem
|
||||
return true
|
||||
} else {
|
||||
logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .none:
|
||||
guard let selectedSource = selectedDebridSource else {
|
||||
return false
|
||||
}
|
||||
|
||||
if let IAItem = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
|
||||
selectedDebridItem = IAItem
|
||||
return true
|
||||
} else {
|
||||
logManager?.error("DebridManager: Could not find the associated \(selectedSource.id) entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -351,73 +184,62 @@ public class DebridManager: ObservableObject {
|
|||
// MARK: - Authentication UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to authenticate with
|
||||
public func authenticateDebrid(debridType: DebridType, apiKey: String?) async {
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
let success = apiKey == nil ? await authenticateRd() : realDebrid.setApiKey(apiKey!)
|
||||
completeDebridAuth(debridType, success: success)
|
||||
case .allDebrid:
|
||||
// Async can't work with nil mapping method
|
||||
let success = apiKey == nil ? await authenticateAd() : allDebrid.setApiKey(apiKey!)
|
||||
completeDebridAuth(debridType, success: success)
|
||||
case .premiumize:
|
||||
if let apiKey {
|
||||
let success = premiumize.setApiKey(apiKey)
|
||||
completeDebridAuth(debridType, success: success)
|
||||
} else {
|
||||
await authenticatePm()
|
||||
public func authenticateDebrid(_ debridSource: some DebridSource, apiKey: String?) async {
|
||||
defer {
|
||||
// Don't cancel processing if using OAuth
|
||||
if !(debridSource is OAuthDebridSource) {
|
||||
debridSource.authProcessing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback to finish debrid auth since functions can be split
|
||||
func completeDebridAuth(_ debridType: DebridType, success: Bool) {
|
||||
if success {
|
||||
enabledDebrids.insert(debridType)
|
||||
if enabledDebrids.count == 1 {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
if enabledDebridCount == 1 {
|
||||
selectedDebridSource = debridSource
|
||||
}
|
||||
}
|
||||
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
realDebridAuthProcessing = false
|
||||
case .allDebrid:
|
||||
allDebridAuthProcessing = false
|
||||
case .premiumize:
|
||||
premiumizeAuthProcessing = false
|
||||
// Set an API key if manually provided
|
||||
if let apiKey {
|
||||
debridSource.setApiKey(apiKey)
|
||||
return
|
||||
}
|
||||
|
||||
// Processing has started
|
||||
debridSource.authProcessing = true
|
||||
|
||||
if let pollingSource = debridSource as? PollingDebridSource {
|
||||
do {
|
||||
let authUrl = try await pollingSource.getAuthUrl()
|
||||
|
||||
if validateAuthUrl(authUrl) {
|
||||
try await pollingSource.authTask?.value
|
||||
} else {
|
||||
throw DebridError.AuthQuery(description: "The authentication URL was invalid")
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "\(debridSource.id) authentication error")
|
||||
|
||||
pollingSource.authTask?.cancel()
|
||||
}
|
||||
} else if let oauthSource = debridSource as? OAuthDebridSource {
|
||||
do {
|
||||
let tempAuthUrl = try oauthSource.getAuthUrl()
|
||||
selectedOAuthDebridSource = oauthSource
|
||||
|
||||
validateAuthUrl(tempAuthUrl, useAuthSession: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "\(debridSource.id) authentication error")
|
||||
}
|
||||
} else {
|
||||
logManager?.error(
|
||||
"DebridManager: Auth: Could not figure out the authentication type for \(debridSource.id). Is this configured properly?"
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get a truncated manual API key if it's being used
|
||||
func getManualAuthKey(_ passedDebridType: DebridType?) async -> String? {
|
||||
guard let debridType = passedDebridType ?? selectedDebridType else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let debridToken: String?
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") {
|
||||
debridToken = FerriteKeychain.shared.get("RealDebrid.AccessToken")
|
||||
} else {
|
||||
debridToken = nil
|
||||
}
|
||||
case .allDebrid:
|
||||
if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") {
|
||||
debridToken = FerriteKeychain.shared.get("AllDebrid.ApiKey")
|
||||
} else {
|
||||
debridToken = nil
|
||||
}
|
||||
case .premiumize:
|
||||
if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") {
|
||||
debridToken = FerriteKeychain.shared.get("Premiumize.AccessToken")
|
||||
} else {
|
||||
debridToken = nil
|
||||
}
|
||||
}
|
||||
|
||||
if let debridToken {
|
||||
func getManualAuthKey(_ debridSource: some DebridSource) async -> String? {
|
||||
if let debridToken = debridSource.manualToken {
|
||||
let splitString = debridToken.suffix(4)
|
||||
|
||||
if debridToken.count > 4 {
|
||||
|
|
@ -447,118 +269,43 @@ public class DebridManager: ObservableObject {
|
|||
return true
|
||||
}
|
||||
|
||||
private func authenticateRd() async -> Bool {
|
||||
do {
|
||||
realDebridAuthProcessing = true
|
||||
let verificationResponse = try await realDebrid.getVerificationInfo()
|
||||
|
||||
if validateAuthUrl(URL(string: verificationResponse.directVerificationURL)) {
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
return true
|
||||
} else {
|
||||
throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid")
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid authentication error")
|
||||
|
||||
realDebrid.authTask?.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticateAd() async -> Bool {
|
||||
do {
|
||||
allDebridAuthProcessing = true
|
||||
let pinResponse = try await allDebrid.getPinInfo()
|
||||
|
||||
if validateAuthUrl(URL(string: pinResponse.userURL)) {
|
||||
try await allDebrid.getApiKey(checkID: pinResponse.check, pin: pinResponse.pin)
|
||||
return true
|
||||
} else {
|
||||
throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid")
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid authentication error")
|
||||
|
||||
allDebrid.authTask?.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticatePm() async {
|
||||
do {
|
||||
premiumizeAuthProcessing = true
|
||||
let tempAuthUrl = try premiumize.buildAuthUrl()
|
||||
|
||||
validateAuthUrl(tempAuthUrl, useAuthSession: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "Premiumize authentication error")
|
||||
|
||||
completeDebridAuth(.premiumize, success: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Currently handles Premiumize callback
|
||||
public func handleCallback(url: URL?, error: Error?) async {
|
||||
public func handleAuthCallback(url: URL?, error: Error?) async {
|
||||
defer {
|
||||
if enabledDebridCount == 1 {
|
||||
selectedDebridSource = selectedOAuthDebridSource
|
||||
}
|
||||
|
||||
selectedOAuthDebridSource?.authProcessing = false
|
||||
}
|
||||
|
||||
do {
|
||||
guard let oauthDebridSource = selectedOAuthDebridSource else {
|
||||
throw DebridError.AuthQuery(description: "OAuth source couldn't be found for callback. Aborting.")
|
||||
}
|
||||
|
||||
if let error {
|
||||
throw Premiumize.PMError.AuthQuery(description: "OAuth callback Error: \(error)")
|
||||
throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)")
|
||||
}
|
||||
|
||||
if let callbackUrl = url {
|
||||
try premiumize.handleAuthCallback(url: callbackUrl)
|
||||
completeDebridAuth(.premiumize, success: true)
|
||||
try oauthDebridSource.handleAuthCallback(url: callbackUrl)
|
||||
} else {
|
||||
throw Premiumize.PMError.AuthQuery(description: "The callback URL was invalid")
|
||||
throw DebridError.AuthQuery(description: "The callback URL was invalid")
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "Premiumize authentication error (callback)")
|
||||
|
||||
completeDebridAuth(.premiumize, success: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Logout UI linked functions
|
||||
// MARK: - Logout UI 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()
|
||||
case .premiumize:
|
||||
logoutPm()
|
||||
public func logout(_ debridSource: some DebridSource) async {
|
||||
await debridSource.logout()
|
||||
|
||||
if selectedDebridSource?.id == debridSource.id {
|
||||
selectedDebridSource = nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
enabledDebrids.remove(.realDebrid)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid logout error")
|
||||
}
|
||||
}
|
||||
|
||||
private func logoutAd() {
|
||||
allDebrid.deleteTokens()
|
||||
enabledDebrids.remove(.allDebrid)
|
||||
|
||||
logManager?.info(
|
||||
"AllDebrid: Logged out, API key needs to be removed",
|
||||
description: "Please manually delete the AllDebrid API key"
|
||||
)
|
||||
}
|
||||
|
||||
private func logoutPm() {
|
||||
premiumize.deleteTokens()
|
||||
enabledDebrids.remove(.premiumize)
|
||||
}
|
||||
|
||||
// MARK: - Debrid fetch UI linked functions
|
||||
|
|
@ -576,299 +323,89 @@ public class DebridManager: ObservableObject {
|
|||
self.currentDebridTask = nil
|
||||
})
|
||||
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
await fetchRdDownload(magnet: magnet, existingLink: cloudInfo)
|
||||
case .allDebrid:
|
||||
await fetchAdDownload(magnet: magnet, existingLockedLink: cloudInfo)
|
||||
case .premiumize:
|
||||
await fetchPmDownload(cloudItemId: cloudInfo)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRdDownload(magnet: Magnet?, existingLink: String?) async {
|
||||
// If an existing link is passed in args, set it to that. Otherwise, find one from RD cloud.
|
||||
let torrentLink: String?
|
||||
if let existingLink {
|
||||
torrentLink = existingLink
|
||||
} else {
|
||||
// Bypass the TTL for up to date information
|
||||
await fetchRdCloud(bypassTTL: true)
|
||||
|
||||
let existingTorrent = realDebridCloudTorrents.first { $0.hash == selectedRealDebridItem?.magnet.hash && $0.status == "downloaded" }
|
||||
torrentLink = existingTorrent?.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0]
|
||||
guard let debridSource = selectedDebridSource else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
// If the links match from a user's downloads, no need to re-run a download
|
||||
if let torrentLink,
|
||||
let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink)
|
||||
{
|
||||
if let cloudInfo {
|
||||
downloadUrl = try await debridSource.checkUserDownloads(link: cloudInfo) ?? ""
|
||||
return
|
||||
}
|
||||
|
||||
if let magnet {
|
||||
let downloadLink = try await debridSource.getDownloadLink(
|
||||
magnet: magnet, ia: selectedDebridItem, iaFile: selectedDebridFile
|
||||
)
|
||||
|
||||
// Update the UI
|
||||
downloadUrl = downloadLink
|
||||
} else if let magnet {
|
||||
// Add a magnet after all the cache checks fail
|
||||
selectedRealDebridID = try await realDebrid.addMagnet(magnet: magnet)
|
||||
|
||||
var fileIds: [Int] = []
|
||||
if let iaFile = selectedRealDebridFile {
|
||||
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
|
||||
return
|
||||
}
|
||||
|
||||
fileIds = iaBatchFromFile.files.map(\.id)
|
||||
}
|
||||
|
||||
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 downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
downloadUrl = downloadLink
|
||||
} else {
|
||||
logManager?.error(
|
||||
"RealDebrid: Could not cache torrent with hash \(String(describing: magnet.hash))",
|
||||
description: "Could not cache this torrent. Aborting."
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
|
||||
throw DebridError.FailedRequest(description: "Could not fetch your file from \(debridSource.id)'s cache or API")
|
||||
}
|
||||
|
||||
// Fetch one more time to add updated data into the RD cloud cache
|
||||
await fetchRdCloud(bypassTTL: true)
|
||||
await fetchDebridCloud(bypassTTL: true)
|
||||
} catch {
|
||||
switch error {
|
||||
case RealDebrid.RDError.EmptyTorrents:
|
||||
case DebridError.IsCaching:
|
||||
showDeleteAlert.toggle()
|
||||
default:
|
||||
await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled")
|
||||
|
||||
await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false)
|
||||
await sendDebridError(error, prefix: "\(debridSource.id) download error", cancelString: "Download cancelled")
|
||||
}
|
||||
|
||||
logManager?.hideIndeterminateToast()
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper to handle cloud fetching
|
||||
public func fetchDebridCloud(bypassTTL: Bool = false) async {
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
await fetchRdCloud(bypassTTL: bypassTTL)
|
||||
case .allDebrid:
|
||||
await fetchAdCloud(bypassTTL: bypassTTL)
|
||||
case .premiumize:
|
||||
await fetchPmCloud(bypassTTL: bypassTTL)
|
||||
case .none:
|
||||
guard let selectedSource = selectedDebridSource else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshes torrents and downloads from a RD user's account
|
||||
public func fetchRdCloud(bypassTTL: Bool = false) async {
|
||||
if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL {
|
||||
if bypassTTL || Date().timeIntervalSince1970 > selectedSource.cloudTTL {
|
||||
do {
|
||||
realDebridCloudTorrents = try await realDebrid.userTorrents()
|
||||
realDebridCloudDownloads = try await realDebrid.userDownloads()
|
||||
// Populates the inner downloads and torrent arrays
|
||||
try await selectedSource.getUserDownloads()
|
||||
try await selectedSource.getUserTorrents()
|
||||
|
||||
// 5 minutes
|
||||
realDebridCloudTTL = Date().timeIntervalSince1970 + 300
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid cloud fetch error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRdDownload(downloadID: String) async {
|
||||
do {
|
||||
try await realDebrid.deleteDownload(debridID: downloadID)
|
||||
|
||||
// Bypass TTL to get current RD values
|
||||
await fetchRdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid download delete error")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRdTorrent(torrentID: String? = nil, presentError: Bool = true) async {
|
||||
do {
|
||||
if let torrentID {
|
||||
try await realDebrid.deleteTorrent(debridID: torrentID)
|
||||
} else if let selectedTorrentID = selectedRealDebridID {
|
||||
try await realDebrid.deleteTorrent(debridID: selectedTorrentID)
|
||||
} else {
|
||||
throw RealDebrid.RDError.FailedRequest(description: "No torrent ID was provided")
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid torrent delete error", presentError: presentError)
|
||||
}
|
||||
}
|
||||
|
||||
func checkRdUserDownloads(userTorrentLink: String) async -> String? {
|
||||
do {
|
||||
let existingLinks = realDebridCloudDownloads.first { $0.link == userTorrentLink }
|
||||
if let existingLink = existingLinks?.download {
|
||||
return existingLink
|
||||
} else {
|
||||
return try await realDebrid.unrestrictLink(debridDownloadLink: userTorrentLink)
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "RealDebrid download check error")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Integrate with AD saved links
|
||||
func fetchAdDownload(magnet: Magnet?, existingLockedLink: String?) async {
|
||||
// If an existing link is passed in args, set it to that. Otherwise, find one from AD cloud.
|
||||
let lockedLink: String?
|
||||
if let existingLockedLink {
|
||||
lockedLink = existingLockedLink
|
||||
} else {
|
||||
// Bypass the TTL for up to date information
|
||||
await fetchAdCloud(bypassTTL: true)
|
||||
|
||||
let existingMagnet = allDebridCloudMagnets.first { $0.hash == selectedAllDebridItem?.magnet.hash && $0.status == "Ready" }
|
||||
lockedLink = existingMagnet?.links[safe: selectedAllDebridFile?.id ?? 0]?.link
|
||||
}
|
||||
|
||||
do {
|
||||
if let lockedLink,
|
||||
let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink)
|
||||
{
|
||||
downloadUrl = unlockedLink
|
||||
} else if let magnet {
|
||||
let magnetID = try await allDebrid.addMagnet(magnet: magnet)
|
||||
let lockedLink = try await allDebrid.fetchMagnetStatus(
|
||||
magnetId: magnetID,
|
||||
selectedIndex: selectedAllDebridFile?.id ?? 0
|
||||
)
|
||||
|
||||
try await allDebrid.saveLink(link: lockedLink)
|
||||
downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink)
|
||||
} else {
|
||||
throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API")
|
||||
}
|
||||
|
||||
// Fetch one more time to add updated data into the AD cloud cache
|
||||
await fetchAdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid download error", cancelString: "Download cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
func checkAdUserLinks(lockedLink: String) async -> String? {
|
||||
do {
|
||||
let existingLinks = allDebridCloudLinks.first { $0.link == lockedLink }
|
||||
if let existingLink = existingLinks?.link {
|
||||
return existingLink
|
||||
} else {
|
||||
try await allDebrid.saveLink(link: lockedLink)
|
||||
return try await allDebrid.unlockLink(lockedLink: lockedLink)
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid download check error")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshes torrents and downloads from a RD user's account
|
||||
public func fetchAdCloud(bypassTTL: Bool = false) async {
|
||||
if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL {
|
||||
do {
|
||||
allDebridCloudMagnets = try await allDebrid.userMagnets()
|
||||
allDebridCloudLinks = try await allDebrid.savedLinks()
|
||||
|
||||
// 5 minutes
|
||||
allDebridCloudTTL = Date().timeIntervalSince1970 + 300
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AlLDebrid cloud fetch error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAdLink(link: String) async {
|
||||
do {
|
||||
try await allDebrid.deleteLink(link: link)
|
||||
|
||||
await fetchAdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid link delete error")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAdMagnet(magnetId: Int) async {
|
||||
do {
|
||||
try await allDebrid.deleteMagnet(magnetId: magnetId)
|
||||
|
||||
await fetchAdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid magnet delete error")
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPmDownload(cloudItemId: String? = nil) async {
|
||||
do {
|
||||
if let cloudItemId {
|
||||
downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link
|
||||
} else if let premiumizeFile = selectedPremiumizeFile {
|
||||
downloadUrl = premiumizeFile.streamUrlString
|
||||
} else if
|
||||
let premiumizeItem = selectedPremiumizeItem,
|
||||
let firstFile = premiumizeItem.files[safe: 0]
|
||||
{
|
||||
downloadUrl = firstFile.streamUrlString
|
||||
} else {
|
||||
throw Premiumize.PMError.FailedRequest(description: "There were no items or files found!")
|
||||
}
|
||||
|
||||
// Fetch one more time to add updated data into the PM cloud cache
|
||||
await fetchPmCloud(bypassTTL: true)
|
||||
|
||||
// Add a PM transfer if the item exists
|
||||
if let premiumizeItem = selectedPremiumizeItem {
|
||||
try await premiumize.createTransfer(magnet: premiumizeItem.magnet)
|
||||
}
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "Premiumize download error", cancelString: "Download or transfer cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshes items and fetches from a PM user account
|
||||
public func fetchPmCloud(bypassTTL: Bool = false) async {
|
||||
if bypassTTL || Date().timeIntervalSince1970 > premiumizeCloudTTL {
|
||||
do {
|
||||
let userItems = try await premiumize.userItems()
|
||||
withAnimation {
|
||||
premiumizeCloudItems = userItems
|
||||
}
|
||||
|
||||
// 5 minutes
|
||||
premiumizeCloudTTL = Date().timeIntervalSince1970 + 300
|
||||
// Update the TTL to 5 minutes from now
|
||||
selectedSource.cloudTTL = Date().timeIntervalSince1970 + 300
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
if error.code != -999 {
|
||||
await sendDebridError(error, prefix: "Premiumize cloud fetch error")
|
||||
await sendDebridError(error, prefix: "\(selectedSource.id) cloud fetch error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func deletePmItem(id: String) async {
|
||||
do {
|
||||
try await premiumize.deleteItem(itemID: id)
|
||||
public func deleteCloudDownload(_ download: DebridCloudDownload) async {
|
||||
guard let selectedSource = selectedDebridSource else {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass TTL to get current RD values
|
||||
await fetchPmCloud(bypassTTL: true)
|
||||
do {
|
||||
try await selectedSource.deleteDownload(downloadId: download.downloadId)
|
||||
|
||||
await fetchDebridCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "Premiumize cloud delete error")
|
||||
await sendDebridError(error, prefix: "\(selectedSource.id) download delete error")
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteCloudTorrent(_ torrent: DebridCloudTorrent) async {
|
||||
guard let selectedSource = selectedDebridSource else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try await selectedSource.deleteTorrent(torrentId: torrent.torrentId)
|
||||
|
||||
await fetchDebridCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "\(selectedSource.id) torrent delete error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class LoggingManager: ObservableObject {
|
|||
if let description {
|
||||
toastDescription = description
|
||||
} else if showErrorToasts {
|
||||
toastDescription = "An error was logged"
|
||||
toastDescription = "An error was logged. Please look at logs in Settings."
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
cleanedSearchText = searchText.lowercased()
|
||||
|
||||
if await !debridManager.enabledDebrids.isEmpty {
|
||||
if await !debridManager.hasEnabledDebrids {
|
||||
await debridManager.clearIAValues()
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
var failedSourceNames: [String] = []
|
||||
for await (requestResult, sourceName) in group {
|
||||
if let requestResult {
|
||||
if await !debridManager.enabledDebrids.isEmpty {
|
||||
if await debridManager.hasEnabledDebrids {
|
||||
await debridManager.populateDebridIA(requestResult.magnets)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ struct HybridSecureField: View {
|
|||
private var isFieldDisabled: Bool = false
|
||||
|
||||
init(text: Binding<String>, onCommit: (() -> Void)? = nil, showPassword: Bool = false) {
|
||||
self._text = text
|
||||
_text = text
|
||||
if let onCommit {
|
||||
self.onCommit = onCommit
|
||||
}
|
||||
|
|
@ -57,6 +57,6 @@ struct HybridSecureField: View {
|
|||
|
||||
extension HybridSecureField {
|
||||
public func fieldDisabled(_ isFieldDisabled: Bool) -> Self {
|
||||
modifyViewProp({ $0.isFieldDisabled = isFieldDisabled })
|
||||
modifyViewProp { $0.isFieldDisabled = isFieldDisabled }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,38 +8,34 @@
|
|||
import SwiftUI
|
||||
|
||||
struct DebridLabelView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@Store var debridSource: DebridSource
|
||||
|
||||
@State var cloudLinks: [String] = []
|
||||
@State var tagColor: Color = .red
|
||||
var magnet: Magnet?
|
||||
|
||||
var body: some View {
|
||||
if let selectedDebridType = debridManager.selectedDebridType {
|
||||
Tag(
|
||||
name: selectedDebridType.toString(abbreviated: true),
|
||||
color: getTagColor(),
|
||||
horizontalPadding: 5,
|
||||
verticalPadding: 3
|
||||
)
|
||||
}
|
||||
Tag(
|
||||
name: debridSource.abbreviation,
|
||||
color: getTagColor(),
|
||||
horizontalPadding: 5,
|
||||
verticalPadding: 3
|
||||
)
|
||||
}
|
||||
|
||||
func getTagColor() -> Color {
|
||||
if let magnet, cloudLinks.isEmpty {
|
||||
switch debridManager.matchMagnetHash(magnet) {
|
||||
case .full:
|
||||
return Color.green
|
||||
case .partial:
|
||||
return Color.orange
|
||||
case .none:
|
||||
return Color.red
|
||||
guard let match = debridSource.IAValues.first(where: { magnet.hash == $0.magnet.hash }) else {
|
||||
return .red
|
||||
}
|
||||
|
||||
return match.files.count > 1 ? .orange : .green
|
||||
} else if cloudLinks.count == 1 {
|
||||
return Color.green
|
||||
return .green
|
||||
} else if cloudLinks.count > 1 {
|
||||
return Color.orange
|
||||
return .orange
|
||||
} else {
|
||||
return Color.red
|
||||
return .red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,23 +15,23 @@ struct SelectedDebridFilterView<Content: View>: View {
|
|||
var body: some View {
|
||||
Menu {
|
||||
Button {
|
||||
debridManager.selectedDebridType = nil
|
||||
debridManager.selectedDebridSource = nil
|
||||
} label: {
|
||||
Text("None")
|
||||
|
||||
if debridManager.selectedDebridType == nil {
|
||||
if debridManager.selectedDebridSource == nil {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
ForEach(debridManager.debridSources, id: \.id) { debridSource in
|
||||
if debridSource.isLoggedIn {
|
||||
Button {
|
||||
debridManager.selectedDebridType = debridType
|
||||
debridManager.selectedDebridSource = debridSource
|
||||
} label: {
|
||||
Text(debridType.toString())
|
||||
Text(debridSource.id)
|
||||
|
||||
if debridManager.selectedDebridType == debridType {
|
||||
if debridManager.selectedDebridSource?.id == debridSource.id {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,5 @@ struct SelectedDebridFilterView<Content: View>: View {
|
|||
} label: {
|
||||
label
|
||||
}
|
||||
.id(debridManager.selectedDebridType)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ struct BookmarksView: View {
|
|||
.frame(height: 15)
|
||||
}
|
||||
.task {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
if debridManager.hasEnabledDebrids {
|
||||
let magnets = bookmarks.compactMap {
|
||||
if let magnetHash = $0.magnetHash {
|
||||
return Magnet(hash: magnetHash, link: $0.magnetLink)
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
//
|
||||
// AllDebridCloudView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/5/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AllDebridCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Links") {
|
||||
ForEach(debridManager.allDebridCloudLinks.filter {
|
||||
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.self) { downloadResponse in
|
||||
Button(downloadResponse.filename) {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = downloadResponse.filename
|
||||
debridManager.downloadUrl = downloadResponse.link
|
||||
|
||||
PersistenceController.shared.createHistory(
|
||||
HistoryEntryJson(
|
||||
name: downloadResponse.filename,
|
||||
url: downloadResponse.link,
|
||||
source: DebridType.allDebrid.toString()
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let savedLink = debridManager.allDebridCloudLinks[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteAdLink(link: savedLink.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisclosureGroup("Magnets") {
|
||||
ForEach(debridManager.allDebridCloudMagnets.filter {
|
||||
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.id) { magnet in
|
||||
Button {
|
||||
if magnet.status == "Ready", !magnet.links.isEmpty {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = magnet.filename
|
||||
|
||||
var historyInfo = HistoryEntryJson(
|
||||
name: magnet.filename,
|
||||
source: DebridType.allDebrid.toString()
|
||||
)
|
||||
|
||||
Task {
|
||||
if magnet.links.count == 1 {
|
||||
if let lockedLink = magnet.links[safe: 0]?.link {
|
||||
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: lockedLink)
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyInfo.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let magnet = Magnet(hash: magnet.hash, link: nil)
|
||||
|
||||
// Do not clear old IA values
|
||||
await debridManager.populateDebridIA([magnet])
|
||||
|
||||
if debridManager.selectDebridResult(magnet: magnet) {
|
||||
navModel.selectedHistoryInfo = historyInfo
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(magnet.filename)
|
||||
|
||||
HStack {
|
||||
Text(magnet.status)
|
||||
Spacer()
|
||||
DebridLabelView(cloudLinks: magnet.links.map(\.link))
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.9, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let magnet = debridManager.allDebridCloudMagnets[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteAdMagnet(magnetId: magnet.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// CloudDownloadView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 6/6/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CloudDownloadView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Store var debridSource: DebridSource
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Downloads") {
|
||||
ForEach(debridSource.cloudDownloads.filter {
|
||||
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.self) { cloudDownload in
|
||||
Button(cloudDownload.fileName) {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = cloudDownload.fileName
|
||||
debridManager.downloadUrl = cloudDownload.link
|
||||
|
||||
PersistenceController.shared.createHistory(
|
||||
HistoryEntryJson(
|
||||
name: cloudDownload.fileName,
|
||||
url: cloudDownload.link,
|
||||
source: debridSource.id
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let cloudDownload = debridSource.cloudDownloads[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteCloudDownload(cloudDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// CloudTorrentView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 6/6/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CloudTorrentView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Store var debridSource: DebridSource
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Torrents") {
|
||||
ForEach(debridSource.cloudTorrents.filter {
|
||||
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.self) { cloudTorrent in
|
||||
Button {
|
||||
if cloudTorrent.status == "downloaded", !cloudTorrent.links.isEmpty {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = cloudTorrent.fileName
|
||||
|
||||
var historyInfo = HistoryEntryJson(
|
||||
name: cloudTorrent.fileName,
|
||||
source: debridSource.id
|
||||
)
|
||||
|
||||
Task {
|
||||
let magnet = Magnet(hash: cloudTorrent.hash, link: nil)
|
||||
await debridManager.populateDebridIA([magnet])
|
||||
if debridManager.selectDebridResult(magnet: magnet) {
|
||||
// Is this a batch?
|
||||
|
||||
if cloudTorrent.links.count == 1 {
|
||||
await debridManager.fetchDebridDownload(magnet: magnet)
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyInfo.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
} else {
|
||||
navModel.selectedMagnet = magnet
|
||||
navModel.selectedHistoryInfo = historyInfo
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(cloudTorrent.fileName)
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(4)
|
||||
|
||||
HStack {
|
||||
Text(cloudTorrent.status.capitalizingFirstLetter())
|
||||
Spacer()
|
||||
DebridLabelView(debridSource: debridSource, cloudLinks: cloudTorrent.links)
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let cloudTorrent = debridSource.cloudTorrents[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteCloudTorrent(cloudTorrent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
//
|
||||
// PremiumizeCloudView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/2/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PremiumizeCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
var body: some View {
|
||||
DisclosureGroup("Items") {
|
||||
ForEach(debridManager.premiumizeCloudItems.filter {
|
||||
searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.id) { item in
|
||||
Button(item.name) {
|
||||
Task {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = item.name
|
||||
|
||||
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: item.id)
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
PersistenceController.shared.createHistory(
|
||||
HistoryEntryJson(
|
||||
name: item.name,
|
||||
url: debridManager.downloadUrl,
|
||||
source: DebridType.premiumize.toString()
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let item = debridManager.premiumizeCloudItems[safe: index] {
|
||||
Task {
|
||||
await debridManager.deletePmItem(id: item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
//
|
||||
// RealDebridCloudView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 12/31/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RealDebridCloudView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
DisclosureGroup("Downloads") {
|
||||
ForEach(debridManager.realDebridCloudDownloads.filter {
|
||||
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.self) { downloadResponse in
|
||||
Button(downloadResponse.filename) {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = downloadResponse.filename
|
||||
debridManager.downloadUrl = downloadResponse.download
|
||||
|
||||
PersistenceController.shared.createHistory(
|
||||
HistoryEntryJson(
|
||||
name: downloadResponse.filename,
|
||||
url: downloadResponse.download,
|
||||
source: DebridType.realDebrid.toString()
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let downloadResponse = debridManager.realDebridCloudDownloads[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteRdDownload(downloadID: downloadResponse.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisclosureGroup("Torrents") {
|
||||
ForEach(debridManager.realDebridCloudTorrents.filter {
|
||||
searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased())
|
||||
}, id: \.self) { torrentResponse in
|
||||
Button {
|
||||
if torrentResponse.status == "downloaded", !torrentResponse.links.isEmpty {
|
||||
navModel.resultFromCloud = true
|
||||
navModel.selectedTitle = torrentResponse.filename
|
||||
|
||||
var historyInfo = HistoryEntryJson(
|
||||
name: torrentResponse.filename,
|
||||
source: DebridType.realDebrid.toString()
|
||||
)
|
||||
|
||||
Task {
|
||||
if torrentResponse.links.count == 1 {
|
||||
if let torrentLink = torrentResponse.links[safe: 0] {
|
||||
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink)
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyInfo.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let magnet = Magnet(hash: torrentResponse.hash, link: nil)
|
||||
|
||||
// Do not clear old IA values
|
||||
await debridManager.populateDebridIA([magnet])
|
||||
|
||||
if debridManager.selectDebridResult(magnet: magnet) {
|
||||
navModel.selectedHistoryInfo = historyInfo
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(torrentResponse.filename)
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(4)
|
||||
|
||||
HStack {
|
||||
Text(torrentResponse.status.capitalizingFirstLetter())
|
||||
Spacer()
|
||||
DebridLabelView(cloudLinks: torrentResponse.links)
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let torrentResponse = debridManager.realDebridCloudTorrents[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteRdTorrent(torrentID: torrentResponse.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,19 +10,18 @@ import SwiftUI
|
|||
struct DebridCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@Store var debridSource: DebridSource
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
switch debridManager.selectedDebridType {
|
||||
case .realDebrid:
|
||||
RealDebridCloudView(searchText: $searchText)
|
||||
case .premiumize:
|
||||
PremiumizeCloudView(searchText: $searchText)
|
||||
case .allDebrid:
|
||||
AllDebridCloudView(searchText: $searchText)
|
||||
case .none:
|
||||
EmptyView()
|
||||
if !debridSource.cloudDownloads.isEmpty {
|
||||
CloudDownloadView(debridSource: debridSource, searchText: $searchText)
|
||||
}
|
||||
|
||||
if !debridSource.cloudTorrents.isEmpty {
|
||||
CloudTorrentView(debridSource: debridSource, searchText: $searchText)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
|
|
@ -32,7 +31,7 @@ struct DebridCloudView: View {
|
|||
.refreshable {
|
||||
await debridManager.fetchDebridCloud(bypassTTL: true)
|
||||
}
|
||||
.onChange(of: debridManager.selectedDebridType) { newType in
|
||||
.onChange(of: debridManager.selectedDebridSource?.id) { newType in
|
||||
if newType != nil {
|
||||
Task {
|
||||
await debridManager.fetchDebridCloud()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ struct LibraryPickerView: View {
|
|||
Text("Bookmarks").tag(NavigationViewModel.LibraryPickerSegment.bookmarks)
|
||||
Text("History").tag(NavigationViewModel.LibraryPickerSegment.history)
|
||||
|
||||
if !debridManager.enabledDebrids.isEmpty {
|
||||
if debridManager.hasEnabledDebrids {
|
||||
Text("Cloud").tag(NavigationViewModel.LibraryPickerSegment.debridCloud)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,14 +53,14 @@ struct SearchFilterHeaderView: View {
|
|||
|
||||
SelectedDebridFilterView {
|
||||
FilterLabelView(
|
||||
name: debridManager.selectedDebridType?.toString(),
|
||||
name: debridManager.selectedDebridSource?.id,
|
||||
fallbackName: "Debrid"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Cache status picker
|
||||
|
||||
if !debridManager.enabledDebrids.isEmpty {
|
||||
if debridManager.hasEnabledDebrids {
|
||||
IAFilterView()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -127,13 +127,14 @@ struct SearchResultButtonView: View {
|
|||
.alert("Caching file", isPresented: $debridManager.showDeleteAlert) {
|
||||
Button("Yes", role: .destructive) {
|
||||
Task {
|
||||
await debridManager.deleteRdTorrent()
|
||||
try? await debridManager.selectedDebridSource?.deleteTorrent(torrentId: nil)
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text(
|
||||
"RealDebrid is currently caching this file. Would you like to delete it? \n\n" +
|
||||
"\(String(describing: debridManager.selectedDebridSource?.id)) is currently caching this file. " +
|
||||
"Would you like to delete it? \n\n" +
|
||||
"Progress can be checked on the RealDebrid website."
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ struct SearchResultInfoView: View {
|
|||
Text(size)
|
||||
}
|
||||
|
||||
DebridLabelView(magnet: result.magnet)
|
||||
if let debridSource = debridManager.selectedDebridSource {
|
||||
DebridLabelView(debridSource: debridSource, magnet: result.magnet)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SwiftUI
|
|||
struct SettingsDebridInfoView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
let debridType: DebridType
|
||||
@Store var debridSource: DebridSource
|
||||
|
||||
@State private var apiKeyTempText: String = ""
|
||||
|
||||
|
|
@ -18,9 +18,9 @@ struct SettingsDebridInfoView: View {
|
|||
List {
|
||||
Section(header: InlineHeader("Description")) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("\(debridType.toString()) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.")
|
||||
Text("\(debridSource.id) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.")
|
||||
|
||||
Link("Website", destination: URL(string: debridType.website()) ?? URL(string: "https://kingbri.dev/ferrite")!)
|
||||
Link("Website", destination: URL(string: debridSource.website) ?? URL(string: "https://kingbri.dev/ferrite")!)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -30,21 +30,21 @@ struct SettingsDebridInfoView: View {
|
|||
) {
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
await debridManager.logoutDebrid(debridType: debridType)
|
||||
} else if !debridManager.authProcessing(debridType) {
|
||||
await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil)
|
||||
if debridSource.isLoggedIn {
|
||||
await debridManager.logout(debridSource)
|
||||
} else if !debridSource.authProcessing {
|
||||
await debridManager.authenticateDebrid(debridSource, apiKey: nil)
|
||||
}
|
||||
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
|
||||
}
|
||||
} label: {
|
||||
Text(
|
||||
debridManager.enabledDebrids.contains(debridType)
|
||||
debridSource.isLoggedIn
|
||||
? "Logout"
|
||||
: (debridManager.authProcessing(debridType) ? "Processing" : "Login")
|
||||
: (debridSource.authProcessing ? "Processing" : "Login")
|
||||
)
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(debridType) ? .red : .blue)
|
||||
.foregroundColor(debridSource.isLoggedIn ? .red : .blue)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,22 +57,22 @@ struct SettingsDebridInfoView: View {
|
|||
onCommit: {
|
||||
Task {
|
||||
if !apiKeyTempText.isEmpty {
|
||||
await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText)
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
|
||||
await debridManager.authenticateDebrid(debridSource, apiKey: apiKeyTempText)
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.fieldDisabled(debridManager.enabledDebrids.contains(debridType))
|
||||
.fieldDisabled(debridSource.isLoggedIn)
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
|
||||
apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle(debridType.toString())
|
||||
.navigationTitle(debridSource.id)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,13 @@ struct LibraryView: View {
|
|||
case .history:
|
||||
HistoryView(allHistoryEntries: allHistoryEntries, searchText: $searchText)
|
||||
case .debridCloud:
|
||||
DebridCloudView(searchText: $searchText)
|
||||
if let selectedDebridSource = debridManager.selectedDebridSource {
|
||||
DebridCloudView(debridSource: selectedDebridSource, searchText: $searchText)
|
||||
} else {
|
||||
// Placeholder view that takes up the entire parent view
|
||||
Color.clear
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
|
|
@ -53,7 +59,7 @@ struct LibraryView: View {
|
|||
EmptyInstructionView(title: "No History", message: "Start watching to build history")
|
||||
}
|
||||
case .debridCloud:
|
||||
if debridManager.selectedDebridType == nil {
|
||||
if debridManager.selectedDebridSource == nil {
|
||||
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +75,7 @@ struct LibraryView: View {
|
|||
switch navModel.libraryPickerSelection {
|
||||
case .bookmarks, .debridCloud:
|
||||
SelectedDebridFilterView {
|
||||
Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid")
|
||||
Text(debridManager.selectedDebridSource?.abbreviation ?? "Debrid")
|
||||
}
|
||||
.transaction {
|
||||
$0.animation = .none
|
||||
|
|
|
|||
|
|
@ -46,14 +46,14 @@ struct SettingsView: View {
|
|||
NavView {
|
||||
Form {
|
||||
Section(header: InlineHeader("Debrid services")) {
|
||||
ForEach(DebridType.allCases, id: \.self) { debridType in
|
||||
ForEach(debridManager.debridSources, id: \.id) { (debridSource: DebridSource) in
|
||||
NavigationLink {
|
||||
SettingsDebridInfoView(debridType: debridType)
|
||||
SettingsDebridInfoView(debridSource: debridSource)
|
||||
} label: {
|
||||
HStack {
|
||||
Text(debridType.toString())
|
||||
Text(debridSource.id)
|
||||
Spacer()
|
||||
Text(debridManager.enabledDebrids.contains(debridType) ? "Enabled" : "Disabled")
|
||||
Text(debridSource.isLoggedIn ? "Enabled" : "Disabled")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ struct SettingsView: View {
|
|||
if changed {
|
||||
Task {
|
||||
let dataRecords = await WKWebsiteDataStore.default().dataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes())
|
||||
|
||||
|
||||
await WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: dataRecords)
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
Section(header: InlineHeader("Default actions")) {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
if debridManager.hasEnabledDebrids {
|
||||
NavigationLink {
|
||||
DefaultActionPickerView(
|
||||
actionRequirement: .debrid,
|
||||
|
|
@ -227,7 +227,7 @@ struct SettingsView: View {
|
|||
callbackURLScheme: "ferrite"
|
||||
) { callbackURL, error in
|
||||
Task {
|
||||
await debridManager.handleCallback(url: callbackURL, error: error)
|
||||
await debridManager.handleAuthCallback(url: callbackURL, error: error)
|
||||
}
|
||||
}
|
||||
.prefersEphemeralWebBrowserSession(useEphemeralAuth)
|
||||
|
|
|
|||
|
|
@ -23,39 +23,14 @@ struct BatchChoiceView: View {
|
|||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
switch debridManager.selectedDebridType {
|
||||
case .realDebrid:
|
||||
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
|
||||
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
|
||||
Button(file.name) {
|
||||
debridManager.selectedRealDebridFile = file
|
||||
ForEach(debridManager.selectedDebridItem?.files ?? [], id: \.self) { file in
|
||||
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
|
||||
Button(file.name) {
|
||||
debridManager.selectedDebridFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.name)
|
||||
}
|
||||
queueCommonDownload(fileName: file.name)
|
||||
}
|
||||
}
|
||||
case .allDebrid:
|
||||
ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in
|
||||
if file.fileName.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
|
||||
Button(file.fileName) {
|
||||
debridManager.selectedAllDebridFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .premiumize:
|
||||
ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in
|
||||
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
|
||||
Button(file.name) {
|
||||
debridManager.selectedPremiumizeFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.tint(.primary)
|
||||
|
|
@ -85,7 +60,7 @@ struct BatchChoiceView: View {
|
|||
// Common function to communicate betwen VMs and queue/display a download
|
||||
func queueCommonDownload(fileName: String) {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchDebridDownload(magnet: navModel.resultFromCloud ? nil : navModel.selectedMagnet)
|
||||
await debridManager.fetchDebridDownload(magnet: navModel.selectedMagnet)
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue