v0.6.0 #19
61 changed files with 3051 additions and 905 deletions
11
.github/workflows/nightly.yml
vendored
11
.github/workflows/nightly.yml
vendored
|
|
@ -14,18 +14,19 @@ jobs:
|
|||
with:
|
||||
xcode-version: latest
|
||||
- name: Get commit SHA
|
||||
id: commitinfo
|
||||
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
- name: Build
|
||||
run: xcodebuild -scheme Ferrite -configuration Release archive -archivePath build/Ferrite.xcarchive CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO
|
||||
env:
|
||||
IS_NIGHTLY: YES
|
||||
- name: Package ipa
|
||||
run: |
|
||||
mkdir Payload
|
||||
cp -r build/Ferrite.xcarchive/Products/Applications/Ferrite.app Payload
|
||||
zip -r Ferrite-iOS_nightly-${{ steps.commitinfo.outputs.sha_short }}.ipa Payload
|
||||
zip -r Ferrite-iOS_nightly-${{ env.sha_short }}.ipa Payload
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Ferrite-iOS_nightly-${{ steps.commitinfo.outputs.sha_short }}.ipa
|
||||
path: Ferrite-iOS_nightly-${{ steps.commitinfo.outputs.sha_short }}.ipa
|
||||
name: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa
|
||||
path: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa
|
||||
if-no-files-found: error
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; };
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; };
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; };
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; };
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
|
||||
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; };
|
||||
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
|
||||
|
|
@ -20,6 +24,11 @@
|
|||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; };
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; };
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; };
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */; };
|
||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7F293542F300486D65 /* PremiumizeModels.swift */; };
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */; };
|
||||
0C42B5982932F6DD008057A0 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Set.swift */; };
|
||||
0C445C62293F9A0B0060744D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C445C61293F9A0B0060744D /* Bundle.swift */; };
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.swift */; };
|
||||
|
|
@ -28,11 +37,14 @@
|
|||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; };
|
||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; };
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
|
||||
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 */; };
|
||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; };
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
|
||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */; };
|
||||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */; };
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; };
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; };
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; };
|
||||
|
|
@ -89,6 +101,7 @@
|
|||
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 */; };
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
|
||||
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; };
|
||||
|
|
@ -99,16 +112,22 @@
|
|||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; };
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; };
|
||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
|
||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; };
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
|
||||
0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = "<group>"; };
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
|
||||
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.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>"; };
|
||||
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -117,6 +136,11 @@
|
|||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; };
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; };
|
||||
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeWrapper.swift; sourceTree = "<group>"; };
|
||||
0C422E7F293542F300486D65 /* PremiumizeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeModels.swift; sourceTree = "<group>"; };
|
||||
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridChoiceView.swift; sourceTree = "<group>"; };
|
||||
0C42B5972932F6DD008057A0 /* Set.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Set.swift; sourceTree = "<group>"; };
|
||||
0C445C61293F9A0B0060744D /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
|
||||
0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -124,9 +148,12 @@
|
|||
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
|
||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
|
||||
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
|
||||
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = "<group>"; };
|
||||
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridModels.swift; sourceTree = "<group>"; };
|
||||
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = "<group>"; };
|
||||
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenter.swift; sourceTree = "<group>"; };
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -181,6 +208,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -191,6 +219,7 @@
|
|||
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
|
||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
|
||||
0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
|
||||
0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
|
@ -207,12 +236,43 @@
|
|||
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */,
|
||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
|
||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0C0755C22934241F00ECA142 /* SheetViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
);
|
||||
path = SheetViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C0755C32934244500ECA142 /* ComponentViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C0755C42934245800ECA142 /* Debrid */,
|
||||
0CA3B23528C265FD00616D3A /* Library */,
|
||||
0C44E2AB28D4E126007711AE /* SearchResult */,
|
||||
0CA0545C288F7CB200850554 /* Settings */,
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */,
|
||||
);
|
||||
path = ComponentViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C0755C42934245800ECA142 /* Debrid */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */,
|
||||
0C0755C5293424A200ECA142 /* DebridLabelView.swift */,
|
||||
);
|
||||
path = Debrid;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -241,8 +301,11 @@
|
|||
0C0D50E3288DFE6E0035ECC8 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */,
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||
0C422E7F293542F300486D65 /* PremiumizeModels.swift */,
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */,
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||
|
|
@ -251,6 +314,16 @@
|
|||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C2886D52960C4F800D6FC16 /* Cloud */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */,
|
||||
0CAF9318296399190050812A /* PremiumizeCloudView.swift */,
|
||||
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */,
|
||||
);
|
||||
path = Cloud;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2A628D4DDC6007711AE /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -281,25 +354,25 @@
|
|||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2AB28D4E126007711AE /* SearchResultViews */ = {
|
||||
0C44E2AB28D4E126007711AE /* SearchResult */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */,
|
||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */,
|
||||
);
|
||||
path = SearchResultViews;
|
||||
path = SearchResult;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */ = {
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
|
||||
);
|
||||
path = SourceViews;
|
||||
path = Source;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA0545C288F7CB200850554 /* SettingsViews */ = {
|
||||
0CA0545C288F7CB200850554 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
|
||||
|
|
@ -308,7 +381,7 @@
|
|||
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||
);
|
||||
path = SettingsViews;
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA148BA288903F000DE2211 /* Ferrite */ = {
|
||||
|
|
@ -357,6 +430,8 @@
|
|||
0CA148C8288903F000DE2211 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CD72E16293D9928001A7EA4 /* Array.swift */,
|
||||
0C445C61293F9A0B0060744D /* Bundle.swift */,
|
||||
0CA148C9288903F000DE2211 /* Collection.swift */,
|
||||
0CA148CA288903F000DE2211 /* Data.swift */,
|
||||
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
|
||||
|
|
@ -365,6 +440,7 @@
|
|||
0CA148CB288903F000DE2211 /* Task.swift */,
|
||||
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
||||
0C7ED14228D65518009E29AD /* FileManager.swift */,
|
||||
0C42B5972932F6DD008057A0 /* Set.swift */,
|
||||
0C7C128528DAA3CD00381CD1 /* URL.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
|
|
@ -373,12 +449,10 @@
|
|||
0CA148EE2889061200DE2211 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */,
|
||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */,
|
||||
0C0755C32934244500ECA142 /* ComponentViews */,
|
||||
0CA148F02889062700DE2211 /* RepresentableViews */,
|
||||
0CA148C0288903F000DE2211 /* CommonViews */,
|
||||
0C44E2AB28D4E126007711AE /* SearchResultViews */,
|
||||
0CA0545C288F7CB200850554 /* SettingsViews */,
|
||||
0C0755C22934241F00ECA142 /* SheetViews */,
|
||||
0CA148D1288903F000DE2211 /* MainView.swift */,
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */,
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
|
||||
|
|
@ -387,8 +461,6 @@
|
|||
0CA148BB288903F000DE2211 /* SettingsView.swift */,
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -417,21 +489,25 @@
|
|||
0CA148F12889066000DE2211 /* API */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */,
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
|
||||
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */,
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */ = {
|
||||
0CA3B23528C265FD00616D3A /* Library */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C2886D52960C4F800D6FC16 /* Cloud */,
|
||||
0CA3B23828C2660D00616D3A /* BookmarksView.swift */,
|
||||
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */,
|
||||
0CA3B23628C2660700616D3A /* HistoryView.swift */,
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */,
|
||||
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */,
|
||||
);
|
||||
path = LibraryViews;
|
||||
path = Library;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CAF1C5F286F5C0D00296F86 = {
|
||||
|
|
@ -470,6 +546,7 @@
|
|||
0CAF1C64286F5C0E00296F86 /* Sources */,
|
||||
0CAF1C65286F5C0E00296F86 /* Frameworks */,
|
||||
0CAF1C66286F5C0E00296F86 /* Resources */,
|
||||
0C445C60293F99360060744D /* Insert environment vars into Plist */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -484,6 +561,7 @@
|
|||
0C7376EF28A97D1400D60918 /* SwiftUIX */,
|
||||
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
|
||||
0CB6516728C5A5EC00DCA721 /* Introspect */,
|
||||
0CDDDE042935235E006810B1 /* BetterSafariView */,
|
||||
);
|
||||
productName = Torrenter;
|
||||
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
|
||||
|
|
@ -521,6 +599,7 @@
|
|||
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
|
||||
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */,
|
||||
);
|
||||
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -544,6 +623,29 @@
|
|||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
0C445C60293F99360060744D /* Insert environment vars into Plist */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH,
|
||||
);
|
||||
name = "Insert environment vars into Plist";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n# From https://stackoverflow.com/questions/26514866/how-can-i-include-the-git-commit-hash-in-xcode\n\nINFO_PLIST=\"${TARGET_BUILD_DIR}\"/\"${INFOPLIST_PATH}\"\n\n# MARK: Adds commit hash to plist\ncommitValue=$(/usr/libexec/PlistBuddy -c 'print :GitCommitHash' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$commitValue\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :GitCommitHash string\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :GitCommitHash `git rev-parse --short HEAD`\" \"${INFO_PLIST}\"\n\n# MARK: Adds the git build type to plist (examples: nightly, stable)\nbuildTypeValue=$(/usr/libexec/PlistBuddy -c 'print :IsNightly' \"${INFO_PLIST}\" 2>/dev/null)\n\n# Check if value is empty\nif [ -z \"$buildTypeValue\" ] \nthen\n /usr/libexec/PlistBuddy -c \"Add :IsNightly bool\" \"${INFO_PLIST}\"\nfi\n\n/usr/libexec/PlistBuddy -c \"Set :IsNightly ${IS_NIGHTLY}\" \"${INFO_PLIST}\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
0CAF1C64286F5C0E00296F86 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
|
|
@ -562,11 +664,14 @@
|
|||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */,
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */,
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
|
||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
|
|
@ -577,8 +682,10 @@
|
|||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
||||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
|
||||
|
|
@ -587,7 +694,10 @@
|
|||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */,
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */,
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */,
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
|
||||
0C445C62293F9A0B0060744D /* Bundle.swift in Sources */,
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */,
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
|
||||
|
|
@ -596,7 +706,10 @@
|
|||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
||||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
|
||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
||||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */,
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
|
||||
|
|
@ -606,7 +719,8 @@
|
|||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */,
|
||||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */,
|
||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */,
|
||||
0C42B5982932F6DD008057A0 /* Set.swift in Sources */,
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
|
|
@ -618,6 +732,7 @@
|
|||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
|
||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||
|
|
@ -630,6 +745,7 @@
|
|||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */,
|
||||
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
|
||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
|
||||
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */,
|
||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */,
|
||||
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */,
|
||||
0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */,
|
||||
|
|
@ -759,7 +875,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8A74DBQ6S3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -778,7 +894,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
@ -794,7 +910,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8A74DBQ6S3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -813,7 +929,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
@ -903,6 +1019,14 @@
|
|||
kind = branch;
|
||||
};
|
||||
};
|
||||
0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/stleamist/BetterSafariView";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
|
@ -941,6 +1065,11 @@
|
|||
package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
||||
productName = Introspect;
|
||||
};
|
||||
0CDDDE042935235E006810B1 /* BetterSafariView */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */;
|
||||
productName = BetterSafariView;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
|
|
|||
227
Ferrite/API/AllDebridWrapper.swift
Normal file
227
Ferrite/API/AllDebridWrapper.swift
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
//
|
||||
// AllDebridWrapper.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/25/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainSwift
|
||||
|
||||
// TODO: Fix errors
|
||||
public class AllDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let keychain = KeychainSwift()
|
||||
|
||||
let baseApiUrl = "https://api.alldebrid.com/v4"
|
||||
let appName = "Ferrite"
|
||||
|
||||
var authTask: Task<Void, Error>?
|
||||
|
||||
// Fetches information for PIN auth
|
||||
public func getPinInfo() async throws -> PinResponse {
|
||||
let url = try buildRequestURL(urlString: "\(baseApiUrl)/pin/get")
|
||||
let request = URLRequest(url: url)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<PinResponse>.self, from: data).data
|
||||
|
||||
return rawResponse
|
||||
} catch {
|
||||
print("Couldn't get pin information!")
|
||||
throw ADError.AuthQuery(description: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetches API keys
|
||||
public func getApiKey(checkID: String, pin: String) async throws {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "agent", value: appName),
|
||||
URLQueryItem(name: "check", value: checkID),
|
||||
URLQueryItem(name: "pin", value: pin)
|
||||
]
|
||||
|
||||
let request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/pin/check", queryItems: queryItems))
|
||||
|
||||
// Timer to poll AD API for key
|
||||
authTask = Task {
|
||||
var count = 0
|
||||
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw ADError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// We don't care if this fails
|
||||
let rawResponse = try? self.jsonDecoder.decode(ADResponse<ApiKeyResponse>.self, from: data).data
|
||||
|
||||
// If there's an API key from the response, end the task successfully
|
||||
if let apiKeyResponse = rawResponse {
|
||||
keychain.set(apiKeyResponse.apikey, forKey: "AllDebrid.ApiKey")
|
||||
|
||||
return
|
||||
} else {
|
||||
try await Task.sleep(seconds: 5)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
throw ADError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
}
|
||||
|
||||
if case let .failure(error) = await authTask?.result {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
keychain.delete("AllDebrid.ApiKey")
|
||||
}
|
||||
|
||||
// Wrapper request function which matches the responses and returns data
|
||||
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
||||
guard let token = keychain.get("AllDebrid.ApiKey") else {
|
||||
throw ADError.InvalidToken
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw ADError.FailedRequest(description: "No HTTP response given")
|
||||
}
|
||||
|
||||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
deleteTokens()
|
||||
throw ADError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.")
|
||||
} else {
|
||||
throw ADError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
// Builds a URL for further requests
|
||||
private func buildRequestURL(urlString: String, queryItems: [URLQueryItem] = []) throws -> URL {
|
||||
guard var components = URLComponents(string: urlString) else {
|
||||
throw ADError.InvalidUrl
|
||||
}
|
||||
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "agent", value: appName)
|
||||
] + queryItems
|
||||
|
||||
if let url = components.url {
|
||||
return url
|
||||
} else {
|
||||
throw ADError.InvalidUrl
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a magnet link to the user's AD account
|
||||
public func addMagnet(magnet: Magnet) async throws -> Int {
|
||||
guard let magnetLink = magnet.link else {
|
||||
throw ADError.FailedRequest(description: "The magnet link is invalid")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload"))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var bodyComponents = URLComponents()
|
||||
bodyComponents.queryItems = [
|
||||
URLQueryItem(name: "magnets[]", value: magnetLink)
|
||||
]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<AddMagnetResponse>.self, from: data).data
|
||||
|
||||
if let magnet = rawResponse.magnets[safe: 0] {
|
||||
return magnet.id
|
||||
} else {
|
||||
throw ADError.InvalidResponse
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchMagnetStatus(magnetId: Int, selectedIndex: Int?) async throws -> String {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "id", value: String(magnetId))
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
|
||||
|
||||
// Better to fetch no link at all than the wrong link
|
||||
if let linkWrapper = rawResponse.magnets[safe: 0]?.links[safe: selectedIndex ?? -1] {
|
||||
return linkWrapper.link
|
||||
} else {
|
||||
throw ADError.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)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/link/unlock", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<UnlockLinkResponse>.self, from: data).data
|
||||
|
||||
return rawResponse.link
|
||||
}
|
||||
|
||||
public func instantAvailability(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
|
||||
}
|
||||
}
|
||||
|
|
@ -8,21 +8,21 @@
|
|||
import Foundation
|
||||
|
||||
public class Github {
|
||||
public func fetchLatestRelease() async throws -> GithubRelease? {
|
||||
public func fetchLatestRelease() async throws -> Release? {
|
||||
let url = URL(string: "https://api.github.com/repos/bdashore3/Ferrite/releases/latest")!
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
|
||||
let rawResponse = try JSONDecoder().decode(GithubRelease.self, from: data)
|
||||
let rawResponse = try JSONDecoder().decode(Release.self, from: data)
|
||||
return rawResponse
|
||||
}
|
||||
|
||||
public func fetchReleases() async throws -> [GithubRelease]? {
|
||||
public func fetchReleases() async throws -> [Release]? {
|
||||
let url = URL(string: "https://api.github.com/repos/bdashore3/Ferrite/releases")!
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
|
||||
let rawResponse = try JSONDecoder().decode([GithubRelease].self, from: data)
|
||||
let rawResponse = try JSONDecoder().decode([Release].self, from: data)
|
||||
return rawResponse
|
||||
}
|
||||
}
|
||||
|
|
|
|||
242
Ferrite/API/PremiumizeWrapper.swift
Normal file
242
Ferrite/API/PremiumizeWrapper.swift
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
//
|
||||
// PremiumizeWrapper.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainSwift
|
||||
|
||||
public class Premiumize {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let keychain = KeychainSwift()
|
||||
|
||||
let baseAuthUrl = "https://www.premiumize.me/authorize"
|
||||
let baseApiUrl = "https://www.premiumize.me/api"
|
||||
let clientId = "791565696"
|
||||
|
||||
public func buildAuthUrl() throws -> URL {
|
||||
var urlComponents = URLComponents(string: baseAuthUrl)!
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: clientId),
|
||||
URLQueryItem(name: "response_type", value: "token"),
|
||||
URLQueryItem(name: "state", value: UUID().uuidString)
|
||||
]
|
||||
|
||||
if let url = urlComponents.url {
|
||||
return url
|
||||
} else {
|
||||
throw PMError.InvalidUrl
|
||||
}
|
||||
}
|
||||
|
||||
public func handleAuthCallback(url: URL) throws {
|
||||
let callbackComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
|
||||
guard let callbackFragment = callbackComponents?.fragment else {
|
||||
throw PMError.InvalidResponse
|
||||
}
|
||||
|
||||
var fragmentComponents = URLComponents()
|
||||
fragmentComponents.query = callbackFragment
|
||||
|
||||
guard let accessToken = fragmentComponents.queryItems?.first(where: { $0.name == "access_token" })?.value else {
|
||||
throw PMError.InvalidToken
|
||||
}
|
||||
|
||||
keychain.set(accessToken, forKey: "Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// Clears tokens. No endpoint to deregister a device
|
||||
public func deleteTokens() {
|
||||
keychain.delete("Premiumize.AccessToken")
|
||||
}
|
||||
|
||||
// Wrapper request function which matches the responses and returns data
|
||||
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
||||
guard let token = keychain.get("Premiumize.AccessToken") else {
|
||||
throw PMError.InvalidToken
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw PMError.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.")
|
||||
} else {
|
||||
throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
// Function to divide and execute cache endpoint requests in parallel
|
||||
// Calls this for 100 hashes at a time due to API limits
|
||||
public func divideCacheRequests(magnets: [Magnet]) async throws -> [Magnet] {
|
||||
let availableMagnets = try await withThrowingTaskGroup(of: [Magnet].self) { group in
|
||||
for chunk in magnets.chunked(into: 100) {
|
||||
group.addTask {
|
||||
try await self.checkCache(magnets: chunk)
|
||||
}
|
||||
}
|
||||
|
||||
var chunkedMagnets: [Magnet] = []
|
||||
for try await magnetArray in group {
|
||||
chunkedMagnets += magnetArray
|
||||
}
|
||||
|
||||
return chunkedMagnets
|
||||
}
|
||||
|
||||
return availableMagnets
|
||||
}
|
||||
|
||||
// Parent function for initial checking of the cache
|
||||
func checkCache(magnets: [Magnet]) async throws -> [Magnet] {
|
||||
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
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data)
|
||||
|
||||
if rawResponse.response.isEmpty {
|
||||
throw PMError.EmptyData
|
||||
} else {
|
||||
let availableMagnets = magnets.enumerated().compactMap { index, magnet in
|
||||
if rawResponse.response[safe: index] == true {
|
||||
return magnet
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return availableMagnets
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
var chunkedIA: [Premiumize.IA] = []
|
||||
for try await ia in group {
|
||||
chunkedIA.append(ia)
|
||||
}
|
||||
return chunkedIA
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
} else {
|
||||
throw PMError.EmptyData
|
||||
}
|
||||
}
|
||||
|
||||
func createTransfer(magnet: Magnet) async throws {
|
||||
guard let magnetLink = magnet.link else {
|
||||
throw PMError.FailedRequest(description: "The magnet link is invalid")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/create")!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
var bodyComponents = URLComponents()
|
||||
bodyComponents.queryItems = [URLQueryItem(name: "src", value: magnetLink)]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
func userItems() async throws -> [UserItem] {
|
||||
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
|
||||
}
|
||||
|
||||
return rawResponse.files
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ItemDetailsResponse.self, from: data)
|
||||
|
||||
return rawResponse
|
||||
}
|
||||
|
||||
func deleteItem(itemID: 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)]
|
||||
|
||||
request.httpBody = bodyComponents.query?.data(using: .utf8)
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,17 +8,6 @@
|
|||
import Foundation
|
||||
import KeychainSwift
|
||||
|
||||
public enum RealDebridError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
||||
public class RealDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let keychain = KeychainSwift()
|
||||
|
|
@ -38,7 +27,7 @@ public class RealDebrid {
|
|||
]
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
throw RealDebridError.InvalidUrl
|
||||
throw RDError.InvalidUrl
|
||||
}
|
||||
|
||||
let request = URLRequest(url: url)
|
||||
|
|
@ -49,7 +38,7 @@ public class RealDebrid {
|
|||
return rawResponse
|
||||
} catch {
|
||||
print("Couldn't get the new client creds!")
|
||||
throw RealDebridError.AuthQuery(description: error.localizedDescription)
|
||||
throw RDError.AuthQuery(description: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +51,7 @@ public class RealDebrid {
|
|||
]
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
throw RealDebridError.InvalidUrl
|
||||
throw RDError.InvalidUrl
|
||||
}
|
||||
|
||||
let request = URLRequest(url: url)
|
||||
|
|
@ -71,9 +60,9 @@ public class RealDebrid {
|
|||
authTask = Task {
|
||||
var count = 0
|
||||
|
||||
while count < 20 {
|
||||
while count < 12 {
|
||||
if Task.isCancelled {
|
||||
throw RealDebridError.AuthQuery(description: "Token request cancelled.")
|
||||
throw RDError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
|
@ -95,7 +84,7 @@ public class RealDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
throw RealDebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
}
|
||||
|
||||
if case let .failure(error) = await authTask?.result {
|
||||
|
|
@ -106,11 +95,11 @@ public class RealDebrid {
|
|||
// Fetch all tokens for the user and store in keychain
|
||||
public func getTokens(deviceCode: String) async throws {
|
||||
guard let clientId = UserDefaults.standard.string(forKey: "RealDebrid.ClientId") else {
|
||||
throw RealDebridError.EmptyData
|
||||
throw RDError.EmptyData
|
||||
}
|
||||
|
||||
guard let clientSecret = keychain.get("RealDebrid.ClientSecret") else {
|
||||
throw RealDebridError.EmptyData
|
||||
throw RDError.EmptyData
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseAuthUrl)/token")!)
|
||||
|
|
@ -172,9 +161,9 @@ public class RealDebrid {
|
|||
}
|
||||
|
||||
// Wrapper request function which matches the responses and returns data
|
||||
@discardableResult public func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
||||
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
|
||||
guard let token = await fetchToken() else {
|
||||
throw RealDebridError.InvalidToken
|
||||
throw RDError.InvalidToken
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
|
@ -182,24 +171,24 @@ public class RealDebrid {
|
|||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw RealDebridError.FailedRequest(description: "No HTTP response given")
|
||||
throw RDError.FailedRequest(description: "No HTTP response given")
|
||||
}
|
||||
|
||||
if response.statusCode >= 200, response.statusCode <= 299 {
|
||||
return data
|
||||
} else if response.statusCode == 401 {
|
||||
try await deleteTokens()
|
||||
throw RealDebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
|
||||
throw RDError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
|
||||
} else {
|
||||
throw RealDebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
throw RDError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the magnet is streamable on RD
|
||||
// Currently does not work for batch links
|
||||
public func instantAvailability(magnetHashes: [String]) async throws -> [RealDebridIA] {
|
||||
var availableHashes: [RealDebridIA] = []
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!)
|
||||
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: "/"))")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
|
||||
|
|
@ -219,17 +208,17 @@ public class RealDebrid {
|
|||
if data.rd.count > 1 || data.rd[0].count > 1 {
|
||||
// Batch array
|
||||
let batches = data.rd.map { fileDict in
|
||||
let batchFiles: [RealDebridIABatchFile] = fileDict.map { key, value in
|
||||
let batchFiles: [RealDebrid.IABatchFile] = fileDict.map { key, value in
|
||||
// Force unwrapped ID. Is safe because ID is guaranteed on a successful response
|
||||
RealDebridIABatchFile(id: Int(key)!, fileName: value.filename)
|
||||
RealDebrid.IABatchFile(id: Int(key)!, fileName: value.filename)
|
||||
}.sorted(by: { $0.id < $1.id })
|
||||
|
||||
return RealDebridIABatch(files: batchFiles)
|
||||
return RealDebrid.IABatch(files: batchFiles)
|
||||
}
|
||||
|
||||
// RD files array
|
||||
// Possibly sort this in the future, but not sure how at the moment
|
||||
var files: [RealDebridIAFile] = []
|
||||
var files: [RealDebrid.IAFile] = []
|
||||
|
||||
for index in batches.indices {
|
||||
let batchFiles = batches[index].files
|
||||
|
|
@ -239,7 +228,7 @@ public class RealDebrid {
|
|||
|
||||
if !files.contains(where: { $0.name == batchFile.fileName }) {
|
||||
files.append(
|
||||
RealDebridIAFile(
|
||||
RealDebrid.IAFile(
|
||||
name: batchFile.fileName,
|
||||
batchIndex: index,
|
||||
batchFileIndex: batchFileIndex
|
||||
|
|
@ -251,8 +240,8 @@ public class RealDebrid {
|
|||
|
||||
// TTL: 5 minutes
|
||||
availableHashes.append(
|
||||
RealDebridIA(
|
||||
hash: hash,
|
||||
RealDebrid.IA(
|
||||
magnet: Magnet(hash: hash, link: nil),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files,
|
||||
batches: batches
|
||||
|
|
@ -260,8 +249,8 @@ public class RealDebrid {
|
|||
)
|
||||
} else {
|
||||
availableHashes.append(
|
||||
RealDebridIA(
|
||||
hash: hash,
|
||||
RealDebrid.IA(
|
||||
magnet: Magnet(hash: hash, link: nil),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300
|
||||
)
|
||||
)
|
||||
|
|
@ -272,7 +261,11 @@ public class RealDebrid {
|
|||
}
|
||||
|
||||
// Adds a magnet link to the user's RD account
|
||||
public func addMagnet(magnetLink: String) async throws -> String {
|
||||
public func addMagnet(magnet: Magnet) async throws -> String {
|
||||
guard let magnetLink = magnet.link else {
|
||||
throw RDError.FailedRequest(description: "The magnet link is invalid")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/addMagnet")!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
|
@ -319,9 +312,9 @@ public class RealDebrid {
|
|||
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" {
|
||||
return torrentLink
|
||||
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
|
||||
throw RealDebridError.EmptyTorrents
|
||||
throw RDError.EmptyTorrents
|
||||
} else {
|
||||
throw RealDebridError.EmptyData
|
||||
throw RDError.EmptyData
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -369,4 +362,11 @@ public class RealDebrid {
|
|||
|
||||
return rawResponse
|
||||
}
|
||||
|
||||
public func deleteDownload(debridID: String) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads/delete/\(debridID)")!)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,18 @@ public class Application {
|
|||
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "0"
|
||||
}
|
||||
|
||||
// Debug = development, Nightly = actions, Release = stable
|
||||
var buildType: String {
|
||||
#if DEBUG
|
||||
return "Debug"
|
||||
#else
|
||||
return "Release"
|
||||
if Bundle.main.isNightly {
|
||||
return "Nightly"
|
||||
} else {
|
||||
return "Release"
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
let osVersion: OperatingSystemVersion = ProcessInfo().operatingSystemVersion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ public class Bookmark: NSManagedObject {
|
|||
title: title,
|
||||
source: source,
|
||||
size: size,
|
||||
magnetLink: magnetLink,
|
||||
magnetHash: magnetHash,
|
||||
magnet: Magnet(hash: magnetHash, link: magnetLink),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
|
|
|
|||
|
|
@ -112,9 +112,11 @@ struct PersistenceController {
|
|||
newBookmark.magnetLink = bookmarkJson.magnetLink
|
||||
newBookmark.seeders = bookmarkJson.seeders
|
||||
newBookmark.leechers = bookmarkJson.leechers
|
||||
|
||||
save(backgroundContext)
|
||||
}
|
||||
|
||||
func createHistory(entryJson: HistoryEntryJson, date: Double?) {
|
||||
func createHistory(_ entryJson: HistoryEntryJson, date: Double? = nil) {
|
||||
let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
|
||||
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate)
|
||||
|
||||
|
|
@ -153,6 +155,8 @@ struct PersistenceController {
|
|||
|
||||
newHistoryEntry.parentHistory?.dateString = historyDateString
|
||||
newHistoryEntry.parentHistory?.date = historyDate
|
||||
|
||||
save(backgroundContext)
|
||||
}
|
||||
|
||||
func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? {
|
||||
|
|
|
|||
17
Ferrite/Extensions/Array.swift
Normal file
17
Ferrite/Extensions/Array.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Array.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 12/4/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array {
|
||||
// From https://www.hackingwithswift.com/example-code/language/how-to-split-an-array-into-chunks
|
||||
func chunked(into size: Int) -> [[Element]] {
|
||||
stride(from: 0, to: count, by: size).map {
|
||||
Array(self[$0 ..< Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Ferrite/Extensions/Bundle.swift
Normal file
18
Ferrite/Extensions/Bundle.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Bundle.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 12/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Bundle {
|
||||
var commitHash: String? {
|
||||
infoDictionary?["GitCommitHash"] as? String
|
||||
}
|
||||
|
||||
var isNightly: Bool {
|
||||
infoDictionary?["IsNightly"] as? Bool ?? false
|
||||
}
|
||||
}
|
||||
26
Ferrite/Extensions/Set.swift
Normal file
26
Ferrite/Extensions/Set.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Array.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/26/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Set: RawRepresentable where Element: Codable {
|
||||
public init?(rawValue: String) {
|
||||
guard let data = rawValue.data(using: .utf8),
|
||||
let result = try? JSONDecoder().decode(Set<Element>.self, from: data)
|
||||
else { return nil }
|
||||
self = result
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
guard let data = try? JSONEncoder().encode(self),
|
||||
let result = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return "[]"
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,21 @@
|
|||
//
|
||||
// Created by Brian Dashore on 8/31/22.
|
||||
//
|
||||
// From https://stackoverflow.com/a/59307884
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
// From https://www.hackingwithswift.com/example-code/strings/how-to-capitalize-the-first-letter-of-a-string
|
||||
func capitalizingFirstLetter() -> String {
|
||||
prefix(1).capitalized + dropFirst()
|
||||
}
|
||||
|
||||
mutating func capitalizeFirstLetter() {
|
||||
self = capitalizingFirstLetter()
|
||||
}
|
||||
|
||||
// From https://stackoverflow.com/a/59307884
|
||||
private func compare(toVersion targetVersion: String) -> ComparisonResult {
|
||||
let versionDelimiter = "."
|
||||
var result: ComparisonResult = .orderedSame
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ extension View {
|
|||
))
|
||||
}
|
||||
|
||||
// From https://github.com/siteline/SwiftUI-Introspect/pull/129
|
||||
public func introspectSearchController(customize: @escaping (UISearchController) -> Void) -> some View {
|
||||
introspectNavigationController { navigationController in
|
||||
let navigationBar = navigationController.navigationBar
|
||||
if let searchController = navigationBar.topItem?.searchController {
|
||||
customize(searchController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Modifiers
|
||||
|
||||
func conditionalContextMenu(id: some Hashable,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,19 @@
|
|||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Ferrite</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ferrite://</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
|
|
|||
169
Ferrite/Models/AllDebridModels.swift
Normal file
169
Ferrite/Models/AllDebridModels.swift
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//
|
||||
// AllDebridModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/25/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension AllDebrid {
|
||||
// MARK: - Errors
|
||||
|
||||
// TODO: Hybridize debrid errors in one structure
|
||||
enum ADError: Error {
|
||||
case InvalidUrl
|
||||
case InvalidPostBody
|
||||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
||||
// MARK: - Generic AllDebrid response
|
||||
|
||||
// Uses a generic parametr for whatever underlying response is present
|
||||
struct ADResponse<ADData: Codable>: Codable {
|
||||
let status: String
|
||||
let data: ADData
|
||||
}
|
||||
|
||||
// MARK: - PinResponse
|
||||
|
||||
struct PinResponse: Codable {
|
||||
let pin, check: String
|
||||
let expiresIn: Int
|
||||
let userURL, baseURL, checkURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pin, check
|
||||
case expiresIn = "expires_in"
|
||||
case userURL = "user_url"
|
||||
case baseURL = "base_url"
|
||||
case checkURL = "check_url"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ApiKeyResponse
|
||||
|
||||
struct ApiKeyResponse: Codable {
|
||||
let apikey: String
|
||||
let activated: Bool
|
||||
let expiresIn: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case apikey, activated
|
||||
case expiresIn = "expires_in"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AddMagnetResponse
|
||||
|
||||
struct AddMagnetResponse: Codable {
|
||||
let magnets: [AddMagnetData]
|
||||
}
|
||||
|
||||
// MARK: - AddMagnetData
|
||||
|
||||
internal struct AddMagnetData: Codable {
|
||||
let magnet, hash, name, filenameOriginal: String
|
||||
let size: Int
|
||||
let ready: Bool
|
||||
let id: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case magnet, hash, name
|
||||
case filenameOriginal = "filename_original"
|
||||
case size, ready, id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MagnetStatusResponse
|
||||
|
||||
struct MagnetStatusResponse: Codable {
|
||||
let magnets: [MagnetStatusData]
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
if let data = try? container.decode(MagnetStatusData.self, forKey: .magnets) {
|
||||
magnets = [data]
|
||||
} else if let data = try? container.decode([MagnetStatusData].self, forKey: .magnets) {
|
||||
magnets = data
|
||||
} else {
|
||||
magnets = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MagnetStatusData
|
||||
|
||||
struct MagnetStatusData: Codable {
|
||||
let id: Int
|
||||
let filename: String
|
||||
let size: Int
|
||||
let hash, status: String
|
||||
let statusCode, downloaded, uploaded, seeders: Int
|
||||
let downloadSpeed, processingPerc, uploadSpeed, uploadDate: Int
|
||||
let completionDate: Int
|
||||
let links: [MagnetStatusLink]
|
||||
let type: String
|
||||
let notified: Bool
|
||||
let version: Int
|
||||
}
|
||||
|
||||
// MARK: - MagnetStatusLink
|
||||
|
||||
// Abridged for required parameters
|
||||
internal struct MagnetStatusLink: Codable {
|
||||
let link: String
|
||||
let filename: String
|
||||
let size: Int
|
||||
}
|
||||
|
||||
// MARK: - UnlockLinkResponse
|
||||
|
||||
// Abridged for required parameters
|
||||
struct UnlockLinkResponse: Codable {
|
||||
let link: String
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailabilityResponse
|
||||
|
||||
struct InstantAvailabilityResponse: Codable {
|
||||
let magnets: [InstantAvailabilityMagnet]
|
||||
}
|
||||
|
||||
// MARK: - IAMagnetResponse
|
||||
|
||||
internal struct InstantAvailabilityMagnet: Codable {
|
||||
let magnet, hash: String
|
||||
let instant: Bool
|
||||
let files: [InstantAvailabilityFile]?
|
||||
}
|
||||
|
||||
// MARK: - IAFileResponse
|
||||
|
||||
internal struct InstantAvailabilityFile: Codable {
|
||||
let name: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name = "n"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - InstantAvailablity client side structures
|
||||
|
||||
struct IA: Codable, Hashable {
|
||||
let magnet: Magnet
|
||||
let expiryTimeStamp: Double
|
||||
var files: [IAFile]
|
||||
}
|
||||
|
||||
struct IAFile: Codable, Hashable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
|
||||
public struct Backup: Codable {
|
||||
let version: Int
|
||||
var bookmarks: [BookmarkJson]?
|
||||
var history: [HistoryJson]?
|
||||
var sourceNames: [String]?
|
||||
|
|
@ -16,7 +17,16 @@ public struct Backup: Codable {
|
|||
|
||||
// MARK: - CoreData translation
|
||||
|
||||
typealias BookmarkJson = SearchResult
|
||||
// Don't typealias to search result as this is a reflection of CoreData's struct
|
||||
struct BookmarkJson: Codable {
|
||||
let title: String?
|
||||
let source: String
|
||||
let size: String?
|
||||
let magnetLink: String?
|
||||
let magnetHash: String?
|
||||
let seeders: String?
|
||||
let leechers: String?
|
||||
}
|
||||
|
||||
// Date is an epoch timestamp
|
||||
struct HistoryJson: Codable {
|
||||
|
|
@ -26,10 +36,10 @@ struct HistoryJson: Codable {
|
|||
}
|
||||
|
||||
struct HistoryEntryJson: Codable {
|
||||
let name: String
|
||||
let subName: String?
|
||||
let url: String
|
||||
let timeStamp: Double?
|
||||
var name: String? = nil
|
||||
var subName: String? = nil
|
||||
var url: String? = nil
|
||||
var timeStamp: Double? = nil
|
||||
let source: String?
|
||||
}
|
||||
|
||||
|
|
|
|||
99
Ferrite/Models/DebridManagerModels.swift
Normal file
99
Ferrite/Models/DebridManagerModels.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// DebridManagerModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/27/22.
|
||||
//
|
||||
|
||||
import Base32
|
||||
import Foundation
|
||||
|
||||
// MARK: - Universal IA enum (IA = InstantAvailability)
|
||||
|
||||
public enum IAStatus: Codable, Hashable, Sendable {
|
||||
case full
|
||||
case partial
|
||||
case none
|
||||
}
|
||||
|
||||
// MARK: - Enum for debrid differentiation. 0 is nil
|
||||
|
||||
public enum DebridType: Int, Codable, Hashable, CaseIterable {
|
||||
case realDebrid = 1
|
||||
case allDebrid = 2
|
||||
case premiumize = 3
|
||||
|
||||
func toString(abbreviated: Bool = false) -> String {
|
||||
switch self {
|
||||
case .realDebrid:
|
||||
return abbreviated ? "RD" : "RealDebrid"
|
||||
case .allDebrid:
|
||||
return abbreviated ? "AD" : "AllDebrid"
|
||||
case .premiumize:
|
||||
return abbreviated ? "PM" : "Premiumize"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper struct for magnet links to contain both the link and hash for easy access
|
||||
public struct Magnet: Codable, Hashable, Sendable {
|
||||
var hash: String?
|
||||
var link: String?
|
||||
|
||||
init(hash: String?, link: String?, title: String? = nil, trackers: [String]? = nil) {
|
||||
if let hash, link == nil {
|
||||
self.hash = parseHash(hash)
|
||||
self.link = generateLink(hash: hash, title: title, trackers: trackers)
|
||||
} else if let link, hash == nil {
|
||||
self.link = link
|
||||
self.hash = parseHash(extractHash(link: link))
|
||||
} else {
|
||||
self.hash = parseHash(hash)
|
||||
self.link = link
|
||||
}
|
||||
}
|
||||
|
||||
func generateLink(hash: String, title: String?, trackers: [String]?) -> String {
|
||||
var magnetLinkArray = ["magnet:?xt=urn:btih:", hash]
|
||||
|
||||
if let title, let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
|
||||
magnetLinkArray.append("&dn=\(encodedTitle)")
|
||||
}
|
||||
|
||||
if let trackers {
|
||||
for trackerUrl in trackers {
|
||||
if URL(string: trackerUrl) != nil,
|
||||
let encodedUrlString = trackerUrl.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)
|
||||
{
|
||||
magnetLinkArray.append("&tr=\(encodedUrlString)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return magnetLinkArray.joined()
|
||||
}
|
||||
|
||||
func extractHash(link: String) -> String? {
|
||||
if let firstSplit = link.split(separator: ":")[safe: 3],
|
||||
let tempHash = firstSplit.split(separator: "&")[safe: 0]
|
||||
{
|
||||
return String(tempHash)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Is this a Base32hex hash?
|
||||
func parseHash(_ magnetHash: String?) -> String? {
|
||||
guard let magnetHash else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if magnetHash.count == 32 {
|
||||
let decryptedMagnetHash = base32DecodeToData(String(magnetHash))
|
||||
return decryptedMagnetHash?.hexEncodedString()
|
||||
} else {
|
||||
return String(magnetHash).lowercased()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,14 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct GithubRelease: Codable, Hashable, Sendable {
|
||||
let htmlUrl: String
|
||||
let tagName: String
|
||||
public extension Github {
|
||||
struct Release: Codable, Hashable, Sendable {
|
||||
let htmlUrl: String
|
||||
let tagName: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case htmlUrl = "html_url"
|
||||
case tagName = "tag_name"
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case htmlUrl = "html_url"
|
||||
case tagName = "tag_name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
Ferrite/Models/PremiumizeModels.swift
Normal file
104
Ferrite/Models/PremiumizeModels.swift
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// PremiumizeModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/28/22.
|
||||
//
|
||||
|
||||
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 {
|
||||
let status: String
|
||||
let response: [Bool]
|
||||
}
|
||||
|
||||
// MARK: - DDLResponse
|
||||
|
||||
struct DDLResponse: Codable {
|
||||
let status: String
|
||||
let content: [DDLData]
|
||||
let location: String
|
||||
let filename: String
|
||||
let filesize: Int
|
||||
}
|
||||
|
||||
// MARK: Content
|
||||
|
||||
struct DDLData: Codable {
|
||||
let path: String
|
||||
let size: Int
|
||||
let link: String
|
||||
let streamLink: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case path, size, link
|
||||
case streamLink = "stream_link"
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
let status: String
|
||||
let files: [UserItem]
|
||||
}
|
||||
|
||||
// MARK: User Items
|
||||
|
||||
// Abridged for required parameters
|
||||
struct UserItem: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let mimeType: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name
|
||||
case mimeType = "mime_type"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ItemDetailsResponse
|
||||
|
||||
// Abridged for required parameters
|
||||
struct ItemDetailsResponse: Codable {
|
||||
let id: String
|
||||
let name: String
|
||||
let link: String
|
||||
let mimeType: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, name, link
|
||||
case mimeType = "mime_type"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,187 +8,197 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
// MARK: - device code endpoint
|
||||
public extension RealDebrid {
|
||||
// MARK: - Errors
|
||||
|
||||
public struct DeviceCodeResponse: Codable, Sendable {
|
||||
let deviceCode, userCode: String
|
||||
let interval, expiresIn: Int
|
||||
let verificationURL, directVerificationURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case deviceCode = "device_code"
|
||||
case userCode = "user_code"
|
||||
case interval
|
||||
case expiresIn = "expires_in"
|
||||
case verificationURL = "verification_url"
|
||||
case directVerificationURL = "direct_verification_url"
|
||||
// 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 credentials endpoint
|
||||
// MARK: - device code endpoint
|
||||
|
||||
public struct DeviceCredentialsResponse: Codable, Sendable {
|
||||
let clientID, clientSecret: String?
|
||||
struct DeviceCodeResponse: Codable, Sendable {
|
||||
let deviceCode, userCode: String
|
||||
let interval, expiresIn: Int
|
||||
let verificationURL, directVerificationURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case clientID = "client_id"
|
||||
case clientSecret = "client_secret"
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case deviceCode = "device_code"
|
||||
case userCode = "user_code"
|
||||
case interval
|
||||
case expiresIn = "expires_in"
|
||||
case verificationURL = "verification_url"
|
||||
case directVerificationURL = "direct_verification_url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - token endpoint
|
||||
// MARK: - device credentials endpoint
|
||||
|
||||
public struct TokenResponse: Codable, Sendable {
|
||||
let accessToken: String
|
||||
let expiresIn: Int
|
||||
let refreshToken, tokenType: String
|
||||
struct DeviceCredentialsResponse: Codable, Sendable {
|
||||
let clientID, clientSecret: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case expiresIn = "expires_in"
|
||||
case refreshToken = "refresh_token"
|
||||
case tokenType = "token_type"
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case clientID = "client_id"
|
||||
case clientSecret = "client_secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - instantAvailability endpoint
|
||||
// MARK: - token endpoint
|
||||
|
||||
// Thanks Skitty!
|
||||
public struct InstantAvailabilityResponse: Codable, Sendable {
|
||||
var data: InstantAvailabilityData?
|
||||
struct TokenResponse: Codable, Sendable {
|
||||
let accessToken: String
|
||||
let expiresIn: Int
|
||||
let refreshToken, tokenType: String
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case expiresIn = "expires_in"
|
||||
case refreshToken = "refresh_token"
|
||||
case tokenType = "token_type"
|
||||
}
|
||||
}
|
||||
|
||||
if let data = try? container.decode(InstantAvailabilityData.self) {
|
||||
self.data = data
|
||||
// MARK: - instantAvailability endpoint
|
||||
|
||||
// Thanks Skitty!
|
||||
struct InstantAvailabilityResponse: Codable, Sendable {
|
||||
var data: InstantAvailabilityData?
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if let data = try? container.decode(InstantAvailabilityData.self) {
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal struct InstantAvailabilityData: Codable, Sendable {
|
||||
var rd: [[String: InstantAvailabilityInfo]]
|
||||
}
|
||||
|
||||
internal struct InstantAvailabilityInfo: Codable, Sendable {
|
||||
var filename: String
|
||||
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] = []
|
||||
}
|
||||
|
||||
struct IABatch: Codable, Hashable, Sendable {
|
||||
let files: [IABatchFile]
|
||||
}
|
||||
|
||||
struct IABatchFile: Codable, Hashable, Sendable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
|
||||
struct IAFile: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let batchIndex: Int
|
||||
let batchFileIndex: Int
|
||||
}
|
||||
|
||||
// MARK: - addMagnet endpoint
|
||||
|
||||
struct AddMagnetResponse: Codable, Sendable {
|
||||
let id: String
|
||||
let uri: String
|
||||
}
|
||||
|
||||
// MARK: - torrentInfo endpoint
|
||||
|
||||
internal struct TorrentInfoResponse: Codable, Sendable {
|
||||
let id, filename, originalFilename, hash: String
|
||||
let bytes, originalBytes: Int
|
||||
let host: String
|
||||
let split, progress: Int
|
||||
let status, added: String
|
||||
let files: [TorrentInfoFile]
|
||||
let links: [String]
|
||||
let ended: String?
|
||||
let speed: Int?
|
||||
let seeders: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename
|
||||
case originalFilename = "original_filename"
|
||||
case hash, bytes
|
||||
case originalBytes = "original_bytes"
|
||||
case host, split, progress, status, added, files, links, ended, speed, seeders
|
||||
}
|
||||
}
|
||||
|
||||
internal struct TorrentInfoFile: Codable, Sendable {
|
||||
let id: Int
|
||||
let path: String
|
||||
let bytes, selected: Int
|
||||
}
|
||||
|
||||
struct UserTorrentsResponse: Codable, Hashable, Sendable {
|
||||
let id, filename, hash: String
|
||||
let bytes: Int
|
||||
let host: String
|
||||
let split, progress: Int
|
||||
let status, added: String
|
||||
let links: [String]
|
||||
let speed, seeders: Int?
|
||||
let ended: String?
|
||||
}
|
||||
|
||||
// MARK: - unrestrictLink endpoint
|
||||
|
||||
internal struct UnrestrictLinkResponse: Codable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
let hostIcon: String
|
||||
let chunks, crc: Int
|
||||
let download: String
|
||||
let streamable: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename, mimeType, filesize, link, host
|
||||
case hostIcon = "host_icon"
|
||||
case chunks, crc, download, streamable
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User downloads list
|
||||
|
||||
struct UserDownloadsResponse: Codable, Hashable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
let hostIcon: String
|
||||
let chunks: Int
|
||||
let download: String
|
||||
let streamable: Int
|
||||
let generated: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename, mimeType, filesize, link, host
|
||||
case hostIcon = "host_icon"
|
||||
case chunks, download, streamable, generated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Instant Availability client side structures
|
||||
|
||||
struct InstantAvailabilityData: Codable, Sendable {
|
||||
var rd: [[String: InstantAvailabilityInfo]]
|
||||
}
|
||||
|
||||
struct InstantAvailabilityInfo: Codable, Sendable {
|
||||
var filename: String
|
||||
var filesize: Int
|
||||
}
|
||||
|
||||
public struct RealDebridIA: Codable, Hashable, Sendable {
|
||||
let hash: String
|
||||
let expiryTimeStamp: Double
|
||||
var files: [RealDebridIAFile] = []
|
||||
var batches: [RealDebridIABatch] = []
|
||||
}
|
||||
|
||||
public struct RealDebridIABatch: Codable, Hashable, Sendable {
|
||||
let files: [RealDebridIABatchFile]
|
||||
}
|
||||
|
||||
public struct RealDebridIABatchFile: Codable, Hashable, Sendable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
|
||||
public struct RealDebridIAFile: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let batchIndex: Int
|
||||
let batchFileIndex: Int
|
||||
}
|
||||
|
||||
public enum RealDebridIAStatus: Codable, Hashable, Sendable {
|
||||
case full
|
||||
case partial
|
||||
case none
|
||||
}
|
||||
|
||||
// MARK: - addMagnet endpoint
|
||||
|
||||
public struct AddMagnetResponse: Codable, Sendable {
|
||||
let id: String
|
||||
let uri: String
|
||||
}
|
||||
|
||||
// MARK: - torrentInfo endpoint
|
||||
|
||||
struct TorrentInfoResponse: Codable, Sendable {
|
||||
let id, filename, originalFilename, hash: String
|
||||
let bytes, originalBytes: Int
|
||||
let host: String
|
||||
let split, progress: Int
|
||||
let status, added: String
|
||||
let files: [TorrentInfoFile]
|
||||
let links: [String]
|
||||
let ended: String?
|
||||
let speed: Int?
|
||||
let seeders: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename
|
||||
case originalFilename = "original_filename"
|
||||
case hash, bytes
|
||||
case originalBytes = "original_bytes"
|
||||
case host, split, progress, status, added, files, links, ended, speed, seeders
|
||||
}
|
||||
}
|
||||
|
||||
struct TorrentInfoFile: Codable, Sendable {
|
||||
let id: Int
|
||||
let path: String
|
||||
let bytes, selected: Int
|
||||
}
|
||||
|
||||
public struct UserTorrentsResponse: Codable, Sendable {
|
||||
let id, filename, hash: String
|
||||
let bytes: Int
|
||||
let host: String
|
||||
let split, progress: Int
|
||||
let status, added: String
|
||||
let links: [String]
|
||||
let speed, seeders: Int?
|
||||
let ended: String?
|
||||
}
|
||||
|
||||
// MARK: - unrestrictLink endpoint
|
||||
|
||||
struct UnrestrictLinkResponse: Codable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
let hostIcon: String
|
||||
let chunks, crc: Int
|
||||
let download: String
|
||||
let streamable: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename, mimeType, filesize, link, host
|
||||
case hostIcon = "host_icon"
|
||||
case chunks, crc, download, streamable
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User downloads list
|
||||
|
||||
public struct UserDownloadsResponse: Codable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
let hostIcon: String
|
||||
let chunks: Int
|
||||
let download: String
|
||||
let streamable: Int
|
||||
let generated: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename, mimeType, filesize, link, host
|
||||
case hostIcon = "host_icon"
|
||||
case chunks, download, streamable, generated
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct SearchResult: Hashable, Codable, Sendable {
|
||||
public struct SearchResult: Codable, Hashable, Sendable {
|
||||
let title: String?
|
||||
let source: String
|
||||
let size: String?
|
||||
let magnetLink: String?
|
||||
let magnetHash: String?
|
||||
let magnet: Magnet
|
||||
let seeders: String?
|
||||
let leechers: String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
import Foundation
|
||||
|
||||
public class BackupManager: ObservableObject {
|
||||
// Constant variable for backup versions
|
||||
let latestBackupVersion: Int = 1
|
||||
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var showRestoreAlert = false
|
||||
|
|
@ -18,7 +21,7 @@ public class BackupManager: ObservableObject {
|
|||
@Published var selectedBackupUrl: URL?
|
||||
|
||||
func createBackup() {
|
||||
var backup = Backup()
|
||||
var backup = Backup(version: latestBackupVersion)
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
|
|
@ -123,7 +126,7 @@ public class BackupManager: ObservableObject {
|
|||
if let storedHistories = backup.history {
|
||||
for storedHistory in storedHistories {
|
||||
for storedEntry in storedHistory.entries {
|
||||
PersistenceController.shared.createHistory(entryJson: storedEntry, date: storedHistory.date)
|
||||
PersistenceController.shared.createHistory(storedEntry, date: storedHistory.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,137 +13,470 @@ public class DebridManager: ObservableObject {
|
|||
// Linked classes
|
||||
var toastModel: ToastViewModel?
|
||||
let realDebrid: RealDebrid = .init()
|
||||
let allDebrid: AllDebrid = .init()
|
||||
let premiumize: Premiumize = .init()
|
||||
|
||||
// UI Variables
|
||||
@Published var showWebView: Bool = false
|
||||
@Published var showAuthSession: Bool = false
|
||||
@Published var showLoadingProgress: Bool = false
|
||||
|
||||
// Service agnostic variables
|
||||
var currentDebridTask: Task<Void, Never>?
|
||||
|
||||
// RealDebrid auth variables
|
||||
@Published var realDebridEnabled: Bool = false {
|
||||
@Published var enabledDebrids: Set<DebridType> = [] {
|
||||
didSet {
|
||||
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
|
||||
UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var selectedDebridType: DebridType? {
|
||||
didSet {
|
||||
UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService")
|
||||
}
|
||||
}
|
||||
|
||||
var currentDebridTask: Task<Void, Never>?
|
||||
var downloadUrl: String = ""
|
||||
var authUrl: URL?
|
||||
|
||||
// RealDebrid auth variables
|
||||
@Published var realDebridAuthProcessing: Bool = false
|
||||
var realDebridAuthUrl: String = ""
|
||||
|
||||
// RealDebrid fetch variables
|
||||
@Published var realDebridIAValues: [RealDebridIA] = []
|
||||
var realDebridDownloadUrl: String = ""
|
||||
@Published var realDebridIAValues: [RealDebrid.IA] = []
|
||||
|
||||
@Published var showDeleteAlert: Bool = false
|
||||
|
||||
// TODO: Switch to an individual item based sheet system to remove these variables
|
||||
var selectedRealDebridItem: RealDebridIA?
|
||||
var selectedRealDebridFile: RealDebridIAFile?
|
||||
var selectedRealDebridItem: RealDebrid.IA?
|
||||
var selectedRealDebridFile: RealDebrid.IAFile?
|
||||
var selectedRealDebridID: String?
|
||||
|
||||
// RealDebrid cloud variables
|
||||
@Published var realDebridCloudTorrents: [RealDebrid.UserTorrentsResponse] = []
|
||||
@Published var realDebridCloudDownloads: [RealDebrid.UserDownloadsResponse] = []
|
||||
var realDebridCloudTTL: Double = 0.0
|
||||
|
||||
// AllDebrid auth variables
|
||||
@Published 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] = []
|
||||
var allDebridCloudTTL: Double = 0.0
|
||||
|
||||
// Premiumize auth variables
|
||||
@Published 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() {
|
||||
realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
||||
if let rawDebridList = UserDefaults.standard.string(forKey: "Debrid.EnabledArray"),
|
||||
let serializedDebridList = Set<DebridType>(rawValue: rawDebridList)
|
||||
{
|
||||
enabledDebrids = serializedDebridList
|
||||
}
|
||||
|
||||
// If a UserDefaults integer isn't set, it's usually 0
|
||||
let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService")
|
||||
selectedDebridType = DebridType(rawValue: rawPreferredService)
|
||||
|
||||
// If a user has one logged in service, automatically set the preferred service to that one
|
||||
if enabledDebrids.count == 1 {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
}
|
||||
}
|
||||
|
||||
public func populateDebridHashes(_ resultHashes: [String]) async {
|
||||
// TODO: Remove this after v0.6.0
|
||||
// Login cleanup function that's automatically run to switch to the new login system
|
||||
public func cleanupOldLogins() async {
|
||||
let realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
||||
if realDebridEnabled {
|
||||
enabledDebrids.insert(.realDebrid)
|
||||
UserDefaults.standard.set(false, forKey: "RealDebrid.Enabled")
|
||||
}
|
||||
|
||||
let allDebridEnabled = UserDefaults.standard.bool(forKey: "AllDebrid.Enabled")
|
||||
if allDebridEnabled {
|
||||
enabledDebrids.insert(.allDebrid)
|
||||
UserDefaults.standard.set(false, forKey: "AllDebrid.Enabled")
|
||||
}
|
||||
|
||||
let premiumizeEnabled = UserDefaults.standard.bool(forKey: "Premiumize.Enabled")
|
||||
if premiumizeEnabled {
|
||||
enabledDebrids.insert(.premiumize)
|
||||
UserDefaults.standard.set(false, forKey: "Premiumize.Enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper function to match error descriptions
|
||||
// Error can be suppressed to end user but must be printed in logs
|
||||
func sendDebridError(_ error: Error, prefix: String, presentError: Bool = true, cancelString: String? = nil) async {
|
||||
let error = error as NSError
|
||||
if presentError {
|
||||
if let cancelString, error.code == -999 {
|
||||
toastModel?.updateToastDescription(cancelString, newToastType: .info)
|
||||
} else if error.code != -999 {
|
||||
toastModel?.updateToastDescription("\(prefix): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
print("\(prefix): \(error)")
|
||||
}
|
||||
|
||||
// Cleans all cached IA values in the event of a full IA refresh
|
||||
public func clearIAValues() {
|
||||
realDebridIAValues = []
|
||||
allDebridIAValues = []
|
||||
premiumizeIAValues = []
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Common function to populate hashes for debrid services
|
||||
public func populateDebridIA(_ resultMagnets: [Magnet]) async {
|
||||
do {
|
||||
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 sendHashes = resultHashes.filter { hash in
|
||||
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if !sendHashes.isEmpty {
|
||||
let fetchedIAValues = try await realDebrid.instantAvailability(magnetHashes: sendHashes)
|
||||
if !sendMagnets.isEmpty {
|
||||
if enabledDebrids.contains(.realDebrid) {
|
||||
let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnets: sendMagnets)
|
||||
realDebridIAValues += fetchedRealDebridIA
|
||||
}
|
||||
|
||||
realDebridIAValues += fetchedIAValues
|
||||
if enabledDebrids.contains(.allDebrid) {
|
||||
let fetchedAllDebridIA = try await allDebrid.instantAvailability(magnets: sendMagnets)
|
||||
allDebridIAValues += fetchedAllDebridIA
|
||||
}
|
||||
|
||||
if enabledDebrids.contains(.premiumize) {
|
||||
// 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 {
|
||||
let error = error as NSError
|
||||
await sendDebridError(error, prefix: "Hash population error")
|
||||
}
|
||||
}
|
||||
|
||||
if error.code != -999 {
|
||||
toastModel?.updateToastDescription("RealDebrid hash error: \(error)")
|
||||
// Common function to match a magnet hash with a provided debrid service
|
||||
public func matchMagnetHash(_ magnet: Magnet) -> IAStatus {
|
||||
guard let magnetHash = magnet.hash else {
|
||||
return .none
|
||||
}
|
||||
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
guard let realDebridMatch = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
print("RealDebrid hash error: \(error)")
|
||||
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:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
public func matchSearchResult(result: SearchResult?) -> RealDebridIAStatus {
|
||||
guard let result else {
|
||||
return .none
|
||||
}
|
||||
|
||||
guard let debridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
if debridMatch.batches.isEmpty {
|
||||
return .full
|
||||
} else {
|
||||
return .partial
|
||||
}
|
||||
}
|
||||
|
||||
public func setSelectedRdResult(result: SearchResult) -> Bool {
|
||||
guard let magnetHash = result.magnetHash else {
|
||||
public func selectDebridResult(magnet: Magnet) -> Bool {
|
||||
guard let magnetHash = magnet.hash else {
|
||||
toastModel?.updateToastDescription("Could not find the torrent magnet hash")
|
||||
return false
|
||||
}
|
||||
|
||||
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) {
|
||||
selectedRealDebridItem = realDebridItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
|
||||
switch selectedDebridType {
|
||||
case .realDebrid:
|
||||
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) {
|
||||
selectedRealDebridItem = realDebridItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .allDebrid:
|
||||
if let allDebridItem = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) {
|
||||
selectedAllDebridItem = allDebridItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.updateToastDescription("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 {
|
||||
toastModel?.updateToastDescription("Could not find the associated Premiumize entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
case .none:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func authenticateRd() async {
|
||||
// MARK: - Authentication UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to authenticate with
|
||||
public func authenticateDebrid(debridType: DebridType) async {
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
let success = await authenticateRd()
|
||||
completeDebridAuth(debridType, success: success)
|
||||
case .allDebrid:
|
||||
let success = await authenticateAd()
|
||||
completeDebridAuth(debridType, success: success)
|
||||
case .premiumize:
|
||||
await authenticatePm()
|
||||
}
|
||||
}
|
||||
|
||||
// Callback to finish debrid auth since functions can be split
|
||||
func completeDebridAuth(_ debridType: DebridType, success: Bool = true) {
|
||||
if enabledDebrids.count == 1, success {
|
||||
selectedDebridType = enabledDebrids.first
|
||||
}
|
||||
|
||||
switch debridType {
|
||||
case .realDebrid:
|
||||
realDebridAuthProcessing = false
|
||||
case .allDebrid:
|
||||
allDebridAuthProcessing = false
|
||||
case .premiumize:
|
||||
premiumizeAuthProcessing = false
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper function to validate and present an auth URL to the user
|
||||
@discardableResult func validateAuthUrl(_ url: URL?, useAuthSession: Bool = false) -> Bool {
|
||||
guard let url else {
|
||||
toastModel?.updateToastDescription("Authentication Error: Invalid URL created: \(String(describing: url))")
|
||||
return false
|
||||
}
|
||||
|
||||
authUrl = url
|
||||
if useAuthSession {
|
||||
showAuthSession.toggle()
|
||||
} else {
|
||||
showWebView.toggle()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func authenticateRd() async -> Bool {
|
||||
do {
|
||||
realDebridAuthProcessing = true
|
||||
let verificationResponse = try await realDebrid.getVerificationInfo()
|
||||
|
||||
realDebridAuthUrl = verificationResponse.directVerificationURL
|
||||
showWebView.toggle()
|
||||
if validateAuthUrl(URL(string: verificationResponse.directVerificationURL)) {
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
enabledDebrids.insert(.realDebrid)
|
||||
} else {
|
||||
throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid")
|
||||
}
|
||||
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
|
||||
realDebridEnabled = true
|
||||
return true
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("RealDebrid authentication error: \(error)")
|
||||
realDebrid.authTask?.cancel()
|
||||
await sendDebridError(error, prefix: "RealDebrid authentication error")
|
||||
|
||||
print("RealDebrid authentication error: \(error)")
|
||||
realDebrid.authTask?.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func logoutRd() async {
|
||||
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)
|
||||
enabledDebrids.insert(.allDebrid)
|
||||
} else {
|
||||
throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid")
|
||||
}
|
||||
|
||||
return true
|
||||
} 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 {
|
||||
do {
|
||||
if let error {
|
||||
throw Premiumize.PMError.AuthQuery(description: "OAuth callback Error: \(error)")
|
||||
}
|
||||
|
||||
if let callbackUrl = url {
|
||||
try premiumize.handleAuthCallback(url: callbackUrl)
|
||||
enabledDebrids.insert(.premiumize)
|
||||
completeDebridAuth(.premiumize)
|
||||
} else {
|
||||
throw Premiumize.PMError.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
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Automatically resets the preferred debrid service if it was set to the logged out service
|
||||
if selectedDebridType == debridType {
|
||||
selectedDebridType = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func logoutRd() async {
|
||||
do {
|
||||
try await realDebrid.deleteTokens()
|
||||
realDebridEnabled = false
|
||||
realDebridAuthProcessing = false
|
||||
enabledDebrids.remove(.realDebrid)
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("RealDebrid logout error: \(error)")
|
||||
|
||||
print("RealDebrid logout error: \(error)")
|
||||
await sendDebridError(error, prefix: "RealDebrid logout error")
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchRdDownload(searchResult: SearchResult) async {
|
||||
private func logoutAd() {
|
||||
allDebrid.deleteTokens()
|
||||
enabledDebrids.remove(.allDebrid)
|
||||
|
||||
toastModel?.updateToastDescription("Please manually delete the AllDebrid API key", newToastType: .info)
|
||||
}
|
||||
|
||||
private func logoutPm() {
|
||||
premiumize.deleteTokens()
|
||||
enabledDebrids.remove(.premiumize)
|
||||
}
|
||||
|
||||
// MARK: - Debrid fetch UI linked functions
|
||||
|
||||
// Common function to delegate what debrid service to fetch from
|
||||
// Cloudinfo is used for any extra information provided by debrid cloud
|
||||
public func fetchDebridDownload(magnet: Magnet?, cloudInfo: String? = nil) async {
|
||||
defer {
|
||||
currentDebridTask = nil
|
||||
showLoadingProgress = false
|
||||
|
|
@ -151,83 +484,252 @@ public class DebridManager: ObservableObject {
|
|||
|
||||
showLoadingProgress = true
|
||||
|
||||
guard let magnetLink = searchResult.magnetLink else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("RealDebrid error: Invalid magnet link")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
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]
|
||||
}
|
||||
|
||||
do {
|
||||
var fileIds: [Int] = []
|
||||
|
||||
if let iaFile = selectedRealDebridFile {
|
||||
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
|
||||
return
|
||||
}
|
||||
|
||||
fileIds = iaBatchFromFile.files.map(\.id)
|
||||
}
|
||||
|
||||
// If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link
|
||||
let existingTorrents = try await realDebrid.userTorrents().filter { $0.hash == selectedRealDebridItem?.hash }
|
||||
|
||||
// If the links match from a user's downloads, no need to re-run a download
|
||||
if let existingTorrent = existingTorrents[safe: 0],
|
||||
let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0]
|
||||
if let torrentLink,
|
||||
let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink)
|
||||
{
|
||||
let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink }
|
||||
if let existingLink = existingLinks[safe: 0]?.download {
|
||||
realDebridDownloadUrl = existingLink
|
||||
} else {
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
realDebridDownloadUrl = downloadLink
|
||||
}
|
||||
|
||||
} else {
|
||||
downloadUrl = downloadLink
|
||||
} else if let magnet {
|
||||
// Add a magnet after all the cache checks fail
|
||||
selectedRealDebridID = try await realDebrid.addMagnet(magnetLink: magnetLink)
|
||||
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 torrentLink = try await realDebrid.torrentInfo(
|
||||
debridID: realDebridId,
|
||||
selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0
|
||||
)
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
realDebridDownloadUrl = downloadLink
|
||||
downloadUrl = downloadLink
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.")
|
||||
}
|
||||
} else {
|
||||
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
|
||||
}
|
||||
|
||||
// Fetch one more time to add updated data into the RD cloud cache
|
||||
await fetchRdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
switch error {
|
||||
case RealDebridError.EmptyTorrents:
|
||||
case RealDebrid.RDError.EmptyTorrents:
|
||||
showDeleteAlert.toggle()
|
||||
default:
|
||||
let error = error as NSError
|
||||
await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled")
|
||||
|
||||
switch error.code {
|
||||
case -999:
|
||||
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
|
||||
default:
|
||||
toastModel?.updateToastDescription("RealDebrid download error: \(error)")
|
||||
}
|
||||
|
||||
await deleteRdTorrent()
|
||||
await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false)
|
||||
}
|
||||
|
||||
showLoadingProgress = false
|
||||
|
||||
print("RealDebrid download error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteRdTorrent() async {
|
||||
if let realDebridId = selectedRealDebridID {
|
||||
try? await realDebrid.deleteTorrent(debridID: realDebridId)
|
||||
// Refreshes torrents and downloads from a RD user's account
|
||||
public func fetchRdCloud(bypassTTL: Bool = false) async {
|
||||
if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL {
|
||||
do {
|
||||
realDebridCloudTorrents = try await realDebrid.userTorrents()
|
||||
realDebridCloudDownloads = try await realDebrid.userDownloads()
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
selectedRealDebridID = nil
|
||||
do {
|
||||
if let lockedLink {
|
||||
downloadUrl = try await allDebrid.unlockLink(lockedLink: lockedLink)
|
||||
} else if let magnet {
|
||||
let magnetID = try await allDebrid.addMagnet(magnet: magnet)
|
||||
let lockedLink = try await allDebrid.fetchMagnetStatus(
|
||||
magnetId: magnetID,
|
||||
selectedIndex: selectedAllDebridFile?.id ?? 0
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// 5 minutes
|
||||
allDebridCloudTTL = Date().timeIntervalSince1970 + 300
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AlLDebrid cloud fetch error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAdMagnet(magnetId: Int) async {
|
||||
do {
|
||||
try await allDebrid.deleteMagnet(magnetId: magnetId)
|
||||
|
||||
await fetchAdCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "AllDebrid 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
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
if error.code != -999 {
|
||||
await sendDebridError(error, prefix: "Premiumize cloud fetch error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func deletePmItem(id: String) async {
|
||||
do {
|
||||
try await premiumize.deleteItem(itemID: id)
|
||||
|
||||
// Bypass TTL to get current RD values
|
||||
await fetchPmCloud(bypassTTL: true)
|
||||
} catch {
|
||||
await sendDebridError(error, prefix: "Premiumize cloud delete error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,11 +32,13 @@ class NavigationViewModel: ObservableObject {
|
|||
@Published var isEditingSearch: Bool = false
|
||||
@Published var isSearching: Bool = false
|
||||
|
||||
@Published var selectedSearchResult: SearchResult?
|
||||
@Published var selectedMagnet: Magnet?
|
||||
@Published var selectedHistoryInfo: HistoryEntryJson?
|
||||
@Published var resultFromCloud: Bool = false
|
||||
|
||||
// For giving information in magnet choice sheet
|
||||
@Published var selectedTitle: String?
|
||||
@Published var selectedBatchTitle: String?
|
||||
@Published var selectedTitle: String = ""
|
||||
@Published var selectedBatchTitle: String = ""
|
||||
|
||||
@Published var hideNavigationBar = false
|
||||
|
||||
|
|
@ -93,16 +95,18 @@ class NavigationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func runMagnetAction(magnetString: String?, _ action: DefaultMagnetActionType? = nil) {
|
||||
let selectedAction = action ?? defaultMagnetAction
|
||||
|
||||
guard let magnetLink = magnetString else {
|
||||
public func runMagnetAction(magnet: Magnet?, _ action: DefaultMagnetActionType? = nil) {
|
||||
// Fall back to selected magnet if the provided magnet is nil
|
||||
let magnet = magnet ?? selectedMagnet
|
||||
guard let magnetLink = magnet?.link else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("Magnet action error: The magnet link is invalid.")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let selectedAction = action ?? defaultMagnetAction
|
||||
|
||||
switch selectedAction {
|
||||
case .none:
|
||||
currentChoiceSheet = .magnet
|
||||
|
|
@ -123,22 +127,4 @@ class NavigationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// The timeStamp and date are nil because the create function will make them automatically
|
||||
PersistenceController.shared.createHistory(
|
||||
entryJson: HistoryEntryJson(
|
||||
name: name ?? "",
|
||||
subName: subName,
|
||||
url: url ?? "",
|
||||
timeStamp: nil,
|
||||
source: source
|
||||
),
|
||||
date: nil
|
||||
)
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import SwiftUI
|
|||
import SwiftyJSON
|
||||
|
||||
class ScrapingViewModel: ObservableObject {
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
// Link the toast view model for single-directional communication
|
||||
var toastModel: ToastViewModel?
|
||||
let byteCountFormatter: ByteCountFormatter = .init()
|
||||
|
|
@ -355,7 +353,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
source: source,
|
||||
existingSearchResult: searchResult
|
||||
),
|
||||
let magnetLink = newSearchResult.magnetLink,
|
||||
let magnetLink = newSearchResult.magnet.link,
|
||||
magnetLink.starts(with: "magnet:"),
|
||||
!tempResults.contains(newSearchResult)
|
||||
{
|
||||
|
|
@ -364,7 +362,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
} else if
|
||||
let searchResult,
|
||||
let magnetLink = searchResult.magnetLink,
|
||||
let magnetLink = searchResult.magnet.link,
|
||||
magnetLink.starts(with: "magnet:"),
|
||||
!tempResults.contains(searchResult)
|
||||
{
|
||||
|
|
@ -376,18 +374,16 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
public func parseJsonResult(_ result: JSON, jsonParser: SourceJsonParser, source: Source, existingSearchResult: SearchResult? = nil) -> SearchResult? {
|
||||
var magnetHash: String? = existingSearchResult?.magnetHash
|
||||
|
||||
var magnetHash: String? = existingSearchResult?.magnet.hash
|
||||
if let magnetHashParser = jsonParser.magnetHash {
|
||||
let rawHash = result[magnetHashParser.query.components(separatedBy: ".")].rawValue
|
||||
|
||||
if !(rawHash is NSNull) {
|
||||
magnetHash = fetchMagnetHash(existingHash: String(describing: rawHash))
|
||||
magnetHash = String(describing: rawHash)
|
||||
}
|
||||
}
|
||||
|
||||
var title: String? = existingSearchResult?.title
|
||||
|
||||
if let titleParser = jsonParser.title {
|
||||
if let existingTitle = existingSearchResult?.title,
|
||||
let discriminatorQuery = titleParser.discriminator
|
||||
|
|
@ -403,21 +399,13 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
var link: String? = existingSearchResult?.magnetLink
|
||||
|
||||
if let magnetLinkParser = jsonParser.magnetLink, existingSearchResult?.magnetLink == nil {
|
||||
var link: String? = existingSearchResult?.magnet.link
|
||||
if let magnetLinkParser = jsonParser.magnetLink, link == nil {
|
||||
let rawLink = result[magnetLinkParser.query.components(separatedBy: ".")].rawValue
|
||||
link = rawLink is NSNull ? nil : String(describing: rawLink)
|
||||
} else if let magnetHash {
|
||||
link = generateMagnetLink(magnetHash: magnetHash, title: title, trackers: source.trackers)
|
||||
}
|
||||
|
||||
if magnetHash == nil, let href = link {
|
||||
magnetHash = fetchMagnetHash(magnetLink: href)
|
||||
}
|
||||
|
||||
var size: String? = existingSearchResult?.size
|
||||
|
||||
if let sizeParser = jsonParser.size, existingSearchResult?.size == nil {
|
||||
let rawSize = result[sizeParser.query.components(separatedBy: ".")].rawValue
|
||||
size = rawSize is NSNull ? nil : String(describing: rawSize)
|
||||
|
|
@ -446,8 +434,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
title: title,
|
||||
source: source.name,
|
||||
size: size,
|
||||
magnetLink: link,
|
||||
magnetHash: magnetHash,
|
||||
magnet: Magnet(hash: magnetHash, link: link, title: title, trackers: source.trackers),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
|
|
@ -478,15 +465,13 @@ class ScrapingViewModel: ObservableObject {
|
|||
// Parse magnet link or translate hash
|
||||
var magnetHash: String?
|
||||
if let magnetHashParser = rssParser.magnetHash {
|
||||
let tempHash = try? runRssComplexQuery(
|
||||
magnetHash = try? runRssComplexQuery(
|
||||
item: item,
|
||||
query: magnetHashParser.query,
|
||||
attribute: magnetHashParser.attribute,
|
||||
discriminator: magnetHashParser.discriminator,
|
||||
regexString: magnetHashParser.regex
|
||||
)
|
||||
|
||||
magnetHash = fetchMagnetHash(existingHash: tempHash)
|
||||
}
|
||||
|
||||
var title: String?
|
||||
|
|
@ -509,8 +494,6 @@ class ScrapingViewModel: ObservableObject {
|
|||
discriminator: magnetLinkParser.discriminator,
|
||||
regexString: magnetLinkParser.regex
|
||||
)
|
||||
} else if let magnetHash {
|
||||
link = generateMagnetLink(magnetHash: magnetHash, title: title, trackers: source.trackers)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
|
@ -519,10 +502,6 @@ class ScrapingViewModel: ObservableObject {
|
|||
continue
|
||||
}
|
||||
|
||||
if magnetHash == nil {
|
||||
magnetHash = fetchMagnetHash(magnetLink: href)
|
||||
}
|
||||
|
||||
var size: String?
|
||||
if let sizeParser = rssParser.size {
|
||||
size = try? runRssComplexQuery(
|
||||
|
|
@ -566,8 +545,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
title: title ?? "No title",
|
||||
source: source.name,
|
||||
size: size ?? "",
|
||||
magnetLink: href,
|
||||
magnetHash: magnetHash,
|
||||
magnet: Magnet(hash: magnetHash, link: href, title: title, trackers: source.trackers),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
|
|
@ -675,9 +653,6 @@ class ScrapingViewModel: ObservableObject {
|
|||
continue
|
||||
}
|
||||
|
||||
// Fetches the magnet hash
|
||||
let magnetHash = fetchMagnetHash(magnetLink: href)
|
||||
|
||||
// Fetches the episode/movie title
|
||||
var title: String?
|
||||
if let titleParser = htmlParser.title {
|
||||
|
|
@ -745,8 +720,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
title: title ?? "No title",
|
||||
source: source.name,
|
||||
size: size ?? "",
|
||||
magnetLink: href,
|
||||
magnetHash: magnetHash,
|
||||
magnet: Magnet(hash: nil, link: href),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
|
|
@ -788,31 +762,6 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetches and possibly converts the magnet hash value to sha1
|
||||
public func fetchMagnetHash(magnetLink: String? = nil, existingHash: String? = nil) -> String? {
|
||||
var magnetHash: String
|
||||
|
||||
if let existingHash {
|
||||
magnetHash = existingHash
|
||||
} else if
|
||||
let magnetLink,
|
||||
let firstSplit = magnetLink.split(separator: ":")[safe: 3],
|
||||
let tempHash = firstSplit.split(separator: "&")[safe: 0]
|
||||
{
|
||||
magnetHash = String(tempHash)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Is this a Base32hex hash?
|
||||
if magnetHash.count == 32 {
|
||||
let decryptedMagnetHash = base32DecodeToData(String(magnetHash))
|
||||
return decryptedMagnetHash?.hexEncodedString()
|
||||
} else {
|
||||
return String(magnetHash).lowercased()
|
||||
}
|
||||
}
|
||||
|
||||
func parseSizeString(sizeString: String) -> String? {
|
||||
// Test if the string can be a full integer
|
||||
guard let size = Int(sizeString) else {
|
||||
|
|
@ -835,28 +784,6 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func generateMagnetLink(magnetHash: String, title: String?, trackers: [String]?) -> String {
|
||||
var magnetLinkArray = ["magnet:?xt=urn:btih:"]
|
||||
|
||||
magnetLinkArray.append(magnetHash)
|
||||
|
||||
if let title, let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
|
||||
magnetLinkArray.append("&dn=\(encodedTitle)")
|
||||
}
|
||||
|
||||
if let trackers {
|
||||
for trackerUrl in trackers {
|
||||
if URL(string: trackerUrl) != nil,
|
||||
let encodedUrlString = trackerUrl.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)
|
||||
{
|
||||
magnetLinkArray.append("&tr=\(encodedUrlString)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return magnetLinkArray.joined()
|
||||
}
|
||||
|
||||
func cleanApiCreds(api: SourceApi) async {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ struct AboutView: View {
|
|||
ListRowTextView(leftText: "Version", rightText: Application.shared.appVersion)
|
||||
ListRowTextView(leftText: "Build number", rightText: Application.shared.appBuild)
|
||||
ListRowTextView(leftText: "Build type", rightText: Application.shared.buildType)
|
||||
|
||||
if let commitHash = Bundle.main.commitHash {
|
||||
ListRowTextView(leftText: "Commit", rightText: commitHash)
|
||||
}
|
||||
|
||||
ListRowLinkView(text: "Discord server", link: "https://discord.gg/sYQxnuD7Fj")
|
||||
ListRowLinkView(text: "GitHub repository", link: "https://github.com/bdashore3/Ferrite")
|
||||
} header: {
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
//
|
||||
// BatchChoiceView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BatchChoiceView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.name) {
|
||||
debridManager.selectedRealDebridFile = file
|
||||
|
||||
if let searchResult = navModel.selectedSearchResult {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: searchResult)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
// The download may complete before this sheet dismisses
|
||||
try? await Task.sleep(seconds: 1)
|
||||
navModel.selectedBatchTitle = file.name
|
||||
navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
|
||||
}
|
||||
|
||||
debridManager.selectedRealDebridFile = nil
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
navModel.currentChoiceSheet = nil
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("Select a file")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
navModel.currentChoiceSheet = nil
|
||||
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BatchChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BatchChoiceView()
|
||||
}
|
||||
}
|
||||
37
Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift
Normal file
37
Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// DebridChoiceView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/26/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DebridChoiceView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Picker("", selection: $debridManager.selectedDebridType) {
|
||||
Text("None")
|
||||
.tag(nil as DebridType?)
|
||||
|
||||
ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
Text(debridType.toString())
|
||||
.tag(DebridType?.some(debridType))
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid")
|
||||
}
|
||||
.animation(.none)
|
||||
}
|
||||
}
|
||||
|
||||
struct DebridChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DebridChoiceView()
|
||||
}
|
||||
}
|
||||
45
Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift
Normal file
45
Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// DebridLabelView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/27/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DebridLabelView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@State var cloudLinks: [String] = []
|
||||
var magnet: Magnet?
|
||||
|
||||
var body: some View {
|
||||
if let selectedDebridType = debridManager.selectedDebridType {
|
||||
Text(selectedDebridType.toString(abbreviated: true))
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background {
|
||||
Group {
|
||||
if let magnet, cloudLinks.isEmpty {
|
||||
switch debridManager.matchMagnetHash(magnet) {
|
||||
case .full:
|
||||
Color.green
|
||||
case .partial:
|
||||
Color.orange
|
||||
case .none:
|
||||
Color.red
|
||||
}
|
||||
} else if cloudLinks.count == 1 {
|
||||
Color.green
|
||||
} else if cloudLinks.count > 1 {
|
||||
Color.orange
|
||||
} else {
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,18 +13,17 @@ struct BookmarksView: View {
|
|||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var bookmarks: FetchedResults<Bookmark>
|
||||
@Binding var searchText: String
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
@State private var bookmarkPredicate: NSPredicate?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if !bookmarks.isEmpty {
|
||||
List {
|
||||
DynamicFetchRequest(predicate: bookmarkPredicate) { (bookmarks: FetchedResults<Bookmark>) in
|
||||
List {
|
||||
if !bookmarks.isEmpty {
|
||||
ForEach(bookmarks, id: \.self) { bookmark in
|
||||
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
|
||||
}
|
||||
|
|
@ -33,7 +32,7 @@ struct BookmarksView: View {
|
|||
if let bookmark = bookmarks[safe: index] {
|
||||
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: nil)
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,20 +48,36 @@ struct BookmarksView: View {
|
|||
PersistenceController.shared.save()
|
||||
}
|
||||
}
|
||||
.inlinedList()
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
.inlinedList()
|
||||
.listStyle(.insetGrouped)
|
||||
.onAppear {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
viewTask = Task {
|
||||
let magnets = bookmarks.compactMap {
|
||||
if let magnetHash = $0.magnetHash {
|
||||
return Magnet(hash: magnetHash, link: $0.magnetLink)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if realDebridEnabled {
|
||||
viewTask = Task {
|
||||
let hashes = bookmarks.compactMap(\.magnetHash)
|
||||
await debridManager.populateDebridHashes(hashes)
|
||||
}
|
||||
}
|
||||
applyPredicate()
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
.onChange(of: searchText) { _ in
|
||||
applyPredicate()
|
||||
}
|
||||
}
|
||||
|
||||
func applyPredicate() {
|
||||
bookmarkPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// AllDebridCloudView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/5/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AllDebridCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
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)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
}
|
||||
} 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.7, animation: .easeOut(duration: 0.2))
|
||||
.backport.tint(.black)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let magnet = debridManager.allDebridCloudMagnets[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteAdMagnet(magnetId: magnet.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewTask = Task {
|
||||
await debridManager.fetchAdCloud()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// PremiumizeCloudView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 1/2/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUIX
|
||||
|
||||
struct PremiumizeCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
|
||||
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()
|
||||
)
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
.backport.tint(.black)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let item = debridManager.premiumizeCloudItems[safe: index] {
|
||||
Task {
|
||||
await debridManager.deletePmItem(id: item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewTask = Task {
|
||||
await debridManager.fetchPmCloud()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// RealDebridCloudView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 12/31/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RealDebridCloudView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
|
||||
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()
|
||||
)
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
.backport.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)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
}
|
||||
} 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))
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let torrentResponse = debridManager.realDebridCloudTorrents[safe: index] {
|
||||
Task {
|
||||
await debridManager.deleteRdTorrent(torrentID: torrentResponse.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewTask = Task {
|
||||
await debridManager.fetchRdCloud()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Ferrite/Views/ComponentViews/Library/DebridCloudView.swift
Normal file
30
Ferrite/Views/ComponentViews/Library/DebridCloudView.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// DebridCloudView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 12/31/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DebridCloudView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,27 +16,27 @@ struct HistoryButtonView: View {
|
|||
|
||||
var body: some View {
|
||||
Button {
|
||||
navModel.selectedTitle = entry.name
|
||||
navModel.selectedBatchTitle = entry.subName
|
||||
navModel.selectedTitle = entry.name ?? ""
|
||||
navModel.selectedBatchTitle = entry.subName ?? ""
|
||||
|
||||
if let url = entry.url {
|
||||
if url.starts(with: "https://") {
|
||||
Task {
|
||||
debridManager.realDebridDownloadUrl = url
|
||||
debridManager.downloadUrl = url
|
||||
navModel.runDebridAction(urlString: url)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
navModel.runMagnetAction(magnetString: url)
|
||||
navModel.runMagnetAction(magnet: Magnet(hash: nil, link: url))
|
||||
}
|
||||
} else {
|
||||
toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.")
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(entry.name ?? "Unknown title")
|
||||
.font(entry.subName == nil ? .body : .subheadline)
|
||||
110
Ferrite/Views/ComponentViews/Library/HistoryView.swift
Normal file
110
Ferrite/Views/ComponentViews/Library/HistoryView.swift
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//
|
||||
// HistoryView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
var history: FetchedResults<History>
|
||||
|
||||
@Binding var searchText: String
|
||||
|
||||
@State private var historyPredicate: NSPredicate?
|
||||
|
||||
var body: some View {
|
||||
DynamicFetchRequest(predicate: historyPredicate) { (allEntries: FetchedResults<HistoryEntry>) in
|
||||
List {
|
||||
if !history.isEmpty {
|
||||
ForEach(groupedHistory(history), id: \.self) { historyGroup in
|
||||
HistorySectionView(allEntries: allEntries, historyGroup: historyGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
.onAppear {
|
||||
applyPredicate()
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
applyPredicate()
|
||||
}
|
||||
}
|
||||
|
||||
func applyPredicate() {
|
||||
if searchText.isEmpty {
|
||||
historyPredicate = nil
|
||||
} else {
|
||||
let namePredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText.lowercased())
|
||||
let subNamePredicate = NSPredicate(format: "subName CONTAINS[cd] %@", searchText.lowercased())
|
||||
historyPredicate = NSCompoundPredicate(type: .or, subpredicates: [namePredicate, subNamePredicate])
|
||||
}
|
||||
}
|
||||
|
||||
func groupedHistory(_ result: FetchedResults<History>) -> [[History]] {
|
||||
Dictionary(grouping: result) { (element: History) in
|
||||
element.dateString ?? ""
|
||||
}
|
||||
.values
|
||||
.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() }
|
||||
}
|
||||
}
|
||||
|
||||
struct HistorySectionView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var formatter: DateFormatter = .init()
|
||||
var allEntries: FetchedResults<HistoryEntry>
|
||||
var historyGroup: [History]
|
||||
|
||||
init(allEntries: FetchedResults<HistoryEntry>, historyGroup: [History]) {
|
||||
self.allEntries = allEntries
|
||||
self.historyGroup = historyGroup
|
||||
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .none
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if compareGroup(historyGroup) > 0 {
|
||||
Section(header: Text(formatter.string(from: historyGroup[0].date ?? Date()))) {
|
||||
ForEach(historyGroup, id: \.self) { history in
|
||||
ForEach(history.entryArray.filter { allEntries.contains($0) }, id: \.self) { entry in
|
||||
HistoryButtonView(entry: entry)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
removeEntry(at: offsets, from: history)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compareGroup(_ group: [History]) -> Int {
|
||||
var totalCount = 0
|
||||
for history in group {
|
||||
totalCount += history.entryArray.reduce(0) { result, item in
|
||||
result + (allEntries.contains { $0.name == item.name || (item.subName.map { !$0.isEmpty } ?? false && $0.subName == item.subName) } ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
return totalCount
|
||||
}
|
||||
|
||||
func removeEntry(at offsets: IndexSet, from history: History) {
|
||||
for index in offsets {
|
||||
if let entry = history.entryArray[safe: index] {
|
||||
history.removeFromEntries(entry)
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
}
|
||||
|
||||
if history.entryArray.isEmpty {
|
||||
PersistenceController.shared.delete(history, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
//
|
||||
// SearchResultButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultButtonView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
@State private var runOnce = false
|
||||
@State var existingBookmark: Bookmark? = nil
|
||||
@State private var showConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if debridManager.currentDebridTask == nil {
|
||||
navModel.selectedMagnet = result.magnet
|
||||
navModel.selectedTitle = result.title ?? ""
|
||||
navModel.resultFromCloud = false
|
||||
|
||||
switch debridManager.matchMagnetHash(result.magnet) {
|
||||
case .full:
|
||||
if debridManager.selectDebridResult(magnet: result.magnet) {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchDebridDownload(magnet: result.magnet)
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
PersistenceController.shared.createHistory(
|
||||
HistoryEntryJson(
|
||||
name: result.title,
|
||||
url: debridManager.downloadUrl,
|
||||
source: result.source
|
||||
)
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.selectDebridResult(magnet: result.magnet) {
|
||||
navModel.selectedHistoryInfo = HistoryEntryJson(
|
||||
name: result.title,
|
||||
source: result.source
|
||||
)
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
PersistenceController.shared.createHistory(
|
||||
HistoryEntryJson(
|
||||
name: result.title,
|
||||
url: result.magnet.link,
|
||||
source: result.source
|
||||
)
|
||||
)
|
||||
|
||||
navModel.runMagnetAction(magnet: result.magnet)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(result.title ?? "No title")
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(4)
|
||||
|
||||
SearchResultInfoView(result: result)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
}
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
.backport.tint(.primary)
|
||||
.conditionalContextMenu(id: existingBookmark) {
|
||||
ZStack {
|
||||
if let bookmark = existingBookmark {
|
||||
Button {
|
||||
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||
|
||||
// When the entity is deleted, let other instances know to remove that reference
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: existingBookmark)
|
||||
} label: {
|
||||
Text("Remove bookmark")
|
||||
Image(systemName: "bookmark.slash.fill")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
let newBookmark = Bookmark(context: backgroundContext)
|
||||
newBookmark.title = result.title
|
||||
newBookmark.source = result.source
|
||||
newBookmark.magnetHash = result.magnet.hash
|
||||
newBookmark.magnetLink = result.magnet.link
|
||||
newBookmark.seeders = result.seeders
|
||||
newBookmark.leechers = result.leechers
|
||||
|
||||
existingBookmark = newBookmark
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
} label: {
|
||||
Text("Bookmark")
|
||||
Image(systemName: "bookmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $debridManager.showDeleteAlert,
|
||||
title: "Caching file",
|
||||
message: "RealDebrid is currently caching this file. Would you like to delete it? \n\nProgress can be checked on the RealDebrid website.",
|
||||
buttons: [
|
||||
AlertButton("Yes", role: .destructive) {
|
||||
Task {
|
||||
await debridManager.deleteRdTorrent()
|
||||
}
|
||||
},
|
||||
AlertButton(role: .cancel)
|
||||
]
|
||||
)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { notification in
|
||||
// If the instance contains the deleted bookmark, remove it.
|
||||
if let deletedBookmark = notification.object as? Bookmark,
|
||||
let bookmark = existingBookmark,
|
||||
deletedBookmark.objectID == bookmark.objectID
|
||||
{
|
||||
existingBookmark = nil
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Only run a exists request if a bookmark isn't passed to the view
|
||||
if existingBookmark == nil, !runOnce {
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
bookmarkRequest.predicate = NSPredicate(
|
||||
format: "title == %@ AND source == %@ AND magnetLink == %@ AND magnetHash = %@",
|
||||
result.title ?? "",
|
||||
result.source,
|
||||
result.magnet.link ?? "",
|
||||
result.magnet.hash ?? ""
|
||||
)
|
||||
bookmarkRequest.fetchLimit = 1
|
||||
|
||||
if let fetchedBookmark = try? backgroundContext.fetch(bookmarkRequest).first {
|
||||
existingBookmark = fetchedBookmark
|
||||
}
|
||||
|
||||
runOnce = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// SearchResultRDView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/26/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultInfoView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(result.source)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let seeders = result.seeders {
|
||||
Text("S: \(seeders)")
|
||||
}
|
||||
|
||||
if let leechers = result.leechers {
|
||||
Text("L: \(leechers)")
|
||||
}
|
||||
|
||||
if let size = result.size {
|
||||
Text(size)
|
||||
}
|
||||
|
||||
DebridLabelView(magnet: result.magnet)
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ struct SettingsAppVersionView: View {
|
|||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
@State private var releases: [GithubRelease] = []
|
||||
@State private var releases: [Github.Release] = []
|
||||
|
||||
@State private var loadedReleases = false
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ struct SourceSettingsView: View {
|
|||
.onDisappear {
|
||||
PersistenceController.shared.save()
|
||||
}
|
||||
.navigationTitle("Source settings")
|
||||
.navigationTitle("Source Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
|
|
@ -14,13 +14,13 @@ struct ContentView: View {
|
|||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<Source>
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@State private var selectedSource: Source? {
|
||||
didSet {
|
||||
scrapingModel.filteredSource = selectedSource
|
||||
|
|
@ -64,6 +64,7 @@ struct ContentView: View {
|
|||
Image(systemName: "chevron.down")
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.animation(.none)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
|
@ -73,6 +74,9 @@ struct ContentView: View {
|
|||
SearchResultsView()
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.navigationBarTitleDisplayMode(
|
||||
navModel.isSearching && Application.shared.osVersion.majorVersion > 14 ? .inline : .large
|
||||
)
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search",
|
||||
text: $scrapingModel.searchText,
|
||||
|
|
@ -86,12 +90,18 @@ struct ContentView: View {
|
|||
let sources = sourceManager.fetchInstalledSources()
|
||||
await scrapingModel.scanSources(sources: sources)
|
||||
|
||||
if realDebridEnabled, !scrapingModel.searchResults.isEmpty {
|
||||
debridManager.realDebridIAValues = []
|
||||
if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty {
|
||||
debridManager.clearIAValues()
|
||||
|
||||
await debridManager.populateDebridHashes(
|
||||
scrapingModel.searchResults.compactMap(\.magnetHash)
|
||||
)
|
||||
// Remove magnets that don't have a hash
|
||||
let magnets = scrapingModel.searchResults.compactMap {
|
||||
if let magnetHash = $0.magnet.hash {
|
||||
return Magnet(hash: magnetHash, link: $0.magnet.link)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
|
||||
navModel.showSearchProgress = false
|
||||
|
|
@ -106,6 +116,16 @@ struct ContentView: View {
|
|||
scrapingModel.searchText = ""
|
||||
}
|
||||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
DebridChoiceView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUIX
|
||||
|
||||
struct LibraryView: View {
|
||||
enum LibraryPickerSegment {
|
||||
case bookmarks
|
||||
case history
|
||||
case debridCloud
|
||||
}
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@FetchRequest(
|
||||
entity: Bookmark.entity(),
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)
|
||||
]
|
||||
sortDescriptors: []
|
||||
) var bookmarks: FetchedResults<Bookmark>
|
||||
|
||||
@FetchRequest(
|
||||
|
|
@ -29,30 +30,55 @@ struct LibraryView: View {
|
|||
]
|
||||
) var history: FetchedResults<History>
|
||||
|
||||
@State private var historyEmpty = true
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@State private var selectedSegment: LibraryPickerSegment = .bookmarks
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
@State private var searchText: String = ""
|
||||
@State private var isEditingSearch = false
|
||||
@State private var isSearching = false
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
VStack {
|
||||
Picker("Segments", selection: $selectedSegment) {
|
||||
Text("Bookmarks").tag(LibraryPickerSegment.bookmarks)
|
||||
Text("History").tag(LibraryPickerSegment.history)
|
||||
|
||||
if !debridManager.enabledDebrids.isEmpty {
|
||||
Text("Cloud").tag(LibraryPickerSegment.debridCloud)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 5)
|
||||
|
||||
switch selectedSegment {
|
||||
case .bookmarks:
|
||||
BookmarksView(bookmarks: bookmarks)
|
||||
BookmarksView(searchText: $searchText)
|
||||
case .history:
|
||||
HistoryView(history: history)
|
||||
HistoryView(history: history, searchText: $searchText)
|
||||
case .debridCloud:
|
||||
DebridCloudView(searchText: $searchText)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
||||
isSearching = true
|
||||
})
|
||||
.showsCancelButton(isEditingSearch || isSearching)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
}
|
||||
.overlay {
|
||||
switch selectedSegment {
|
||||
case .bookmarks:
|
||||
|
|
@ -63,18 +89,27 @@ struct LibraryView: View {
|
|||
if history.isEmpty {
|
||||
EmptyInstructionView(title: "No History", message: "Start watching to build history")
|
||||
}
|
||||
case .debridCloud:
|
||||
if debridManager.selectedDebridType == nil {
|
||||
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Library")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
HStack(spacing: Application.shared.osVersion.majorVersion > 14 ? 10 : 18) {
|
||||
Spacer()
|
||||
EditButton()
|
||||
|
||||
if selectedSegment == .history {
|
||||
switch selectedSegment {
|
||||
case .bookmarks, .debridCloud:
|
||||
DebridChoiceView()
|
||||
case .history:
|
||||
HistoryActionsView()
|
||||
}
|
||||
}
|
||||
.animation(.none)
|
||||
}
|
||||
}
|
||||
.environment(\.editMode, $editMode)
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
//
|
||||
// HistoryView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var history: FetchedResults<History>
|
||||
var formatter: DateFormatter = .init()
|
||||
|
||||
@State private var historyIndex = 0
|
||||
|
||||
init(history: FetchedResults<History>) {
|
||||
self.history = history
|
||||
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .none
|
||||
}
|
||||
|
||||
func groupedEntries(_ result: FetchedResults<History>) -> [[History]] {
|
||||
Dictionary(grouping: result) { (element: History) in
|
||||
element.dateString ?? ""
|
||||
}.values.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !history.isEmpty {
|
||||
List {
|
||||
ForEach(groupedEntries(history), id: \.self) { (section: [History]) in
|
||||
Section(header: Text(formatter.string(from: section[0].date ?? Date()))) {
|
||||
ForEach(section, id: \.self) { history in
|
||||
ForEach(history.entryArray) { entry in
|
||||
HistoryButtonView(entry: entry)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
removeEntry(at: offsets, from: history)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
|
||||
func removeEntry(at offsets: IndexSet, from history: History) {
|
||||
for index in offsets {
|
||||
if let entry = history.entryArray[safe: index] {
|
||||
history.removeFromEntries(entry)
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
}
|
||||
|
||||
if history.entryArray.isEmpty {
|
||||
PersistenceController.shared.delete(history, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,11 @@ struct WebView: UIViewRepresentable {
|
|||
var url: URL
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let webView = WKWebView()
|
||||
// Make the WebView ephemeral
|
||||
let config = WKWebViewConfiguration()
|
||||
config.websiteDataStore = WKWebsiteDataStore.nonPersistent()
|
||||
|
||||
let webView = WKWebView(frame: .zero, configuration: config)
|
||||
let _ = webView.load(URLRequest(url: url))
|
||||
return webView
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
//
|
||||
// SearchResultButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultButtonView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
@State private var runOnce = false
|
||||
@State var existingBookmark: Bookmark? = nil
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if debridManager.currentDebridTask == nil {
|
||||
navModel.selectedSearchResult = result
|
||||
navModel.selectedTitle = result.title
|
||||
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: result)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
navModel.addToHistory(name: result.title, source: result.source, url: result.magnetLink)
|
||||
navModel.runMagnetAction(magnetString: result.magnetLink)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(result.title ?? "No title")
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(4)
|
||||
|
||||
SearchResultRDView(result: result)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
}
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
.backport.tint(.primary)
|
||||
.conditionalContextMenu(id: existingBookmark) {
|
||||
if let bookmark = existingBookmark {
|
||||
Button {
|
||||
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||
|
||||
// When the entity is deleted, let other instances know to remove that reference
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: nil)
|
||||
} label: {
|
||||
Text("Remove bookmark")
|
||||
Image(systemName: "bookmark.slash.fill")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
let newBookmark = Bookmark(context: backgroundContext)
|
||||
newBookmark.title = result.title
|
||||
newBookmark.source = result.source
|
||||
newBookmark.magnetHash = result.magnetHash
|
||||
newBookmark.magnetLink = result.magnetLink
|
||||
newBookmark.seeders = result.seeders
|
||||
newBookmark.leechers = result.leechers
|
||||
|
||||
existingBookmark = newBookmark
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
} label: {
|
||||
Text("Bookmark")
|
||||
Image(systemName: "bookmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $debridManager.showDeleteAlert,
|
||||
title: "Caching file",
|
||||
message: "RealDebrid is currently caching this file. Would you like to delete it? \n\nProgress can be checked on the RealDebrid website.",
|
||||
buttons: [
|
||||
AlertButton("Yes", role: .destructive) {
|
||||
Task {
|
||||
await debridManager.deleteRdTorrent()
|
||||
}
|
||||
},
|
||||
AlertButton(role: .cancel)
|
||||
]
|
||||
)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in
|
||||
existingBookmark = nil
|
||||
}
|
||||
.onAppear {
|
||||
// Only run a exists request if a bookmark isn't passed to the view
|
||||
if existingBookmark == nil, !runOnce {
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
bookmarkRequest.predicate = NSPredicate(
|
||||
format: "title == %@ AND source == %@ AND magnetLink == %@ AND magnetHash = %@",
|
||||
result.title ?? "",
|
||||
result.source,
|
||||
result.magnetLink ?? "",
|
||||
result.magnetHash ?? ""
|
||||
)
|
||||
bookmarkRequest.fetchLimit = 1
|
||||
|
||||
if let fetchedBookmark = try? backgroundContext.fetch(bookmarkRequest).first {
|
||||
existingBookmark = fetchedBookmark
|
||||
}
|
||||
|
||||
runOnce = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
//
|
||||
// SearchResultRDView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/26/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultRDView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(result.source)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let seeders = result.seeders {
|
||||
Text("S: \(seeders)")
|
||||
}
|
||||
|
||||
if let leechers = result.leechers {
|
||||
Text("L: \(leechers)")
|
||||
}
|
||||
|
||||
if let size = result.size {
|
||||
Text(size)
|
||||
}
|
||||
|
||||
if realDebridEnabled {
|
||||
Text("RD")
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background {
|
||||
Group {
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
Color.green
|
||||
case .partial:
|
||||
Color.orange
|
||||
case .none:
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import SwiftUI
|
|||
struct SearchResultsView: View {
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
// Created by Brian Dashore on 7/11/22.
|
||||
//
|
||||
|
||||
import BetterSafariView
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
|
||||
|
|
@ -14,6 +15,8 @@ struct SettingsView: View {
|
|||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
|
||||
|
||||
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
|
||||
|
|
@ -24,24 +27,61 @@ struct SettingsView: View {
|
|||
Form {
|
||||
Section(header: InlineHeader("Debrid Services")) {
|
||||
HStack {
|
||||
Text("Real Debrid")
|
||||
Text("RealDebrid")
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.realDebridEnabled {
|
||||
await debridManager.logoutRd()
|
||||
if debridManager.enabledDebrids.contains(.realDebrid) {
|
||||
await debridManager.logoutDebrid(debridType: .realDebrid)
|
||||
} else if !debridManager.realDebridAuthProcessing {
|
||||
await debridManager.authenticateRd()
|
||||
await debridManager.authenticateDebrid(debridType: .realDebrid)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.realDebridEnabled ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.realDebridEnabled ? .red : .blue)
|
||||
.onChange(of: debridManager.realDebridEnabled) { changed in
|
||||
print("Debrid enabled changed to \(changed)")
|
||||
}
|
||||
Text(debridManager.enabledDebrids.contains(.realDebrid) ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(.realDebrid) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("AllDebrid")
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.enabledDebrids.contains(.allDebrid) {
|
||||
await debridManager.logoutDebrid(debridType: .allDebrid)
|
||||
} else if !debridManager.allDebridAuthProcessing {
|
||||
await debridManager.authenticateDebrid(debridType: .allDebrid)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.enabledDebrids.contains(.allDebrid) ? "Logout" : (debridManager.allDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(.allDebrid) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Premiumize")
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.enabledDebrids.contains(.premiumize) {
|
||||
await debridManager.logoutDebrid(debridType: .premiumize)
|
||||
} else if !debridManager.premiumizeAuthProcessing {
|
||||
await debridManager.authenticateDebrid(debridType: .premiumize)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(debridManager.enabledDebrids.contains(.premiumize) ? "Logout" : (debridManager.premiumizeAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(.premiumize) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Behavior")) {
|
||||
Toggle(isOn: $autocorrectSearch) {
|
||||
Text("Autocorrect search")
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Source management")) {
|
||||
|
|
@ -49,7 +89,7 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
Section(header: Text("Default actions")) {
|
||||
if debridManager.realDebridEnabled {
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
NavigationLink(
|
||||
destination: DebridActionPickerView(),
|
||||
label: {
|
||||
|
|
@ -118,7 +158,18 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
.sheet(isPresented: $debridManager.showWebView) {
|
||||
LoginWebView(url: URL(string: debridManager.realDebridAuthUrl)!)
|
||||
LoginWebView(url: debridManager.authUrl ?? URL(string: "https://google.com")!)
|
||||
}
|
||||
.webAuthenticationSession(isPresented: $debridManager.showAuthSession) {
|
||||
WebAuthenticationSession(
|
||||
url: debridManager.authUrl ?? URL(string: "https://google.com")!,
|
||||
callbackURLScheme: "ferrite"
|
||||
) { callbackURL, error in
|
||||
Task {
|
||||
await debridManager.handleCallback(url: callbackURL, error: error)
|
||||
}
|
||||
}
|
||||
.prefersEphemeralWebBrowserSession(true)
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
|
|
|
|||
99
Ferrite/Views/SheetViews/BatchChoiceView.swift
Normal file
99
Ferrite/Views/SheetViews/BatchChoiceView.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// BatchChoiceView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BatchChoiceView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
switch debridManager.selectedDebridType {
|
||||
case .realDebrid:
|
||||
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.name) {
|
||||
debridManager.selectedRealDebridFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.name)
|
||||
}
|
||||
}
|
||||
case .allDebrid:
|
||||
ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.fileName) {
|
||||
debridManager.selectedAllDebridFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.fileName)
|
||||
}
|
||||
}
|
||||
case .premiumize:
|
||||
ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in
|
||||
Button(file.name) {
|
||||
debridManager.selectedPremiumizeFile = file
|
||||
|
||||
queueCommonDownload(fileName: file.name)
|
||||
}
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.navigationTitle("Select a file")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
navModel.currentChoiceSheet = nil
|
||||
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
|
||||
debridManager.clearSelectedDebridItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
navModel.selectedBatchTitle = fileName
|
||||
|
||||
if var selectedHistoryInfo = navModel.selectedHistoryInfo {
|
||||
selectedHistoryInfo.url = debridManager.downloadUrl
|
||||
selectedHistoryInfo.subName = fileName
|
||||
PersistenceController.shared.createHistory(selectedHistoryInfo)
|
||||
}
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
|
||||
debridManager.clearSelectedDebridItems()
|
||||
}
|
||||
|
||||
navModel.currentChoiceSheet = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct BatchChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BatchChoiceView()
|
||||
}
|
||||
}
|
||||
|
|
@ -25,34 +25,34 @@ struct MagnetChoiceView: View {
|
|||
Form {
|
||||
Section(header: "Now Playing") {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(navModel.selectedTitle ?? "No title")
|
||||
Text(navModel.selectedTitle)
|
||||
.font(.callout)
|
||||
.lineLimit(navModel.selectedBatchTitle == nil ? .max : 1)
|
||||
.lineLimit(navModel.selectedBatchTitle.isEmpty ? .max : 1)
|
||||
|
||||
if let batchTitle = navModel.selectedBatchTitle {
|
||||
Text(batchTitle)
|
||||
if !navModel.selectedBatchTitle.isEmpty {
|
||||
Text(navModel.selectedBatchTitle)
|
||||
.foregroundColor(.gray)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
Section(header: "Real Debrid options") {
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
Section(header: "Debrid options") {
|
||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .outplayer)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .vlc)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .vlc)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .infuse)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .infuse)
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = debridManager.realDebridDownloadUrl
|
||||
UIPasteboard.general.string = debridManager.downloadUrl
|
||||
showLinkCopyAlert.toggle()
|
||||
}
|
||||
.backport.alert(
|
||||
|
|
@ -63,7 +63,7 @@ struct MagnetChoiceView: View {
|
|||
)
|
||||
|
||||
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
|
||||
if let url = URL(string: debridManager.realDebridDownloadUrl) {
|
||||
if let url = URL(string: debridManager.downloadUrl) {
|
||||
navModel.activityItems = [url]
|
||||
navModel.showLocalActivitySheet.toggle()
|
||||
}
|
||||
|
|
@ -71,30 +71,31 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section(header: "Magnet options") {
|
||||
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink
|
||||
showMagnetCopyAlert.toggle()
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $showMagnetCopyAlert,
|
||||
title: "Copied",
|
||||
message: "Magnet link copied successfully",
|
||||
buttons: [AlertButton("OK")]
|
||||
)
|
||||
|
||||
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
|
||||
if let result = navModel.selectedSearchResult,
|
||||
let magnetLink = result.magnetLink,
|
||||
let url = URL(string: magnetLink)
|
||||
{
|
||||
navModel.activityItems = [url]
|
||||
navModel.showLocalActivitySheet.toggle()
|
||||
if !navModel.resultFromCloud {
|
||||
Section(header: "Magnet options") {
|
||||
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = navModel.selectedMagnet?.link
|
||||
showMagnetCopyAlert.toggle()
|
||||
}
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $showMagnetCopyAlert,
|
||||
title: "Copied",
|
||||
message: "Magnet link copied successfully",
|
||||
buttons: [AlertButton("OK")]
|
||||
)
|
||||
|
||||
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runMagnetAction(magnetString: navModel.selectedSearchResult?.magnetLink, .webtor)
|
||||
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
|
||||
if let magnetLink = navModel.selectedMagnet?.link,
|
||||
let url = URL(string: magnetLink)
|
||||
{
|
||||
navModel.activityItems = [url]
|
||||
navModel.showLocalActivitySheet.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runMagnetAction(magnet: navModel.selectedMagnet, .webtor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,14 +109,19 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
.onDisappear {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
debridManager.downloadUrl = ""
|
||||
navModel.selectedTitle = ""
|
||||
navModel.selectedBatchTitle = ""
|
||||
navModel.resultFromCloud = false
|
||||
}
|
||||
.navigationTitle("Link actions")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
debridManager.downloadUrl = ""
|
||||
navModel.selectedTitle = ""
|
||||
navModel.selectedBatchTitle = ""
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
|
|
@ -15,13 +15,11 @@ struct SourcesView: View {
|
|||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<Source>
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@State private var checkedForSources = false
|
||||
@State private var isEditing = false
|
||||
@State private var isEditingSearch = false
|
||||
@State private var isSearching = false
|
||||
|
||||
@State private var viewTask: Task<Void, Never>? = nil
|
||||
@State private var searchText: String = ""
|
||||
|
|
@ -35,7 +33,7 @@ struct SourcesView: View {
|
|||
ZStack {
|
||||
if !checkedForSources {
|
||||
ProgressView()
|
||||
} else if sources.isEmpty, sourceManager.availableSources.isEmpty {
|
||||
} else if installedSources.isEmpty, sourceManager.availableSources.isEmpty {
|
||||
EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings")
|
||||
} else {
|
||||
List {
|
||||
|
|
@ -119,11 +117,18 @@ struct SourcesView: View {
|
|||
}
|
||||
.navigationTitle("Sources")
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditing)
|
||||
.showsCancelButton(isEditing)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
}
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: {
|
||||
isSearching = true
|
||||
})
|
||||
.showsCancelButton(isEditingSearch || isSearching)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
.introspectSearchController { searchController in
|
||||
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
|
||||
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -14,27 +14,39 @@ Finding shows and movies is usually pretty easy because there are many websites
|
|||
|
||||
However, the main problem is that these websites tend to suck in terms of UI or finding media to watch. Ferrite aims to provide a better UI to search and find the media you want.
|
||||
|
||||
I also wanted to support the use of RealDebrid since there aren't any (free) options on iOS that have support for this service.
|
||||
I also wanted to support the use of debrid services since there aren't any (free) options on iOS that have support for this service.
|
||||
|
||||
## What iOS versions are supported?
|
||||
|
||||
iOS 14 and up. I was able to successfully backport the app!
|
||||
- v0.7 and up: iOS 15 and up
|
||||
|
||||
- v0.6.x and lower: iOS 14 and up
|
||||
|
||||
## Planned features
|
||||
|
||||
- Website API support in sources: This allows for website APIs to be used in Ferrite sources which is quicker than scraping or RSS parsing
|
||||
More of these can be found in [issues](https://github.com/bdashore3/Ferrite/issues), but here is a small snippet:
|
||||
|
||||
- A decentralized actions API: Allows for playback on other devices
|
||||
|
||||
- More involved search filtering
|
||||
|
||||
- Companion apps for playback on other devices
|
||||
|
||||
## Downloads
|
||||
|
||||
Ferrite will only exist as an ipa. There are and will never be any plans to release on TestFlight or the App Store. Ipa builds are automatically built and are provided in Github actions artifacts.
|
||||
|
||||
## Plugins/Sources
|
||||
|
||||
Sources are not provided by the application. They must be found from external means or you can make them yourself using the [wiki](https://github.com/bdashore3/Ferrite/wiki). Various communities have created sources for Ferrite and they can be imported in the app with ease.
|
||||
|
||||
## Building from source
|
||||
|
||||
Xcode 14 must be used since Ferrite requires some iOS 16 APIs that are not present in Xcode 13. Please make sure you have the right Xcode or download the beta xip from Apple's developer website.
|
||||
Xcode 14 must be used.
|
||||
|
||||
There is currently one branch in the repository:
|
||||
There are currently two branches in the repository:
|
||||
|
||||
- default: The current working branch. This will change in the future once a stable version is released.
|
||||
- default: A snapshot of the latest stable build. Tags can also be used for older versions.
|
||||
- next: The development branch. Nightlies are automatically built here.
|
||||
|
||||
## Nightly builds
|
||||
|
|
|
|||
Loading…
Reference in a new issue