Compare commits

...

20 commits

Author SHA1 Message Date
kingbri
1ce37f9a49 Actions: Update to latest
Bump actions and macos build versions.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-09 20:53:10 -04:00
kingbri
ea4a4350ba Debrid: Fix UI updates for IA
Hook to the published variable to push updates.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-09 20:41:08 -04:00
kingbri
26b9bdd702 Premiumize: Fix service-specific errors
This parameter should be optional and errors if it isn't.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-09 20:28:02 -04:00
kingbri
9b936778b0 Logging: Improve generic error message
Point the user to settings logs rather than giving no extra information.
It would be a good idea to give the type of error in the future.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-08 12:28:19 -04:00
kingbri
75291c4396 Tree: Format
Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-08 12:27:36 -04:00
kingbri
2c25bd98db Debrid: Migrate auth to protocol
Unify authentication to the new protocol. Also remove logout on
invalid requests. This became annoying and didn't update the UI
properly.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-08 01:09:18 -04:00
kingbri
447d8b5bd0 Debrid: Unify cloud views
Cloud torrents and downloads are unified with the new protocol.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-06 20:39:09 -04:00
kingbri
f9b2959ae2 Debrid: Remove more redundant vars
the IA vars are no longer needed since that's unified.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-06 11:54:31 -04:00
kingbri
01fce90d6f Debrid: Migrate preferred service setter
PreferredService is now the debrid ID.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-06 11:53:56 -04:00
kingbri
cb9231d3e7 Debrid: Remove separated download functions
No longer needed due to the common type.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-06 10:48:23 -04:00
kingbri
9fe9241ca3 Debrid: Remove redundant logout functions
Logout is now handled in the debrid class itself.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-06 10:46:18 -04:00
kingbri
554b72857b Debrid: Fix cache alert
Change the returned error to one that's unique to caching. Also
make deleteTorrents optional to delete the first torrent if necessary
since that's always being cached.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-06 10:44:44 -04:00
kingbri
3cb8a979b1 Debrid: Swap to common DebridError
Removes the redundant error types.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-05 22:50:40 -04:00
kingbri
13988b3c6c Debrid: Refactor IA and download functions
Use the common protocol to handle these.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-05 22:16:40 -04:00
kingbri
4e6cfee608 Debrid: Remove ID storage
Storing an ID reference is redundant. Store a class reference
instead.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-05 13:05:26 -04:00
kingbri
46e66ab457 Debrid: Migrate more components to the protocol
Protocols can't be used in ObservedObjects. Observable in iOS 17
and up solves this, but Ferrite targets iOS 16 and up, so add a
type-erased StateObject which supports protocols.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-05 12:33:11 -04:00
kingbri
449b0eaa7e Debrid: Allow for UI updates
Mark as an ObservableObject so the UI can see parameters that are
being updated in the class.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-04 14:02:59 -04:00
kingbri
3137d20656 Debrid: Migrate common arrays to their API classes
Add convenience vars which makes the API classes the source of truth
for any interaction.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-04 12:09:19 -04:00
kingbri
1d8a965bb7 Debrid: Fix RealDebrid download handling
The torrent ID is no longer stored in the DebridManager.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-04 10:52:35 -04:00
kingbri
853b956105 Debrid: Add common functions for existing torrents/downloads
This fixes cloud torrent fetching and also doesn't duplicate torrents
inside the cloud service. Unrestricted links don't get duplicated,
so no need to check against those.

Signed-off-by: kingbri <bdashore3@proton.me>
2024-06-03 23:56:56 -04:00
32 changed files with 885 additions and 1217 deletions

View file

@ -6,13 +6,13 @@ on:
jobs:
build:
runs-on: macos-12
runs-on: macos-14
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
xcode-version: latest-stable
- name: Get commit SHA
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build
@ -25,7 +25,7 @@ jobs:
cp -r build/Ferrite.xcarchive/Products/Applications/Ferrite.app Payload
zip -r Ferrite-iOS_nightly-${{ env.sha_short }}.ipa Payload
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa
path: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa

View file

@ -7,13 +7,13 @@ on:
jobs:
build:
runs-on: macos-12
runs-on: macos-14
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
xcode-version: latest-stable
- 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:
@ -30,7 +30,7 @@ jobs:
run: |
zip -j Ferrite-iOS_v${{ env.app_version }}.ipa.zip Ferrite-iOS_v${{ env.app_version }}.ipa
- name: Upload release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: Ferrite-iOS_v${{ env.app_version }}.ipa.zip

View file

@ -20,7 +20,6 @@
0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */; };
0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */; };
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
0C2B028F29E9E61E00DCF127 /* SortFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */; };
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; };
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
@ -54,7 +53,6 @@
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
0C5708EB29B8F89300BE07F9 /* SettingsLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */; };
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */; };
@ -96,6 +94,7 @@
0C84FCE729E4B61A00B0DFE4 /* FilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */; };
0C84FCE929E5ADEF00B0DFE4 /* FilterAmountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */; };
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.swift */; };
0C8AE2482C0FFB6600701675 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8AE2472C0FFB6600701675 /* Store.swift */; };
0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */; };
0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; };
0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */; };
@ -129,12 +128,13 @@
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; };
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; };
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; };
0CB0115B29D36D9E009AFEDE /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */; };
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0AB5E29BD2A200015422C /* KodiServerView.swift */; };
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; };
0CB725322C123E6F0047FC0B /* CloudDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */; };
0CB725342C123E760047FC0B /* CloudTorrentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB725332C123E760047FC0B /* CloudTorrentView.swift */; };
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; };
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
@ -173,7 +173,6 @@
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableWrapper.swift; sourceTree = "<group>"; };
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; };
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = "<group>"; };
0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortFilterView.swift; sourceTree = "<group>"; };
0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
@ -206,7 +205,6 @@
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLogView.swift; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiWrapper.swift; sourceTree = "<group>"; };
0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKodiView.swift; sourceTree = "<group>"; };
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiModels.swift; sourceTree = "<group>"; };
@ -244,6 +242,7 @@
0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterModels.swift; sourceTree = "<group>"; };
0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterAmountLabelView.swift; sourceTree = "<group>"; };
0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = "<group>"; };
0C8AE2472C0FFB6600701675 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = "<group>"; };
0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = "<group>"; };
0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = "<group>"; };
@ -277,12 +276,13 @@
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = "<group>"; };
0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
0CAF9318296399190050812A /* PremiumizeCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeCloudView.swift; sourceTree = "<group>"; };
0CB0115A29D36D9E009AFEDE /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
0CB0AB5E29BD2A200015422C /* KodiServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiServerView.swift; sourceTree = "<group>"; };
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudDownloadView.swift; sourceTree = "<group>"; };
0CB725332C123E760047FC0B /* CloudTorrentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudTorrentView.swift; sourceTree = "<group>"; };
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableInteraction.swift; sourceTree = "<group>"; };
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
@ -411,9 +411,8 @@
0C2886D52960C4F800D6FC16 /* Cloud */ = {
isa = PBXGroup;
children = (
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */,
0CAF9318296399190050812A /* PremiumizeCloudView.swift */,
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */,
0CB725312C123E6F0047FC0B /* CloudDownloadView.swift */,
0CB725332C123E760047FC0B /* CloudTorrentView.swift */,
);
path = Cloud;
sourceTree = "<group>";
@ -457,6 +456,7 @@
0C44E2A728D4DDDC007711AE /* Application.swift */,
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */,
0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */,
0C8AE2472C0FFB6600701675 /* Store.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -853,11 +853,11 @@
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */,
0CF2C0A529D1EBD400E716DD /* UIApplication.swift in Sources */,
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */,
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */,
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
0C8AE2482C0FFB6600701675 /* Store.swift in Sources */,
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */,
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
@ -871,10 +871,10 @@
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */,
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */,
0C84FCE529E4B43200B0DFE4 /* SelectedDebridFilterView.swift in Sources */,
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
0CB725342C123E760047FC0B /* CloudTorrentView.swift in Sources */,
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */,
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
@ -901,7 +901,6 @@
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */,
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
@ -955,6 +954,7 @@
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */,
0CB725322C123E6F0047FC0B /* CloudDownloadView.swift in Sources */,
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */,
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,

View file

@ -8,12 +8,30 @@
import Foundation
// TODO: Fix errors
public class AllDebrid: PollingDebridSource {
public class AllDebrid: PollingDebridSource, ObservableObject {
public let id = "AllDebrid"
public let abbreviation = "AD"
public let website = "https://alldebrid.com"
public var authTask: Task<Void, Error>?
public var authProcessing: Bool = false
public var isLoggedIn: Bool {
getToken() != nil
}
public var manualToken: String? {
if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") {
return getToken()
} else {
return nil
}
}
@Published public var IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = []
public var cloudTTL: Double = 0.0
let baseApiUrl = "https://api.alldebrid.com/v4"
let appName = "Ferrite"
@ -32,7 +50,7 @@ public class AllDebrid: PollingDebridSource {
// Validate the URL before doing anything else
let rawResponse = try jsonDecoder.decode(ADResponse<PinResponse>.self, from: data).data
guard let userUrl = URL(string: rawResponse.userURL) else {
throw ADError.AuthQuery(description: "The login URL is invalid")
throw DebridError.AuthQuery(description: "The login URL is invalid")
}
// Spawn the polling task separately
@ -43,7 +61,7 @@ public class AllDebrid: PollingDebridSource {
return userUrl
} catch {
print("Couldn't get pin information!")
throw ADError.AuthQuery(description: error.localizedDescription)
throw DebridError.AuthQuery(description: error.localizedDescription)
}
}
@ -63,7 +81,7 @@ public class AllDebrid: PollingDebridSource {
while count < 12 {
if Task.isCancelled {
throw ADError.AuthQuery(description: "Token request cancelled.")
throw DebridError.AuthQuery(description: "Token request cancelled.")
}
let (data, _) = try await URLSession.shared.data(for: request)
@ -82,7 +100,7 @@ public class AllDebrid: PollingDebridSource {
}
}
throw ADError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
throw DebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
}
if case let .failure(error) = await authTask?.result {
@ -91,11 +109,9 @@ public class AllDebrid: PollingDebridSource {
}
// Adds a manual API key instead of web auth
public func setApiKey(_ key: String) -> Bool {
public func setApiKey(_ key: String) {
FerriteKeychain.shared.set(key, forKey: "AllDebrid.ApiKey")
UserDefaults.standard.set(true, forKey: "AllDebrid.UseManualKey")
return FerriteKeychain.shared.get("AllDebrid.ApiKey") == key
}
public func getToken() -> String? {
@ -113,7 +129,7 @@ public class AllDebrid: PollingDebridSource {
// Wrapper request function which matches the responses and returns data
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
guard let token = getToken() else {
throw ADError.InvalidToken
throw DebridError.InvalidToken
}
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
@ -121,23 +137,22 @@ public class AllDebrid: PollingDebridSource {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw ADError.FailedRequest(description: "No HTTP response given")
throw DebridError.FailedRequest(description: "No HTTP response given")
}
if response.statusCode >= 200, response.statusCode <= 299 {
return data
} else if response.statusCode == 401 {
logout()
throw ADError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.")
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to AllDebrid in Settings.")
} else {
throw ADError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
}
}
// Builds a URL for further requests
private func buildRequestURL(urlString: String, queryItems: [URLQueryItem] = []) throws -> URL {
guard var components = URLComponents(string: urlString) else {
throw ADError.InvalidUrl
throw DebridError.InvalidUrl
}
components.queryItems = [
@ -147,14 +162,33 @@ public class AllDebrid: PollingDebridSource {
if let url = components.url {
return url
} else {
throw ADError.InvalidUrl
throw DebridError.InvalidUrl
}
}
// MARK: - Instant availability
public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] {
let queryItems = magnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
public func instantAvailability(magnets: [Magnet]) async throws {
let now = Date().timeIntervalSince1970
let sendMagnets = magnets.filter { magnet in
if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) {
if now > IAValues[IAIndex].expiryTimeStamp {
IAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else {
return true
}
}
if sendMagnets.isEmpty {
return
}
let queryItems = sendMagnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems))
let data = try await performRequest(request: &request, requestName: #function)
@ -175,16 +209,24 @@ public class AllDebrid: PollingDebridSource {
)
}
return availableHashes
IAValues += availableHashes
}
// MARK: - Downloading
// Wrapper function to fetch a download link from the API
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
let magnetID = try await addMagnet(magnet: magnet)
let selectedMagnetId: String
if let existingMagnet = cloudTorrents.first(where: { $0.hash == magnet.hash && $0.status == "Ready" }) {
selectedMagnetId = existingMagnet.torrentId
} else {
let magnetId = try await addMagnet(magnet: magnet)
selectedMagnetId = String(magnetId)
}
let lockedLink = try await fetchMagnetStatus(
magnetId: magnetID,
magnetId: selectedMagnetId,
selectedIndex: iaFile?.fileId ?? 0
)
@ -197,7 +239,7 @@ public class AllDebrid: PollingDebridSource {
// Adds a magnet link to the user's AD account
public func addMagnet(magnet: Magnet) async throws -> Int {
guard let magnetLink = magnet.link else {
throw ADError.FailedRequest(description: "The magnet link is invalid")
throw DebridError.FailedRequest(description: "The magnet link is invalid")
}
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload"))
@ -217,13 +259,13 @@ public class AllDebrid: PollingDebridSource {
if let magnet = rawResponse.magnets[safe: 0] {
return magnet.id
} else {
throw ADError.InvalidResponse
throw DebridError.InvalidResponse
}
}
public func fetchMagnetStatus(magnetId: Int, selectedIndex: Int?) async throws -> String {
public func fetchMagnetStatus(magnetId: String, selectedIndex: Int?) async throws -> String {
let queryItems = [
URLQueryItem(name: "id", value: String(magnetId))
URLQueryItem(name: "id", value: magnetId)
]
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems))
@ -234,7 +276,7 @@ public class AllDebrid: PollingDebridSource {
if let linkWrapper = rawResponse.magnets[safe: 0]?.links[safe: selectedIndex ?? -1] {
return linkWrapper.link
} else {
throw ADError.EmptyTorrents
throw DebridError.EmptyTorrents
}
}
@ -262,17 +304,17 @@ public class AllDebrid: PollingDebridSource {
// MARK: - Cloud methods
// Referred to as "User magnets" in AllDebrid's API
public func getUserTorrents() async throws -> [DebridCloudTorrent] {
public func getUserTorrents() async throws {
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status"))
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
if rawResponse.magnets.isEmpty {
throw ADError.EmptyData
throw DebridError.EmptyData
}
let torrents = rawResponse.magnets.map { magnetResponse in
cloudTorrents = rawResponse.magnets.map { magnetResponse in
DebridCloudTorrent(
torrentId: String(magnetResponse.id),
source: self.id,
@ -282,11 +324,13 @@ public class AllDebrid: PollingDebridSource {
links: magnetResponse.links.map(\.link)
)
}
return torrents
}
public func deleteTorrent(torrentId: String) async throws {
public func deleteTorrent(torrentId: String?) async throws {
guard let torrentId else {
throw DebridError.FailedRequest(description: "The torrentID \(String(describing: torrentId)) is invalid")
}
let queryItems = [
URLQueryItem(name: "id", value: torrentId)
]
@ -295,24 +339,27 @@ public class AllDebrid: PollingDebridSource {
try await performRequest(request: &request, requestName: #function)
}
public func getUserDownloads() async throws -> [DebridCloudDownload] {
public func getUserDownloads() async throws {
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links"))
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(ADResponse<SavedLinksResponse>.self, from: data).data
if rawResponse.links.isEmpty {
throw ADError.EmptyData
throw DebridError.EmptyData
}
// The link is also the ID
let downloads = rawResponse.links.map { link in
cloudDownloads = rawResponse.links.map { link in
DebridCloudDownload(
downloadId: link.link, source: self.id, fileName: link.filename, link: link.link
)
}
}
return downloads
// Not used
public func checkUserDownloads(link: String) async throws -> String? {
nil
}
// The downloadId is actually the download link

View file

@ -7,10 +7,27 @@
import Foundation
public class Premiumize: OAuthDebridSource {
public class Premiumize: OAuthDebridSource, ObservableObject {
public let id = "Premiumize"
public let abbreviation = "PM"
public let website = "https://premiumize.me"
@Published public var authProcessing: Bool = false
public var isLoggedIn: Bool {
getToken() != nil
}
public var manualToken: String? {
if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") {
return getToken()
} else {
return nil
}
}
@Published public var IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = []
public var cloudTTL: Double = 0.0
let baseAuthUrl = "https://www.premiumize.me/authorize"
let baseApiUrl = "https://www.premiumize.me/api"
@ -31,7 +48,7 @@ public class Premiumize: OAuthDebridSource {
if let url = urlComponents.url {
return url
} else {
throw PMError.InvalidUrl
throw DebridError.InvalidUrl
}
}
@ -39,25 +56,23 @@ public class Premiumize: OAuthDebridSource {
let callbackComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let callbackFragment = callbackComponents?.fragment else {
throw PMError.InvalidResponse
throw DebridError.InvalidResponse
}
var fragmentComponents = URLComponents()
fragmentComponents.query = callbackFragment
guard let accessToken = fragmentComponents.queryItems?.first(where: { $0.name == "access_token" })?.value else {
throw PMError.InvalidToken
throw DebridError.InvalidToken
}
FerriteKeychain.shared.set(accessToken, forKey: "Premiumize.AccessToken")
}
// Adds a manual API key instead of web auth
public func setApiKey(_ key: String) -> Bool {
public func setApiKey(_ key: String) {
FerriteKeychain.shared.set(key, forKey: "Premiumize.AccessToken")
UserDefaults.standard.set(true, forKey: "Premiumize.UseManualKey")
return FerriteKeychain.shared.get("Premiumize.AccessToken") == key
}
public func getToken() -> String? {
@ -75,7 +90,7 @@ public class Premiumize: OAuthDebridSource {
// Wrapper request function which matches the responses and returns data
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
guard let token = getToken() else {
throw PMError.InvalidToken
throw DebridError.InvalidToken
}
// Use the API query parameter if a manual API key is present
@ -84,7 +99,7 @@ public class Premiumize: OAuthDebridSource {
let requestUrl = request.url,
var components = URLComponents(url: requestUrl, resolvingAgainstBaseURL: false)
else {
throw PMError.InvalidUrl
throw DebridError.InvalidUrl
}
let apiTokenItem = URLQueryItem(name: "apikey", value: token)
@ -103,42 +118,52 @@ public class Premiumize: OAuthDebridSource {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw PMError.FailedRequest(description: "No HTTP response given")
throw DebridError.FailedRequest(description: "No HTTP response given")
}
if response.statusCode >= 200, response.statusCode <= 299 {
return data
} else if response.statusCode == 401 {
logout()
throw PMError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to Premiumize in Settings.")
} else {
throw PMError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
}
}
// MARK: - Instant availability
public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] {
var collectedIA: [DebridIA] = []
public func instantAvailability(magnets: [Magnet]) async throws {
let now = Date().timeIntervalSince1970
// Only strip magnets that don't have an associated link for PM
let strippedMagnets: [Magnet] = magnets.compactMap {
if let magnetLink = $0.link {
return Magnet(hash: $0.hash, link: magnetLink)
// Remove magnets that don't have an associated link for PM along with existing TTL logic
let sendMagnets = magnets.filter { magnet in
if magnet.link == nil {
return false
}
if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) {
if now > IAValues[IAIndex].expiryTimeStamp {
IAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else {
return nil
return true
}
}
let availableMagnets = try await divideCacheRequests(magnets: strippedMagnets)
if sendMagnets.isEmpty {
return
}
let availableMagnets = try await divideCacheRequests(magnets: sendMagnets)
// Split DDL requests into chunks of 10
for chunk in availableMagnets.chunked(into: 10) {
let tempIA = try await divideDDLRequests(magnetChunk: chunk)
collectedIA += tempIA
IAValues += tempIA
}
return collectedIA
}
// Function to divide and execute DDL endpoint requests in parallel
@ -164,7 +189,7 @@ public class Premiumize: OAuthDebridSource {
// Grabs DDL links
func fetchDDL(magnet: Magnet) async throws -> DebridIA {
if magnet.hash == nil {
throw PMError.EmptyData
throw DebridError.EmptyData
}
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/directdl")!)
@ -178,9 +203,10 @@ public class Premiumize: OAuthDebridSource {
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(DDLResponse.self, from: data)
let content = rawResponse.content ?? []
if !rawResponse.content.isEmpty {
let files = rawResponse.content.map { file in
if !content.isEmpty {
let files = content.map { file in
DebridIAFile(
fileId: 0,
name: file.path.split(separator: "/").last.flatMap { String($0) } ?? file.path,
@ -195,7 +221,7 @@ public class Premiumize: OAuthDebridSource {
files: files
)
} else {
throw PMError.EmptyData
throw DebridError.EmptyData
}
}
@ -225,7 +251,7 @@ public class Premiumize: OAuthDebridSource {
var urlComponents = URLComponents(string: "\(baseApiUrl)/cache/check")!
urlComponents.queryItems = magnets.map { URLQueryItem(name: "items[]", value: $0.hash) }
guard let url = urlComponents.url else {
throw PMError.InvalidUrl
throw DebridError.InvalidUrl
}
var request = URLRequest(url: url)
@ -234,7 +260,7 @@ public class Premiumize: OAuthDebridSource {
let rawResponse = try jsonDecoder.decode(CacheCheckResponse.self, from: data)
if rawResponse.response.isEmpty {
throw PMError.EmptyData
throw DebridError.EmptyData
} else {
let availableMagnets = magnets.enumerated().compactMap { index, magnet in
if rawResponse.response[safe: index] == true {
@ -260,13 +286,13 @@ public class Premiumize: OAuthDebridSource {
} else if let premiumizeItem = ia, let firstFile = premiumizeItem.files[safe: 0], let streamUrlString = firstFile.streamUrlString {
return streamUrlString
} else {
throw PMError.FailedRequest(description: "Could not fetch your file from the Premiumize API")
throw DebridError.FailedRequest(description: "Could not fetch your file from the Premiumize API")
}
}
func createTransfer(magnet: Magnet) async throws {
guard let magnetLink = magnet.link else {
throw PMError.FailedRequest(description: "The magnet link is invalid")
throw DebridError.FailedRequest(description: "The magnet link is invalid")
}
var request = URLRequest(url: URL(string: "\(baseApiUrl)/transfer/create")!)
@ -283,29 +309,27 @@ public class Premiumize: OAuthDebridSource {
// MARK: - Cloud methods
public func getUserDownloads() async throws -> [DebridCloudDownload] {
public func getUserDownloads() async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/listall")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(AllItemsResponse.self, from: data)
if rawResponse.files.isEmpty {
throw PMError.EmptyData
throw DebridError.EmptyData
}
// The "link" is the ID for Premiumize
let downloads = rawResponse.files.map { file in
cloudDownloads = rawResponse.files.map { file in
DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id)
}
return downloads
}
func itemDetails(itemID: String) async throws -> ItemDetailsResponse {
var urlComponents = URLComponents(string: "\(baseApiUrl)/item/details")!
urlComponents.queryItems = [URLQueryItem(name: "id", value: itemID)]
guard let url = urlComponents.url else {
throw PMError.InvalidUrl
throw DebridError.InvalidUrl
}
var request = URLRequest(url: url)
@ -316,6 +340,11 @@ public class Premiumize: OAuthDebridSource {
return rawResponse
}
public func checkUserDownloads(link: String) async throws -> String? {
// Link is the cloud item ID
try await itemDetails(itemID: link).link
}
public func deleteDownload(downloadId: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/item/delete")!)
request.httpMethod = "POST"
@ -330,9 +359,7 @@ public class Premiumize: OAuthDebridSource {
}
// No user torrents for Premiumize
public func getUserTorrents() async throws -> [DebridCloudTorrent] {
[]
}
public func getUserTorrents() async throws {}
public func deleteTorrent(torrentId: String) async throws {}
public func deleteTorrent(torrentId: String?) async throws {}
}

View file

@ -7,12 +7,32 @@
import Foundation
public class RealDebrid: PollingDebridSource {
public class RealDebrid: PollingDebridSource, ObservableObject {
public let id = "RealDebrid"
public let abbreviation = "RD"
public let website = "https://real-debrid.com"
public var authTask: Task<Void, Error>?
@Published public var authProcessing: Bool = false
// Check the manual token since getTokens() is async
public var isLoggedIn: Bool {
FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil
}
public var manualToken: String? {
if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") {
return FerriteKeychain.shared.get("RealDebrid.AccessToken")
} else {
return nil
}
}
@Published public var IAValues: [DebridIA] = []
@Published public var cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = []
public var cloudTTL: Double = 0.0
let baseAuthUrl = "https://api.real-debrid.com/oauth/v2"
let baseApiUrl = "https://api.real-debrid.com/rest/1.0"
let openSourceClientId = "X245A4XAIBGVM"
@ -40,7 +60,7 @@ public class RealDebrid: PollingDebridSource {
]
guard let url = urlComponents.url else {
throw RDError.InvalidUrl
throw DebridError.InvalidUrl
}
let request = URLRequest(url: url)
@ -50,7 +70,7 @@ public class RealDebrid: PollingDebridSource {
// Validate the URL before doing anything else
let rawResponse = try jsonDecoder.decode(DeviceCodeResponse.self, from: data)
guard let directVerificationUrl = URL(string: rawResponse.directVerificationURL) else {
throw RDError.AuthQuery(description: "The verification URL is invalid")
throw DebridError.AuthQuery(description: "The verification URL is invalid")
}
// Spawn the polling task separately
@ -61,7 +81,7 @@ public class RealDebrid: PollingDebridSource {
return directVerificationUrl
} catch {
print("Couldn't get the new client creds!")
throw RDError.AuthQuery(description: error.localizedDescription)
throw DebridError.AuthQuery(description: error.localizedDescription)
}
}
@ -74,7 +94,7 @@ public class RealDebrid: PollingDebridSource {
]
guard let url = urlComponents.url else {
throw RDError.InvalidUrl
throw DebridError.InvalidUrl
}
let request = URLRequest(url: url)
@ -84,7 +104,7 @@ public class RealDebrid: PollingDebridSource {
while count < 12 {
if Task.isCancelled {
throw RDError.AuthQuery(description: "Token request cancelled.")
throw DebridError.AuthQuery(description: "Token request cancelled.")
}
let (data, _) = try await URLSession.shared.data(for: request)
@ -97,7 +117,7 @@ public class RealDebrid: PollingDebridSource {
await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId")
FerriteKeychain.shared.set(clientSecret, forKey: "RealDebrid.ClientSecret")
try await getTokens(deviceCode: deviceCode)
try await getApiTokens(deviceCode: deviceCode)
return
} else {
@ -106,17 +126,17 @@ public class RealDebrid: PollingDebridSource {
}
}
throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
throw DebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
}
// Fetch all tokens for the user and store in FerriteKeychain.shared
public func getTokens(deviceCode: String) async throws {
public func getApiTokens(deviceCode: String) async throws {
guard let clientId = UserDefaults.standard.string(forKey: "RealDebrid.ClientId") else {
throw RDError.EmptyData
throw DebridError.EmptyData
}
guard let clientSecret = FerriteKeychain.shared.get("RealDebrid.ClientSecret") else {
throw RDError.EmptyData
throw DebridError.EmptyData
}
var request = URLRequest(url: URL(string: "\(baseAuthUrl)/token")!)
@ -144,13 +164,13 @@ public class RealDebrid: PollingDebridSource {
await setUserDefaultsValue(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp")
}
public func fetchToken() async -> String? {
public func getToken() async -> String? {
let accessTokenStamp = UserDefaults.standard.double(forKey: "RealDebrid.AccessTokenStamp")
if Date().timeIntervalSince1970 > accessTokenStamp {
do {
if let refreshToken = FerriteKeychain.shared.get("RealDebrid.RefreshToken") {
try await getTokens(deviceCode: refreshToken)
try await getApiTokens(deviceCode: refreshToken)
}
} catch {
print(error)
@ -163,14 +183,12 @@ public class RealDebrid: PollingDebridSource {
// Adds a manual API key instead of web auth
// Clear out existing refresh tokens and timestamps
public func setApiKey(_ key: String) -> Bool {
public func setApiKey(_ key: String) {
FerriteKeychain.shared.set(key, forKey: "RealDebrid.AccessToken")
FerriteKeychain.shared.delete("RealDebrid.RefreshToken")
FerriteKeychain.shared.delete("RealDebrid.AccessTokenStamp")
UserDefaults.standard.set(true, forKey: "RealDebrid.UseManualKey")
return FerriteKeychain.shared.get("RealDebrid.AccessToken") == key
}
// Deletes tokens from device and RD's servers
@ -195,8 +213,8 @@ public class RealDebrid: PollingDebridSource {
// Wrapper request function which matches the responses and returns data
@discardableResult private func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
guard let token = await fetchToken() else {
throw RDError.InvalidToken
guard let token = await getToken() else {
throw DebridError.InvalidToken
}
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
@ -204,25 +222,42 @@ public class RealDebrid: PollingDebridSource {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw RDError.FailedRequest(description: "No HTTP response given")
throw DebridError.FailedRequest(description: "No HTTP response given")
}
if response.statusCode >= 200, response.statusCode <= 299 {
return data
} else if response.statusCode == 401 {
await logout()
throw RDError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
throw DebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.")
} else {
throw RDError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
throw DebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).")
}
}
// MARK: - Instant availability
// Checks if the magnet is streamable on RD
public func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA] {
var availableHashes: [DebridIA] = []
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnets.compactMap(\.hash).joined(separator: "/"))")!)
public func instantAvailability(magnets: [Magnet]) async throws {
let now = Date().timeIntervalSince1970
let sendMagnets = magnets.filter { magnet in
if let IAIndex = IAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }) {
if now > IAValues[IAIndex].expiryTimeStamp {
IAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else {
return true
}
}
if sendMagnets.isEmpty {
return
}
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(sendMagnets.compactMap(\.hash).joined(separator: "/"))")!)
let data = try await performRequest(request: &request, requestName: #function)
@ -269,7 +304,7 @@ public class RealDebrid: PollingDebridSource {
}
// TTL: 5 minutes
availableHashes.append(
IAValues.append(
DebridIA(
magnet: Magnet(hash: hash, link: nil),
source: id,
@ -278,7 +313,7 @@ public class RealDebrid: PollingDebridSource {
)
)
} else {
availableHashes.append(
IAValues.append(
DebridIA(
magnet: Magnet(hash: hash, link: nil),
source: id,
@ -288,31 +323,46 @@ public class RealDebrid: PollingDebridSource {
)
}
}
return availableHashes
}
// MARK: - Downloading
// Wrapper function to fetch a download link from the API
public func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String {
let selectedMagnetId = try await addMagnet(magnet: magnet)
var selectedMagnetId = ""
try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? [])
do {
// Don't queue a new job if the torrent already exists
if let existingTorrent = cloudTorrents.first(where: { $0.hash == magnet.hash && $0.status == "downloaded" }) {
selectedMagnetId = existingTorrent.torrentId
} else {
selectedMagnetId = try await addMagnet(magnet: magnet)
let torrentLink = try await torrentInfo(
debridID: selectedMagnetId,
selectedIndex: iaFile?.fileId ?? 0
)
let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink)
try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? [])
}
return downloadLink
// RealDebrid has 1 as the first ID for a file
let torrentLink = try await torrentInfo(
debridID: selectedMagnetId,
selectedFileId: iaFile?.fileId ?? 1
)
let downloadLink = try await unrestrictLink(debridDownloadLink: torrentLink)
return downloadLink
} catch {
if case DebridError.EmptyTorrents = error, !selectedMagnetId.isEmpty {
try? await deleteTorrent(torrentId: selectedMagnetId)
}
// Re-raise the error to the calling function
throw error
}
}
// Adds a magnet link to the user's RD account
public func addMagnet(magnet: Magnet) async throws -> String {
guard let magnetLink = magnet.link else {
throw RDError.FailedRequest(description: "The magnet link is invalid")
throw DebridError.FailedRequest(description: "The magnet link is invalid")
}
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/addMagnet")!)
@ -351,21 +401,21 @@ public class RealDebrid: PollingDebridSource {
}
// Gets the info of a torrent from a given ID
public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String {
public func torrentInfo(debridID: String, selectedFileId: Int?) async throws -> String {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
let filteredFiles = rawResponse.files.filter { $0.selected == 1 }
let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedIndex })
let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedFileId })
// Let the user know if a torrent is downloading
if let torrentLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" {
return torrentLink
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
throw RDError.EmptyTorrents
throw DebridError.IsCaching
} else {
throw RDError.EmptyData
throw DebridError.EmptyTorrents
}
}
@ -389,12 +439,12 @@ public class RealDebrid: PollingDebridSource {
// MARK: - Cloud methods
// Gets the user's torrent library
public func getUserTorrents() async throws -> [DebridCloudTorrent] {
public func getUserTorrents() async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data)
let torrents = rawResponse.map { response in
cloudTorrents = rawResponse.map { response in
DebridCloudTorrent(
torrentId: response.id,
source: self.id,
@ -404,29 +454,45 @@ public class RealDebrid: PollingDebridSource {
links: response.links
)
}
return torrents
}
// Deletes a torrent download from RD
public func deleteTorrent(torrentId: String) async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(torrentId)")!)
public func deleteTorrent(torrentId: String?) async throws {
let deleteId: String
if let torrentId {
deleteId = torrentId
} else {
// Refresh the torrent cloud
// The first file is the currently caching one
let _ = try await getUserTorrents()
guard let firstTorrent = cloudTorrents[safe: -1] else {
throw DebridError.EmptyTorrents
}
deleteId = firstTorrent.torrentId
}
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(deleteId)")!)
request.httpMethod = "DELETE"
try await performRequest(request: &request, requestName: #function)
}
// Gets the user's downloads
public func getUserDownloads() async throws -> [DebridCloudDownload] {
public func getUserDownloads() async throws {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads")!)
let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data)
let downloads = rawResponse.map { response in
cloudDownloads = rawResponse.map { response in
DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download)
}
}
return downloads
// Not used
public func checkUserDownloads(link: String) -> String? {
nil
}
public func deleteDownload(downloadId: String) async throws {

View file

@ -8,20 +8,6 @@
import Foundation
public extension AllDebrid {
// MARK: - Errors
// TODO: Hybridize debrid errors in one structure
enum ADError: Error {
case InvalidUrl
case InvalidPostBody
case InvalidResponse
case InvalidToken
case EmptyData
case EmptyTorrents
case FailedRequest(description: String)
case AuthQuery(description: String)
}
// MARK: - Generic AllDebrid response
// Uses a generic parametr for whatever underlying response is present

View file

@ -43,3 +43,15 @@ public struct DebridCloudTorrent: Hashable, Sendable {
let hash: String
let links: [String]
}
public enum DebridError: Error {
case InvalidUrl
case InvalidPostBody
case InvalidResponse
case InvalidToken
case EmptyData
case EmptyTorrents
case IsCaching
case FailedRequest(description: String)
case AuthQuery(description: String)
}

View file

@ -8,20 +8,6 @@
import Foundation
public extension Premiumize {
// MARK: - Errors
// TODO: Hybridize debrid errors in one structure
enum PMError: Error {
case InvalidUrl
case InvalidPostBody
case InvalidResponse
case InvalidToken
case EmptyData
case EmptyTorrents
case FailedRequest(description: String)
case AuthQuery(description: String)
}
// MARK: - CacheCheckResponse
struct CacheCheckResponse: Codable {
@ -33,7 +19,7 @@ public extension Premiumize {
struct DDLResponse: Codable {
let status: String
let content: [DDLData]
let content: [DDLData]?
let location: String
let filename: String
let filesize: Int

View file

@ -9,20 +9,6 @@
import Foundation
public extension RealDebrid {
// MARK: - Errors
// TODO: Hybridize debrid errors in one structure
enum RDError: Error {
case InvalidUrl
case InvalidPostBody
case InvalidResponse
case InvalidToken
case EmptyData
case EmptyTorrents
case FailedRequest(description: String)
case AuthQuery(description: String)
}
// MARK: - device code endpoint
struct DeviceCodeResponse: Codable, Sendable {

View file

@ -7,29 +7,48 @@
import Foundation
public protocol DebridSource {
public protocol DebridSource: AnyObservableObject {
// ID of the service
// var id: DebridInfo { get }
var id: String { get }
var abbreviation: String { get }
var website: String { get }
// Auth variables
var authProcessing: Bool { get set }
var isLoggedIn: Bool { get }
// Manual API key
var manualToken: String? { get }
// Common authentication functions
func setApiKey(_ key: String) -> Bool
func setApiKey(_ key: String)
func logout() async
func instantAvailability(magnets: [Magnet]) async throws -> [DebridIA]
// Instant availability variables
var IAValues: [DebridIA] { get set }
// Instant availability functions
func instantAvailability(magnets: [Magnet]) async throws
// Fetches a download link from a source
// Include the instant availability information with the args
// Torrents also checked here
func getDownloadLink(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> String
// Fetches cloud information from the service
func getUserDownloads() async throws -> [DebridCloudDownload]
func getUserTorrents() async throws -> [DebridCloudTorrent]
// Cloud variables
var cloudDownloads: [DebridCloudDownload] { get set }
var cloudTorrents: [DebridCloudTorrent] { get set }
var cloudTTL: Double { get set }
// Deletes information from the service
// User downloads functions
func getUserDownloads() async throws
func checkUserDownloads(link: String) async throws -> String?
func deleteDownload(downloadId: String) async throws
func deleteTorrent(torrentId: String) async throws
// User torrent functions
func getUserTorrents() async throws
func deleteTorrent(torrentId: String?) async throws
}
public protocol PollingDebridSource: DebridSource {

147
Ferrite/Utils/Store.swift Normal file
View file

@ -0,0 +1,147 @@
//
// Store.swift
// Ferrite
//
//
// Originally created by William Baker on 09/06/2022.
// https://github.com/Tiny-Home-Consulting/Dependiject/blob/master/Dependiject/Store.swift
// Copyright (c) 2022 Tiny Home Consulting LLC. All rights reserved.
//
// Combined together by Brian Dashore
//
// TODO: Replace with Observable when minVersion >= iOS 17
//
import Combine
import SwiftUI
class ErasedObservableObject: ObservableObject {
let objectWillChange: AnyPublisher<Void, Never>
init(objectWillChange: AnyPublisher<Void, Never>) {
self.objectWillChange = objectWillChange
}
static func empty() -> ErasedObservableObject {
.init(objectWillChange: Empty().eraseToAnyPublisher())
}
}
public protocol AnyObservableObject: AnyObject {
var objectWillChange: ObservableObjectPublisher { get }
}
// The generic type names were chosen to match the SwiftUI equivalents:
// - ObjectType from StateObject<ObjectType> and ObservedObject<ObjectType>
// - Subject from ObservedObject.Wrapper.subscript<Subject>(dynamicMember:)
// - S from Publisher.receive<S>(on:options:)
/// A property wrapper used to wrap injected observable objects.
///
/// This is similar to SwiftUI's
/// [`StateObject`](https://developer.apple.com/documentation/swiftui/stateobject), but without
/// compile-time type restrictions. The lack of compile-time restrictions means that `ObjectType`
/// may be a protocol rather than a class.
///
/// - Important: At runtime, the wrapped value must conform to ``AnyObservableObject``.
///
/// To pass properties of the observable object down the view hierarchy as bindings, use the
/// projected value:
/// ```swift
/// struct ExampleView: View {
/// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self)
///
/// var body: some View {
/// TextField("username", text: $viewModel.username)
/// }
/// }
/// ```
/// Not all injected objects need this property wrapper. See the example projects for examples each
/// way.
@propertyWrapper
public struct Store<ObjectType> {
/// The underlying object being stored.
public let wrappedValue: ObjectType
// See https://github.com/Tiny-Home-Consulting/Dependiject/issues/38
fileprivate var _observableObject: ObservedObject<ErasedObservableObject>
@MainActor internal var observableObject: ErasedObservableObject {
_observableObject.wrappedValue
}
/// A projected value which has the same properties as the wrapped value, but presented as
/// bindings.
///
/// Use this to pass bindings down the view hierarchy:
/// ```swift
/// struct ExampleView: View {
/// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self)
///
/// var body: some View {
/// TextField("username", text: $viewModel.username)
/// }
/// }
/// ```
public var projectedValue: Wrapper {
Wrapper(self)
}
/// Create a stored value on a custom scheduler.
///
/// Use this init to schedule updates on a specific scheduler other than `DispatchQueue.main`.
public init<S: Scheduler>(wrappedValue: ObjectType,
on scheduler: S,
schedulerOptions: S.SchedulerOptions? = nil)
{
self.wrappedValue = wrappedValue
if let observable = wrappedValue as? AnyObservableObject {
let objectWillChange = observable.objectWillChange
.receive(on: scheduler, options: schedulerOptions)
.eraseToAnyPublisher()
_observableObject = .init(initialValue: .init(objectWillChange: objectWillChange))
} else {
assertionFailure(
"Only use the Store property wrapper with objects conforming to AnyObservableObject."
)
_observableObject = .init(initialValue: .empty())
}
}
/// Create a stored value which publishes on the main thread.
///
/// To control when updates are published, see ``init(wrappedValue:on:schedulerOptions:)``.
public init(wrappedValue: ObjectType) {
self.init(wrappedValue: wrappedValue, on: DispatchQueue.main)
}
/// An equivalent to SwiftUI's
/// [`ObservedObject.Wrapper`](https://developer.apple.com/documentation/swiftui/observedobject/wrapper)
/// type.
@dynamicMemberLookup
public struct Wrapper {
private var store: Store
internal init(_ store: Store<ObjectType>) {
self.store = store
}
/// Returns a binding to the resulting value of a given key path.
public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
) -> Binding<Subject> {
Binding {
self.store.wrappedValue[keyPath: keyPath]
} set: {
self.store.wrappedValue[keyPath: keyPath] = $0
}
}
}
}
extension Store: DynamicProperty {
public nonisolated mutating func update() {
_observableObject.update()
}
}

View file

@ -12,26 +12,35 @@ import SwiftUI
public class DebridManager: ObservableObject {
// Linked classes
var logManager: LoggingManager?
let realDebrid: RealDebrid = .init()
let allDebrid: AllDebrid = .init()
let premiumize: Premiumize = .init()
@Published var realDebrid: RealDebrid = .init()
@Published var allDebrid: AllDebrid = .init()
@Published var premiumize: Premiumize = .init()
lazy var debridSources: [DebridSource] = [realDebrid, allDebrid, premiumize]
// UI Variables
@Published var showWebView: Bool = false
@Published var showAuthSession: Bool = false
// Service agnostic variables
@Published var enabledDebrids: Set<DebridType> = [] {
var hasEnabledDebrids: Bool {
debridSources.contains { $0.isLoggedIn }
}
var enabledDebridCount: Int {
debridSources.filter(\.isLoggedIn).count
}
@Published var selectedDebridSource: DebridSource? {
didSet {
UserDefaults.standard.set(enabledDebrids.rawValue, forKey: "Debrid.EnabledArray")
UserDefaults.standard.set(selectedDebridSource?.id ?? "", forKey: "Debrid.PreferredService")
}
}
@Published var selectedDebridType: DebridType? {
didSet {
UserDefaults.standard.set(selectedDebridType?.rawValue ?? 0, forKey: "Debrid.PreferredService")
}
}
var selectedDebridItem: DebridIA?
var selectedDebridFile: DebridIAFile?
// TODO: Figure out a way to remove this var
var selectedOAuthDebridSource: OAuthDebridSource?
@Published var filteredIAStatus: Set<IAStatus> = []
@ -39,104 +48,46 @@ public class DebridManager: ObservableObject {
var downloadUrl: String = ""
var authUrl: URL?
// Is the current debrid type processing an auth request
func authProcessing(_ passedDebridType: DebridType?) -> Bool {
guard let debridType = passedDebridType ?? selectedDebridType else {
return false
}
switch debridType {
case .realDebrid:
return realDebridAuthProcessing
case .allDebrid:
return allDebridAuthProcessing
case .premiumize:
return premiumizeAuthProcessing
}
}
// RealDebrid auth variables
var realDebridAuthProcessing: Bool = false
// RealDebrid fetch variables
@Published var realDebridIAValues: [DebridIA] = []
@Published var showDeleteAlert: Bool = false
var selectedRealDebridItem: DebridIA?
var selectedRealDebridFile: DebridIAFile?
var selectedRealDebridID: String?
// TODO: Maybe make these generic?
// RealDebrid cloud variables
@Published var realDebridCloudTorrents: [DebridCloudTorrent] = []
@Published var realDebridCloudDownloads: [DebridCloudDownload] = []
var realDebridCloudTTL: Double = 0.0
// AllDebrid auth variables
var allDebridAuthProcessing: Bool = false
// AllDebrid fetch variables
@Published var allDebridIAValues: [DebridIA] = []
var selectedAllDebridItem: DebridIA?
var selectedAllDebridFile: DebridIAFile?
// AllDebrid cloud variables
@Published var allDebridCloudMagnets: [DebridCloudTorrent] = []
@Published var allDebridCloudLinks: [DebridCloudDownload] = []
var allDebridCloudTTL: Double = 0.0
// Premiumize auth variables
var premiumizeAuthProcessing: Bool = false
// Premiumize fetch variables
@Published var premiumizeIAValues: [DebridIA] = []
var selectedPremiumizeItem: DebridIA?
var selectedPremiumizeFile: DebridIAFile?
// Premiumize cloud variables
@Published var premiumizeCloudItems: [DebridCloudDownload] = []
var premiumizeCloudTTL: Double = 0.0
init() {
if let rawDebridList = UserDefaults.standard.string(forKey: "Debrid.EnabledArray"),
let serializedDebridList = Set<DebridType>(rawValue: rawDebridList)
{
enabledDebrids = serializedDebridList
}
// Set the preferred service. Contains migration logic for earlier versions
if let rawPreferredService = UserDefaults.standard.string(forKey: "Debrid.PreferredService") {
let debridServiceId: String?
// If a UserDefaults integer isn't set, it's usually 0
let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService")
selectedDebridType = DebridType(rawValue: rawPreferredService)
if let preferredServiceInt = Int(rawPreferredService) {
debridServiceId = migratePreferredService(preferredServiceInt)
} else {
debridServiceId = rawPreferredService
}
// If a user has one logged in service, automatically set the preferred service to that one
if enabledDebrids.count == 1 {
selectedDebridType = enabledDebrids.first
// Only set the debrid source if it's logged in
// Otherwise remove the key
let tempDebridSource = debridSources.first { $0.id == debridServiceId }
if tempDebridSource?.isLoggedIn ?? false {
selectedDebridSource = tempDebridSource
} else {
UserDefaults.standard.removeObject(forKey: "Debrid.PreferredService")
}
}
}
// TODO: Remove this after v0.6.0
// Login cleanup function that's automatically run to switch to the new login system
public func cleanupOldLogins() async {
let realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
if realDebridEnabled {
enabledDebrids.insert(.realDebrid)
UserDefaults.standard.set(false, forKey: "RealDebrid.Enabled")
}
// TODO: Remove after v0.8.0
// Function to migrate the preferred service to the new string ID format
public func migratePreferredService(_ idInt: Int) -> String? {
// Undo the EnabledDebrids key
UserDefaults.standard.removeObject(forKey: "Debrid.EnabledArray")
let allDebridEnabled = UserDefaults.standard.bool(forKey: "AllDebrid.Enabled")
if allDebridEnabled {
enabledDebrids.insert(.allDebrid)
UserDefaults.standard.set(false, forKey: "AllDebrid.Enabled")
}
let premiumizeEnabled = UserDefaults.standard.bool(forKey: "Premiumize.Enabled")
if premiumizeEnabled {
enabledDebrids.insert(.premiumize)
UserDefaults.standard.set(false, forKey: "Premiumize.Enabled")
}
return DebridType(rawValue: idInt)?.toString()
}
// Wrapper function to match error descriptions
@ -169,88 +120,29 @@ public class DebridManager: ObservableObject {
// Cleans all cached IA values in the event of a full IA refresh
public func clearIAValues() {
realDebridIAValues = []
allDebridIAValues = []
premiumizeIAValues = []
for debridSource in debridSources {
debridSource.IAValues = []
}
}
// Clears all selected files and items
public func clearSelectedDebridItems() {
switch selectedDebridType {
case .realDebrid:
selectedRealDebridFile = nil
selectedRealDebridItem = nil
case .allDebrid:
selectedAllDebridFile = nil
selectedAllDebridItem = nil
case .premiumize:
selectedPremiumizeFile = nil
selectedPremiumizeItem = nil
case .none:
break
}
selectedDebridItem = nil
selectedDebridFile = nil
}
// Common function to populate hashes for debrid services
public func populateDebridIA(_ resultMagnets: [Magnet]) async {
let now = Date()
// If a hash isn't found in the IA, update it
// If the hash is expired, remove it and update it
let sendMagnets = resultMagnets.filter { magnet in
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.realDebrid) {
if now.timeIntervalSince1970 > realDebridIAValues[IAIndex].expiryTimeStamp {
realDebridIAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else if let IAIndex = allDebridIAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.allDebrid) {
if now.timeIntervalSince1970 > allDebridIAValues[IAIndex].expiryTimeStamp {
allDebridIAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else if let IAIndex = premiumizeIAValues.firstIndex(where: { $0.magnet.hash == magnet.hash }), enabledDebrids.contains(.premiumize) {
if now.timeIntervalSince1970 > premiumizeIAValues[IAIndex].expiryTimeStamp {
premiumizeIAValues.remove(at: IAIndex)
return true
} else {
return false
}
} else {
return true
}
}
// Don't exit the function if the API fetch errors
if !sendMagnets.isEmpty {
if enabledDebrids.contains(.realDebrid) {
do {
let fetchedRealDebridIA = try await realDebrid.instantAvailability(magnets: sendMagnets)
realDebridIAValues += fetchedRealDebridIA
} catch {
await sendDebridError(error, prefix: "RealDebrid IA fetch error")
}
for debridSource in debridSources {
if !debridSource.isLoggedIn {
continue
}
if enabledDebrids.contains(.allDebrid) {
do {
let fetchedAllDebridIA = try await allDebrid.instantAvailability(magnets: sendMagnets)
allDebridIAValues += fetchedAllDebridIA
} catch {
await sendDebridError(error, prefix: "AllDebrid IA fetch error")
}
}
if enabledDebrids.contains(.premiumize) {
do {
let fetchedPremiumizeIA = try await premiumize.instantAvailability(magnets: sendMagnets)
premiumizeIAValues += fetchedPremiumizeIA
} catch {
await sendDebridError(error, prefix: "Premiumize IA fetch error")
}
// Don't exit the function if the API fetch errors
do {
try await debridSource.instantAvailability(magnets: resultMagnets)
} catch {
await sendDebridError(error, prefix: "\(debridSource.id) IA fetch error")
}
}
}
@ -261,38 +153,11 @@ public class DebridManager: ObservableObject {
return .none
}
switch selectedDebridType {
case .realDebrid:
guard let realDebridMatch = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
return .none
}
if realDebridMatch.files.count > 1 {
return .partial
} else {
return .full
}
case .allDebrid:
guard let allDebridMatch = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
return .none
}
if allDebridMatch.files.count > 1 {
return .partial
} else {
return .full
}
case .premiumize:
guard let premiumizeMatch = premiumizeIAValues.first(where: { magnetHash == $0.magnet.hash }) else {
return .none
}
if premiumizeMatch.files.count > 1 {
return .partial
} else {
return .full
}
case .none:
if let selectedDebridSource,
let match = selectedDebridSource.IAValues.first(where: { magnetHash == $0.magnet.hash })
{
return match.files.count > 1 ? .partial : .full
} else {
return .none
}
}
@ -303,32 +168,15 @@ public class DebridManager: ObservableObject {
return false
}
switch selectedDebridType {
case .realDebrid:
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedRealDebridItem = realDebridItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
return false
}
case .allDebrid:
if let allDebridItem = allDebridIAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedAllDebridItem = allDebridItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)")
return false
}
case .premiumize:
if let premiumizeItem = premiumizeIAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedPremiumizeItem = premiumizeItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)")
return false
}
case .none:
guard let selectedSource = selectedDebridSource else {
return false
}
if let IAItem = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedDebridItem = IAItem
return true
} else {
logManager?.error("DebridManager: Could not find the associated \(selectedSource.id) entry for magnet hash \(magnetHash)")
return false
}
}
@ -336,73 +184,62 @@ public class DebridManager: ObservableObject {
// MARK: - Authentication UI linked functions
// Common function to delegate what debrid service to authenticate with
public func authenticateDebrid(debridType: DebridType, apiKey: String?) async {
switch debridType {
case .realDebrid:
let success = apiKey == nil ? await authenticateRd() : realDebrid.setApiKey(apiKey!)
completeDebridAuth(debridType, success: success)
case .allDebrid:
// Async can't work with nil mapping method
let success = apiKey == nil ? await authenticateAd() : allDebrid.setApiKey(apiKey!)
completeDebridAuth(debridType, success: success)
case .premiumize:
if let apiKey {
let success = premiumize.setApiKey(apiKey)
completeDebridAuth(debridType, success: success)
} else {
await authenticatePm()
public func authenticateDebrid(_ debridSource: some DebridSource, apiKey: String?) async {
defer {
// Don't cancel processing if using OAuth
if !(debridSource is OAuthDebridSource) {
debridSource.authProcessing = false
}
}
}
// Callback to finish debrid auth since functions can be split
func completeDebridAuth(_ debridType: DebridType, success: Bool) {
if success {
enabledDebrids.insert(debridType)
if enabledDebrids.count == 1 {
selectedDebridType = enabledDebrids.first
if enabledDebridCount == 1 {
selectedDebridSource = debridSource
}
}
switch debridType {
case .realDebrid:
realDebridAuthProcessing = false
case .allDebrid:
allDebridAuthProcessing = false
case .premiumize:
premiumizeAuthProcessing = false
// Set an API key if manually provided
if let apiKey {
debridSource.setApiKey(apiKey)
return
}
// Processing has started
debridSource.authProcessing = true
if let pollingSource = debridSource as? PollingDebridSource {
do {
let authUrl = try await pollingSource.getAuthUrl()
if validateAuthUrl(authUrl) {
try await pollingSource.authTask?.value
} else {
throw DebridError.AuthQuery(description: "The authentication URL was invalid")
}
} catch {
await sendDebridError(error, prefix: "\(debridSource.id) authentication error")
pollingSource.authTask?.cancel()
}
} else if let oauthSource = debridSource as? OAuthDebridSource {
do {
let tempAuthUrl = try oauthSource.getAuthUrl()
selectedOAuthDebridSource = oauthSource
validateAuthUrl(tempAuthUrl, useAuthSession: true)
} catch {
await sendDebridError(error, prefix: "\(debridSource.id) authentication error")
}
} else {
logManager?.error(
"DebridManager: Auth: Could not figure out the authentication type for \(debridSource.id). Is this configured properly?"
)
return
}
}
// Get a truncated manual API key if it's being used
func getManualAuthKey(_ passedDebridType: DebridType?) async -> String? {
guard let debridType = passedDebridType ?? selectedDebridType else {
return nil
}
let debridToken: String?
switch debridType {
case .realDebrid:
if UserDefaults.standard.bool(forKey: "RealDebrid.UseManualKey") {
debridToken = FerriteKeychain.shared.get("RealDebrid.AccessToken")
} else {
debridToken = nil
}
case .allDebrid:
if UserDefaults.standard.bool(forKey: "AllDebrid.UseManualKey") {
debridToken = FerriteKeychain.shared.get("AllDebrid.ApiKey")
} else {
debridToken = nil
}
case .premiumize:
if UserDefaults.standard.bool(forKey: "Premiumize.UseManualKey") {
debridToken = FerriteKeychain.shared.get("Premiumize.AccessToken")
} else {
debridToken = nil
}
}
if let debridToken {
func getManualAuthKey(_ debridSource: some DebridSource) async -> String? {
if let debridToken = debridSource.manualToken {
let splitString = debridToken.suffix(4)
if debridToken.count > 4 {
@ -432,114 +269,43 @@ public class DebridManager: ObservableObject {
return true
}
private func authenticateRd() async -> Bool {
do {
realDebridAuthProcessing = true
let authUrl = try await realDebrid.getAuthUrl()
if validateAuthUrl(authUrl) {
try await realDebrid.authTask?.value
return true
} else {
throw RealDebrid.RDError.AuthQuery(description: "The verification URL was invalid")
}
} catch {
await sendDebridError(error, prefix: "RealDebrid authentication error")
realDebrid.authTask?.cancel()
return false
}
}
private func authenticateAd() async -> Bool {
do {
allDebridAuthProcessing = true
let authUrl = try await allDebrid.getAuthUrl()
if validateAuthUrl(authUrl) {
try await allDebrid.authTask?.value
return true
} else {
throw AllDebrid.ADError.AuthQuery(description: "The PIN URL was invalid")
}
} catch {
await sendDebridError(error, prefix: "AllDebrid authentication error")
allDebrid.authTask?.cancel()
return false
}
}
private func authenticatePm() async {
do {
premiumizeAuthProcessing = true
let tempAuthUrl = try premiumize.getAuthUrl()
validateAuthUrl(tempAuthUrl, useAuthSession: true)
} catch {
await sendDebridError(error, prefix: "Premiumize authentication error")
completeDebridAuth(.premiumize, success: false)
}
}
// Currently handles Premiumize callback
public func handleCallback(url: URL?, error: Error?) async {
public func handleAuthCallback(url: URL?, error: Error?) async {
defer {
if enabledDebridCount == 1 {
selectedDebridSource = selectedOAuthDebridSource
}
selectedOAuthDebridSource?.authProcessing = false
}
do {
guard let oauthDebridSource = selectedOAuthDebridSource else {
throw DebridError.AuthQuery(description: "OAuth source couldn't be found for callback. Aborting.")
}
if let error {
throw Premiumize.PMError.AuthQuery(description: "OAuth callback Error: \(error)")
throw DebridError.AuthQuery(description: "OAuth callback Error: \(error)")
}
if let callbackUrl = url {
try premiumize.handleAuthCallback(url: callbackUrl)
completeDebridAuth(.premiumize, success: true)
try oauthDebridSource.handleAuthCallback(url: callbackUrl)
} else {
throw Premiumize.PMError.AuthQuery(description: "The callback URL was invalid")
throw DebridError.AuthQuery(description: "The callback URL was invalid")
}
} catch {
await sendDebridError(error, prefix: "Premiumize authentication error (callback)")
completeDebridAuth(.premiumize, success: false)
}
}
// MARK: - Logout UI linked functions
// MARK: - Logout UI functions
// Common function to delegate what debrid service to logout of
public func logoutDebrid(debridType: DebridType) async {
switch debridType {
case .realDebrid:
await logoutRd()
case .allDebrid:
logoutAd()
case .premiumize:
logoutPm()
public func logout(_ debridSource: some DebridSource) async {
await debridSource.logout()
if selectedDebridSource?.id == debridSource.id {
selectedDebridSource = nil
}
// Automatically resets the preferred debrid service if it was set to the logged out service
if selectedDebridType == debridType {
selectedDebridType = nil
}
}
private func logoutRd() async {
await realDebrid.logout()
enabledDebrids.remove(.realDebrid)
}
private func logoutAd() {
allDebrid.logout()
enabledDebrids.remove(.allDebrid)
logManager?.info(
"AllDebrid: Logged out, API key needs to be removed",
description: "Please manually delete the AllDebrid API key"
)
}
private func logoutPm() {
premiumize.logout()
enabledDebrids.remove(.premiumize)
}
// MARK: - Debrid fetch UI linked functions
@ -557,275 +323,89 @@ public class DebridManager: ObservableObject {
self.currentDebridTask = nil
})
switch selectedDebridType {
case .realDebrid:
await fetchRdDownload(magnet: magnet, existingLink: cloudInfo)
case .allDebrid:
await fetchAdDownload(magnet: magnet, existingLockedLink: cloudInfo)
case .premiumize:
await fetchPmDownload(magnet: magnet, cloudItemId: cloudInfo)
case .none:
break
guard let debridSource = selectedDebridSource else {
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 {
// If the links match from a user's downloads, no need to re-run a download
/*
if let torrentLink,
let downloadLink = await checkRdUserDownloads(userTorrentLink: torrentLink)
{
downloadUrl = downloadLink
} else */
if let cloudInfo {
downloadUrl = try await debridSource.checkUserDownloads(link: cloudInfo) ?? ""
return
}
if let magnet {
let downloadLink = try await realDebrid.getDownloadLink(
magnet: magnet, ia: selectedRealDebridItem, iaFile: selectedRealDebridFile
let downloadLink = try await debridSource.getDownloadLink(
magnet: magnet, ia: selectedDebridItem, iaFile: selectedDebridFile
)
// Update the UI
downloadUrl = downloadLink
} else {
throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API")
throw DebridError.FailedRequest(description: "Could not fetch your file from \(debridSource.id)'s cache or API")
}
// Fetch one more time to add updated data into the RD cloud cache
await fetchRdCloud(bypassTTL: true)
await fetchDebridCloud(bypassTTL: true)
} catch {
switch error {
case RealDebrid.RDError.EmptyTorrents:
case DebridError.IsCaching:
showDeleteAlert.toggle()
default:
await sendDebridError(error, prefix: "RealDebrid download error", cancelString: "Download cancelled")
// await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false)
await sendDebridError(error, prefix: "\(debridSource.id) download error", cancelString: "Download cancelled")
}
logManager?.hideIndeterminateToast()
}
}
// Wrapper to handle cloud fetching
public func fetchDebridCloud(bypassTTL: Bool = false) async {
switch selectedDebridType {
case .realDebrid:
await fetchRdCloud(bypassTTL: bypassTTL)
case .allDebrid:
await fetchAdCloud(bypassTTL: bypassTTL)
case .premiumize:
await fetchPmCloud(bypassTTL: bypassTTL)
case .none:
guard let selectedSource = selectedDebridSource else {
return
}
}
// Refreshes torrents and downloads from a RD user's account
public func fetchRdCloud(bypassTTL: Bool = false) async {
if bypassTTL || Date().timeIntervalSince1970 > realDebridCloudTTL {
if bypassTTL || Date().timeIntervalSince1970 > selectedSource.cloudTTL {
do {
realDebridCloudTorrents = try await realDebrid.getUserTorrents()
realDebridCloudDownloads = try await realDebrid.getUserDownloads()
// Populates the inner downloads and torrent arrays
try await selectedSource.getUserDownloads()
try await selectedSource.getUserTorrents()
// 5 minutes
realDebridCloudTTL = Date().timeIntervalSince1970 + 300
} catch {
await sendDebridError(error, prefix: "RealDebrid cloud fetch error")
}
}
}
func deleteRdDownload(downloadID: String) async {
do {
try await realDebrid.deleteDownload(downloadId: 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(torrentId: torrentID)
} 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?.fileName {
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?.fileId ?? 0]?.link
}
*/
do {
/*
if let lockedLink,
let unlockedLink = await checkAdUserLinks(lockedLink: lockedLink)
{
downloadUrl = unlockedLink
} else if let magnet {
*/
if let magnet {
let downloadLink = try await allDebrid.getDownloadLink(
magnet: magnet, ia: selectedAllDebridItem, iaFile: selectedAllDebridFile
)
// Update UI
downloadUrl = downloadLink
} else {
throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API")
}
// Fetch one more time to add updated data into the AD cloud cache
await fetchAdCloud(bypassTTL: true)
} catch {
await sendDebridError(error, prefix: "AllDebrid download error", cancelString: "Download cancelled")
}
}
func checkAdUserLinks(lockedLink: String) async -> String? {
do {
let existingLinks = allDebridCloudLinks.first { $0.link == lockedLink }
if let existingLink = existingLinks?.link {
return existingLink
} else {
try await allDebrid.saveLink(link: lockedLink)
return try await allDebrid.unlockLink(lockedLink: lockedLink)
}
} catch {
await sendDebridError(error, prefix: "AllDebrid download check error")
return nil
}
}
// Refreshes torrents and downloads from a RD user's account
public func fetchAdCloud(bypassTTL: Bool = false) async {
if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL {
do {
allDebridCloudMagnets = try await allDebrid.getUserTorrents()
allDebridCloudLinks = try await allDebrid.getUserDownloads()
// 5 minutes
allDebridCloudTTL = Date().timeIntervalSince1970 + 300
} catch {
await sendDebridError(error, prefix: "AlLDebrid cloud fetch error")
}
}
}
func deleteAdLink(link: String) async {
do {
try await allDebrid.deleteDownload(downloadId: link)
await fetchAdCloud(bypassTTL: true)
} catch {
await sendDebridError(error, prefix: "AllDebrid link delete error")
}
}
func deleteAdMagnet(magnetId: String) async {
do {
try await allDebrid.deleteTorrent(torrentId: magnetId)
await fetchAdCloud(bypassTTL: true)
} catch {
await sendDebridError(error, prefix: "AllDebrid magnet delete error")
}
}
func fetchPmDownload(magnet: Magnet?, cloudItemId: String? = nil) async {
do {
if let cloudItemId {
downloadUrl = try await premiumize.itemDetails(itemID: cloudItemId).link
} else if let magnet {
let downloadLink = try await premiumize.getDownloadLink(
magnet: magnet, ia: selectedPremiumizeItem, iaFile: selectedPremiumizeFile
)
downloadUrl = downloadLink
} else {
throw Premiumize.PMError.FailedRequest(description: "Could not fetch your file from Premiumize's cache or API")
}
// Fetch one more time to add updated data into the PM cloud cache
await fetchPmCloud(bypassTTL: true)
} 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.getUserDownloads()
withAnimation {
premiumizeCloudItems = userItems
}
// 5 minutes
premiumizeCloudTTL = Date().timeIntervalSince1970 + 300
// Update the TTL to 5 minutes from now
selectedSource.cloudTTL = Date().timeIntervalSince1970 + 300
} catch {
let error = error as NSError
if error.code != -999 {
await sendDebridError(error, prefix: "Premiumize cloud fetch error")
await sendDebridError(error, prefix: "\(selectedSource.id) cloud fetch error")
}
}
}
}
public func deletePmItem(id: String) async {
do {
try await premiumize.deleteDownload(downloadId: id)
public func deleteCloudDownload(_ download: DebridCloudDownload) async {
guard let selectedSource = selectedDebridSource else {
return
}
// Bypass TTL to get current RD values
await fetchPmCloud(bypassTTL: true)
do {
try await selectedSource.deleteDownload(downloadId: download.downloadId)
await fetchDebridCloud(bypassTTL: true)
} catch {
await sendDebridError(error, prefix: "Premiumize cloud delete error")
await sendDebridError(error, prefix: "\(selectedSource.id) download delete error")
}
}
public func deleteCloudTorrent(_ torrent: DebridCloudTorrent) async {
guard let selectedSource = selectedDebridSource else {
return
}
do {
try await selectedSource.deleteTorrent(torrentId: torrent.torrentId)
await fetchDebridCloud(bypassTTL: true)
} catch {
await sendDebridError(error, prefix: "\(selectedSource.id) torrent delete error")
}
}
}

View file

@ -121,7 +121,7 @@ class LoggingManager: ObservableObject {
if let description {
toastDescription = description
} else if showErrorToasts {
toastDescription = "An error was logged"
toastDescription = "An error was logged. Please look at logs in Settings."
}
}

View file

@ -80,7 +80,7 @@ class ScrapingViewModel: ObservableObject {
cleanedSearchText = searchText.lowercased()
if await !debridManager.enabledDebrids.isEmpty {
if await !debridManager.hasEnabledDebrids {
await debridManager.clearIAValues()
}
@ -114,7 +114,7 @@ class ScrapingViewModel: ObservableObject {
var failedSourceNames: [String] = []
for await (requestResult, sourceName) in group {
if let requestResult {
if await !debridManager.enabledDebrids.isEmpty {
if await debridManager.hasEnabledDebrids {
await debridManager.populateDebridIA(requestResult.magnets)
}

View file

@ -8,38 +8,34 @@
import SwiftUI
struct DebridLabelView: View {
@EnvironmentObject var debridManager: DebridManager
@Store var debridSource: DebridSource
@State var cloudLinks: [String] = []
@State var tagColor: Color = .red
var magnet: Magnet?
var body: some View {
if let selectedDebridType = debridManager.selectedDebridType {
Tag(
name: selectedDebridType.toString(abbreviated: true),
color: getTagColor(),
horizontalPadding: 5,
verticalPadding: 3
)
}
Tag(
name: debridSource.abbreviation,
color: getTagColor(),
horizontalPadding: 5,
verticalPadding: 3
)
}
func getTagColor() -> Color {
if let magnet, cloudLinks.isEmpty {
switch debridManager.matchMagnetHash(magnet) {
case .full:
return Color.green
case .partial:
return Color.orange
case .none:
return Color.red
guard let match = debridSource.IAValues.first(where: { magnet.hash == $0.magnet.hash }) else {
return .red
}
return match.files.count > 1 ? .orange : .green
} else if cloudLinks.count == 1 {
return Color.green
return .green
} else if cloudLinks.count > 1 {
return Color.orange
return .orange
} else {
return Color.red
return .red
}
}
}

View file

@ -15,23 +15,23 @@ struct SelectedDebridFilterView<Content: View>: View {
var body: some View {
Menu {
Button {
debridManager.selectedDebridType = nil
debridManager.selectedDebridSource = nil
} label: {
Text("None")
if debridManager.selectedDebridType == nil {
if debridManager.selectedDebridSource == nil {
Image(systemName: "checkmark")
}
}
ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in
if debridManager.enabledDebrids.contains(debridType) {
ForEach(debridManager.debridSources, id: \.id) { debridSource in
if debridSource.isLoggedIn {
Button {
debridManager.selectedDebridType = debridType
debridManager.selectedDebridSource = debridSource
} label: {
Text(debridType.toString())
Text(debridSource.id)
if debridManager.selectedDebridType == debridType {
if debridManager.selectedDebridSource?.id == debridSource.id {
Image(systemName: "checkmark")
}
}
@ -40,6 +40,5 @@ struct SelectedDebridFilterView<Content: View>: View {
} label: {
label
}
.id(debridManager.selectedDebridType)
}
}

View file

@ -56,7 +56,7 @@ struct BookmarksView: View {
.frame(height: 15)
}
.task {
if debridManager.enabledDebrids.count > 0 {
if debridManager.hasEnabledDebrids {
let magnets = bookmarks.compactMap {
if let magnetHash = $0.magnetHash {
return Magnet(hash: magnetHash, link: $0.magnetLink)

View file

@ -1,125 +0,0 @@
//
// AllDebridCloudView.swift
// Ferrite
//
// Created by Brian Dashore on 1/5/23.
//
import SwiftUI
struct AllDebridCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var pluginManager: PluginManager
@Binding var searchText: String
var body: some View {
DisclosureGroup("Links") {
ForEach(debridManager.allDebridCloudLinks.filter {
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
}, id: \.self) { cloudDownload in
Button(cloudDownload.fileName) {
navModel.resultFromCloud = true
navModel.selectedTitle = cloudDownload.fileName
debridManager.downloadUrl = cloudDownload.link
PersistenceController.shared.createHistory(
HistoryEntryJson(
name: cloudDownload.fileName,
url: cloudDownload.link,
source: DebridType.allDebrid.toString()
),
performSave: true
)
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
if let cloudDownload = debridManager.allDebridCloudLinks[safe: index] {
Task {
await debridManager.deleteAdLink(link: cloudDownload.downloadId)
}
}
}
}
}
DisclosureGroup("Magnets") {
ForEach(debridManager.allDebridCloudMagnets.filter {
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
}, id: \.self) { cloudTorrent in
Button {
if cloudTorrent.status == "Ready", !cloudTorrent.links.isEmpty {
navModel.resultFromCloud = true
navModel.selectedTitle = cloudTorrent.fileName
var historyInfo = HistoryEntryJson(
name: cloudTorrent.fileName,
source: DebridType.allDebrid.toString()
)
Task {
if cloudTorrent.links.count == 1 {
if let torrentLink = cloudTorrent.links[safe: 0] {
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink)
if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo, performSave: true)
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)
}
}
} else {
let magnet = Magnet(hash: cloudTorrent.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(cloudTorrent.fileName)
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(4)
HStack {
Text(cloudTorrent.status.capitalizingFirstLetter())
Spacer()
DebridLabelView(cloudLinks: cloudTorrent.links)
}
.font(.caption)
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
if let cloudTorrent = debridManager.allDebridCloudMagnets[safe: index] {
Task {
await debridManager.deleteAdMagnet(magnetId: cloudTorrent.torrentId)
}
}
}
}
}
}
}

View file

@ -0,0 +1,57 @@
//
// CloudDownloadView.swift
// Ferrite
//
// Created by Brian Dashore on 6/6/24.
//
import SwiftUI
struct CloudDownloadView: View {
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var pluginManager: PluginManager
@Store var debridSource: DebridSource
@Binding var searchText: String
var body: some View {
DisclosureGroup("Downloads") {
ForEach(debridSource.cloudDownloads.filter {
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
}, id: \.self) { cloudDownload in
Button(cloudDownload.fileName) {
navModel.resultFromCloud = true
navModel.selectedTitle = cloudDownload.fileName
debridManager.downloadUrl = cloudDownload.link
PersistenceController.shared.createHistory(
HistoryEntryJson(
name: cloudDownload.fileName,
url: cloudDownload.link,
source: debridSource.id
),
performSave: true
)
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
if let cloudDownload = debridSource.cloudDownloads[safe: index] {
Task {
await debridManager.deleteCloudDownload(cloudDownload)
}
}
}
}
}
}
}

View file

@ -0,0 +1,89 @@
//
// CloudTorrentView.swift
// Ferrite
//
// Created by Brian Dashore on 6/6/24.
//
import SwiftUI
struct CloudTorrentView: View {
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var pluginManager: PluginManager
@Store var debridSource: DebridSource
@Binding var searchText: String
var body: some View {
DisclosureGroup("Torrents") {
ForEach(debridSource.cloudTorrents.filter {
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
}, id: \.self) { cloudTorrent in
Button {
if cloudTorrent.status == "downloaded", !cloudTorrent.links.isEmpty {
navModel.resultFromCloud = true
navModel.selectedTitle = cloudTorrent.fileName
var historyInfo = HistoryEntryJson(
name: cloudTorrent.fileName,
source: debridSource.id
)
Task {
let magnet = Magnet(hash: cloudTorrent.hash, link: nil)
await debridManager.populateDebridIA([magnet])
if debridManager.selectDebridResult(magnet: magnet) {
// Is this a batch?
if cloudTorrent.links.count == 1 {
await debridManager.fetchDebridDownload(magnet: magnet)
if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo, performSave: true)
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)
}
} else {
navModel.selectedMagnet = magnet
navModel.selectedHistoryInfo = historyInfo
navModel.currentChoiceSheet = .batch
}
}
}
}
} label: {
VStack(alignment: .leading, spacing: 10) {
Text(cloudTorrent.fileName)
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(4)
HStack {
Text(cloudTorrent.status.capitalizingFirstLetter())
Spacer()
DebridLabelView(debridSource: debridSource, cloudLinks: cloudTorrent.links)
}
.font(.caption)
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
if let cloudTorrent = debridSource.cloudTorrents[safe: index] {
Task {
await debridManager.deleteCloudTorrent(cloudTorrent)
}
}
}
}
}
}
}

View file

@ -1,60 +0,0 @@
//
// PremiumizeCloudView.swift
// Ferrite
//
// Created by Brian Dashore on 1/2/23.
//
import SwiftUI
struct PremiumizeCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var pluginManager: PluginManager
@Binding var searchText: String
var body: some View {
DisclosureGroup("Items") {
ForEach(debridManager.premiumizeCloudItems.filter {
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
}, id: \.self) { cloudDownload in
Button(cloudDownload.fileName) {
Task {
navModel.resultFromCloud = true
navModel.selectedTitle = cloudDownload.fileName
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: cloudDownload.downloadId)
if !debridManager.downloadUrl.isEmpty {
PersistenceController.shared.createHistory(
HistoryEntryJson(
name: cloudDownload.fileName,
url: cloudDownload.link,
source: DebridType.premiumize.toString()
),
performSave: true
)
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)
}
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
if let cloudDownload = debridManager.premiumizeCloudItems[safe: index] {
Task {
await debridManager.deletePmItem(id: cloudDownload.downloadId)
}
}
}
}
}
}
}

View file

@ -1,127 +0,0 @@
//
// RealDebridCloudView.swift
// Ferrite
//
// Created by Brian Dashore on 12/31/22.
//
import SwiftUI
struct RealDebridCloudView: View {
@EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var pluginManager: PluginManager
@Binding var searchText: String
var body: some View {
Group {
DisclosureGroup("Downloads") {
ForEach(debridManager.realDebridCloudDownloads.filter {
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
}, id: \.self) { cloudDownload in
Button(cloudDownload.fileName) {
navModel.resultFromCloud = true
navModel.selectedTitle = cloudDownload.fileName
debridManager.downloadUrl = cloudDownload.link
PersistenceController.shared.createHistory(
HistoryEntryJson(
name: cloudDownload.fileName,
url: cloudDownload.link,
source: DebridType.realDebrid.toString()
),
performSave: true
)
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
if let cloudDownload = debridManager.realDebridCloudDownloads[safe: index] {
Task {
await debridManager.deleteRdDownload(downloadID: cloudDownload.downloadId)
}
}
}
}
}
DisclosureGroup("Torrents") {
ForEach(debridManager.realDebridCloudTorrents.filter {
searchText.isEmpty ? true : $0.fileName.lowercased().contains(searchText.lowercased())
}, id: \.self) { cloudTorrent in
Button {
if cloudTorrent.status == "downloaded", !cloudTorrent.links.isEmpty {
navModel.resultFromCloud = true
navModel.selectedTitle = cloudTorrent.fileName
var historyInfo = HistoryEntryJson(
name: cloudTorrent.fileName,
source: DebridType.realDebrid.toString()
)
Task {
if cloudTorrent.links.count == 1 {
if let torrentLink = cloudTorrent.links[safe: 0] {
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink)
if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo, performSave: true)
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)
}
}
} else {
let magnet = Magnet(hash: cloudTorrent.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(cloudTorrent.fileName)
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(4)
HStack {
Text(cloudTorrent.status.capitalizingFirstLetter())
Spacer()
DebridLabelView(cloudLinks: cloudTorrent.links)
}
.font(.caption)
}
}
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
.tint(.primary)
}
.onDelete { offsets in
for index in offsets {
if let cloudTorrent = debridManager.realDebridCloudTorrents[safe: index] {
Task {
await debridManager.deleteRdTorrent(torrentID: cloudTorrent.torrentId)
}
}
}
}
}
}
}
}

View file

@ -10,19 +10,18 @@ import SwiftUI
struct DebridCloudView: View {
@EnvironmentObject var debridManager: DebridManager
@Store var debridSource: DebridSource
@Binding var searchText: String
var body: some View {
List {
switch debridManager.selectedDebridType {
case .realDebrid:
RealDebridCloudView(searchText: $searchText)
case .premiumize:
PremiumizeCloudView(searchText: $searchText)
case .allDebrid:
AllDebridCloudView(searchText: $searchText)
case .none:
EmptyView()
if !debridSource.cloudDownloads.isEmpty {
CloudDownloadView(debridSource: debridSource, searchText: $searchText)
}
if !debridSource.cloudTorrents.isEmpty {
CloudTorrentView(debridSource: debridSource, searchText: $searchText)
}
}
.listStyle(.plain)
@ -32,7 +31,7 @@ struct DebridCloudView: View {
.refreshable {
await debridManager.fetchDebridCloud(bypassTTL: true)
}
.onChange(of: debridManager.selectedDebridType) { newType in
.onChange(of: debridManager.selectedDebridSource?.id) { newType in
if newType != nil {
Task {
await debridManager.fetchDebridCloud()

View file

@ -19,7 +19,7 @@ struct LibraryPickerView: View {
Text("Bookmarks").tag(NavigationViewModel.LibraryPickerSegment.bookmarks)
Text("History").tag(NavigationViewModel.LibraryPickerSegment.history)
if !debridManager.enabledDebrids.isEmpty {
if debridManager.hasEnabledDebrids {
Text("Cloud").tag(NavigationViewModel.LibraryPickerSegment.debridCloud)
}
}

View file

@ -53,14 +53,14 @@ struct SearchFilterHeaderView: View {
SelectedDebridFilterView {
FilterLabelView(
name: debridManager.selectedDebridType?.toString(),
name: debridManager.selectedDebridSource?.id,
fallbackName: "Debrid"
)
}
// MARK: - Cache status picker
if !debridManager.enabledDebrids.isEmpty {
if debridManager.hasEnabledDebrids {
IAFilterView()
}

View file

@ -127,13 +127,14 @@ struct SearchResultButtonView: View {
.alert("Caching file", isPresented: $debridManager.showDeleteAlert) {
Button("Yes", role: .destructive) {
Task {
await debridManager.deleteRdTorrent()
try? await debridManager.selectedDebridSource?.deleteTorrent(torrentId: nil)
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text(
"RealDebrid is currently caching this file. Would you like to delete it? \n\n" +
"\(String(describing: debridManager.selectedDebridSource?.id)) is currently caching this file. " +
"Would you like to delete it? \n\n" +
"Progress can be checked on the RealDebrid website."
)
}

View file

@ -30,7 +30,9 @@ struct SearchResultInfoView: View {
Text(size)
}
DebridLabelView(magnet: result.magnet)
if let debridSource = debridManager.selectedDebridSource {
DebridLabelView(debridSource: debridSource, magnet: result.magnet)
}
}
.font(.caption)
}

View file

@ -10,7 +10,7 @@ import SwiftUI
struct SettingsDebridInfoView: View {
@EnvironmentObject var debridManager: DebridManager
let debridType: DebridType
@Store var debridSource: DebridSource
@State private var apiKeyTempText: String = ""
@ -18,9 +18,9 @@ struct SettingsDebridInfoView: View {
List {
Section(header: InlineHeader("Description")) {
VStack(alignment: .leading, spacing: 10) {
Text("\(debridType.toString()) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.")
Text("\(debridSource.id) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.")
Link("Website", destination: URL(string: debridType.website()) ?? URL(string: "https://kingbri.dev/ferrite")!)
Link("Website", destination: URL(string: debridSource.website) ?? URL(string: "https://kingbri.dev/ferrite")!)
}
}
@ -30,21 +30,21 @@ struct SettingsDebridInfoView: View {
) {
Button {
Task {
if debridManager.enabledDebrids.contains(debridType) {
await debridManager.logoutDebrid(debridType: debridType)
} else if !debridManager.authProcessing(debridType) {
await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil)
if debridSource.isLoggedIn {
await debridManager.logout(debridSource)
} else if !debridSource.authProcessing {
await debridManager.authenticateDebrid(debridSource, apiKey: nil)
}
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
}
} label: {
Text(
debridManager.enabledDebrids.contains(debridType)
debridSource.isLoggedIn
? "Logout"
: (debridManager.authProcessing(debridType) ? "Processing" : "Login")
: (debridSource.authProcessing ? "Processing" : "Login")
)
.foregroundColor(debridManager.enabledDebrids.contains(debridType) ? .red : .blue)
.foregroundColor(debridSource.isLoggedIn ? .red : .blue)
}
}
@ -57,22 +57,22 @@ struct SettingsDebridInfoView: View {
onCommit: {
Task {
if !apiKeyTempText.isEmpty {
await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText)
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
await debridManager.authenticateDebrid(debridSource, apiKey: apiKeyTempText)
apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
}
}
}
)
.fieldDisabled(debridManager.enabledDebrids.contains(debridType))
.fieldDisabled(debridSource.isLoggedIn)
}
.onAppear {
Task {
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
apiKeyTempText = await debridManager.getManualAuthKey(debridSource) ?? ""
}
}
}
.listStyle(.insetGrouped)
.navigationTitle(debridType.toString())
.navigationTitle(debridSource.id)
.navigationBarTitleDisplayMode(.inline)
}
}

View file

@ -38,7 +38,13 @@ struct LibraryView: View {
case .history:
HistoryView(allHistoryEntries: allHistoryEntries, searchText: $searchText)
case .debridCloud:
DebridCloudView(searchText: $searchText)
if let selectedDebridSource = debridManager.selectedDebridSource {
DebridCloudView(debridSource: selectedDebridSource, searchText: $searchText)
} else {
// Placeholder view that takes up the entire parent view
Color.clear
.frame(maxWidth: .infinity)
}
}
}
.overlay {
@ -53,7 +59,7 @@ struct LibraryView: View {
EmptyInstructionView(title: "No History", message: "Start watching to build history")
}
case .debridCloud:
if debridManager.selectedDebridType == nil {
if debridManager.selectedDebridSource == nil {
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
}
}
@ -69,7 +75,7 @@ struct LibraryView: View {
switch navModel.libraryPickerSelection {
case .bookmarks, .debridCloud:
SelectedDebridFilterView {
Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid")
Text(debridManager.selectedDebridSource?.abbreviation ?? "Debrid")
}
.transaction {
$0.animation = .none

View file

@ -46,14 +46,14 @@ struct SettingsView: View {
NavView {
Form {
Section(header: InlineHeader("Debrid services")) {
ForEach(DebridType.allCases, id: \.self) { debridType in
ForEach(debridManager.debridSources, id: \.id) { (debridSource: DebridSource) in
NavigationLink {
SettingsDebridInfoView(debridType: debridType)
SettingsDebridInfoView(debridSource: debridSource)
} label: {
HStack {
Text(debridType.toString())
Text(debridSource.id)
Spacer()
Text(debridManager.enabledDebrids.contains(debridType) ? "Enabled" : "Disabled")
Text(debridSource.isLoggedIn ? "Enabled" : "Disabled")
.foregroundColor(.secondary)
}
}
@ -128,7 +128,7 @@ struct SettingsView: View {
}
Section(header: InlineHeader("Default actions")) {
if debridManager.enabledDebrids.count > 0 {
if debridManager.hasEnabledDebrids {
NavigationLink {
DefaultActionPickerView(
actionRequirement: .debrid,
@ -227,7 +227,7 @@ struct SettingsView: View {
callbackURLScheme: "ferrite"
) { callbackURL, error in
Task {
await debridManager.handleCallback(url: callbackURL, error: error)
await debridManager.handleAuthCallback(url: callbackURL, error: error)
}
}
.prefersEphemeralWebBrowserSession(useEphemeralAuth)

View file

@ -23,39 +23,14 @@ struct BatchChoiceView: View {
var body: some View {
NavView {
List {
switch debridManager.selectedDebridType {
case .realDebrid:
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedRealDebridFile = file
ForEach(debridManager.selectedDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedDebridFile = file
queueCommonDownload(fileName: file.name)
}
queueCommonDownload(fileName: file.name)
}
}
case .allDebrid:
ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedAllDebridFile = file
queueCommonDownload(fileName: file.name)
}
}
}
case .premiumize:
ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) {
debridManager.selectedPremiumizeFile = file
queueCommonDownload(fileName: file.name)
}
}
}
case .none:
EmptyView()
}
}
.tint(.primary)
@ -85,7 +60,7 @@ struct BatchChoiceView: View {
// Common function to communicate betwen VMs and queue/display a download
func queueCommonDownload(fileName: String) {
debridManager.currentDebridTask = Task {
await debridManager.fetchDebridDownload(magnet: navModel.resultFromCloud ? nil : navModel.selectedMagnet)
await debridManager.fetchDebridDownload(magnet: navModel.selectedMagnet)
if !debridManager.downloadUrl.isEmpty {
try? await Task.sleep(seconds: 1)