Compare commits
75 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d7bc9b314 | ||
|
|
25bff02875 | ||
|
|
20dd00fa85 | ||
|
|
f9d2f38329 | ||
|
|
a7e20f30e6 | ||
|
|
ecf92239d2 | ||
|
|
dd54ec027b | ||
|
|
84357ea2c5 | ||
|
|
4fb5f77718 | ||
|
|
e5a872e09f | ||
|
|
1d6ac13e84 | ||
|
|
896efed663 | ||
|
|
a3463948ea | ||
|
|
215cd0feec | ||
|
|
9213b8627b | ||
|
|
6b40bb3ea2 | ||
|
|
dbf12c0a79 | ||
|
|
70b628b608 | ||
|
|
78f2aff25b | ||
|
|
489da8e82e | ||
|
|
078e48d316 | ||
|
|
646c22c9be | ||
|
|
d512d8b88d | ||
|
|
d0728e1a9b | ||
|
|
89367b72da | ||
|
|
c5a08cc725 | ||
|
|
0d39fd481a | ||
|
|
5223c60acd | ||
|
|
80e966512a | ||
|
|
8f7fe94d21 | ||
|
|
3ef041f889 | ||
|
|
e49e37af36 | ||
|
|
d6d731102c | ||
|
|
4beb953596 | ||
|
|
e1eca593f3 | ||
|
|
9b4f31daac | ||
|
|
24e39f9fba | ||
|
|
904b5a74b5 | ||
|
|
ecdd0199f6 | ||
|
|
3b771e5deb | ||
|
|
d8107cb5b6 | ||
|
|
42e202b207 | ||
|
|
afceea7bfb | ||
|
|
4ae1966934 | ||
|
|
796cc65016 | ||
|
|
90f44348b8 | ||
|
|
6192ef1ede | ||
|
|
973fbb4099 | ||
|
|
243a16e3c4 | ||
|
|
44a90b77eb | ||
|
|
59ac719d9a | ||
|
|
02636e0bda | ||
|
|
40b323bd56 | ||
|
|
91f124130c | ||
|
|
ec8455c08d | ||
|
|
0c3648120d | ||
|
|
9650e6deec | ||
|
|
07731e7b00 | ||
|
|
b80f8900b7 | ||
|
|
cf0c5a30f7 | ||
|
|
96a6722e65 | ||
|
|
0caf8a8120 | ||
|
|
273403b711 | ||
|
|
f9ecc746a1 | ||
|
|
c641fdf300 | ||
|
|
f902142fee | ||
|
|
37ef64224e | ||
|
|
9e306eff1e | ||
|
|
37450ef979 | ||
|
|
0fe1cbc888 | ||
|
|
2e746320cf | ||
|
|
13a40a237a | ||
|
|
46e0687bd7 | ||
|
|
b8978fd29c | ||
|
|
cc550dd208 |
27 changed files with 288 additions and 186 deletions
|
|
@ -104,6 +104,7 @@
|
|||
0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; };
|
||||
0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */; };
|
||||
0C8DC35829CE2ACA008A83AD /* SourceSettingsMethodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35729CE2ACA008A83AD /* SourceSettingsMethodView.swift */; };
|
||||
0C93DED82CF80101009EA8D2 /* SettingsDebridLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C93DED72CF80101009EA8D2 /* SettingsDebridLinkView.swift */; };
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */; };
|
||||
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsPluginListView.swift */; };
|
||||
0CA05459288EE9E600850554 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* PluginManager.swift */; };
|
||||
|
|
@ -259,6 +260,7 @@
|
|||
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>"; };
|
||||
0C8DC35729CE2ACA008A83AD /* SourceSettingsMethodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsMethodView.swift; sourceTree = "<group>"; };
|
||||
0C93DED72CF80101009EA8D2 /* SettingsDebridLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDebridLinkView.swift; sourceTree = "<group>"; };
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionPickerView.swift; sourceTree = "<group>"; };
|
||||
0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = "<group>"; };
|
||||
0CA05458288EE9E600850554 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -555,6 +557,7 @@
|
|||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||
0C6771FD29B521F1005D38D2 /* SettingsDebridInfoView.swift */,
|
||||
0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */,
|
||||
0C93DED72CF80101009EA8D2 /* SettingsDebridLinkView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -981,6 +984,7 @@
|
|||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
||||
0C93DED82CF80101009EA8D2 /* SettingsDebridLinkView.swift in Sources */,
|
||||
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */,
|
||||
0CB725322C123E6F0047FC0B /* CloudDownloadView.swift in Sources */,
|
||||
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */,
|
||||
|
|
@ -1122,7 +1126,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8A74DBQ6S3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -1142,7 +1146,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
MARKETING_VERSION = 0.7.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
@ -1158,7 +1162,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8A74DBQ6S3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -1178,7 +1182,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.7.1;
|
||||
MARKETING_VERSION = 0.7.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
let id = "AllDebrid"
|
||||
let abbreviation = "AD"
|
||||
let website = "https://alldebrid.com"
|
||||
let description: String? = "AllDebrid is a debrid service that is used for downloads and media playback. " +
|
||||
"You must pay to access this service. \n\n" +
|
||||
"It is not recommended to use this service since media cache checks are not possible via the API. " +
|
||||
"Ferrite's instant availability solely looks at a user's magnet library. \n\n" +
|
||||
"If you must use this service, it is recommended to download search results manually using the context menu. \n\n" +
|
||||
"This service does not inform if a magnet link is a batch before downloading."
|
||||
|
||||
let cachedStatus: [String] = ["Ready"]
|
||||
var authTask: Task<Void, Error>?
|
||||
|
||||
|
|
@ -81,7 +88,7 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
URLQueryItem(name: "pin", value: pin)
|
||||
]
|
||||
|
||||
let request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/pin/check", queryItems: queryItems))
|
||||
let request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/pin/check", queryItems: queryItems))
|
||||
|
||||
// Timer to poll AD API for key
|
||||
authTask = Task {
|
||||
|
|
@ -192,31 +199,22 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
if sendMagnets.isEmpty {
|
||||
return
|
||||
}
|
||||
// Fetch the user magnets to the latest version
|
||||
try await getUserMagnets()
|
||||
|
||||
let queryItems = sendMagnets.map { URLQueryItem(name: "magnets[]", value: $0.hash) }
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/instant", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<InstantAvailabilityResponse>.self, from: data).data
|
||||
|
||||
let filteredMagnets = rawResponse.magnets.filter { $0.instant == true && $0.files != nil }
|
||||
let availableHashes = filteredMagnets.map { magnetResp in
|
||||
// Force unwrap is OK here since the filter caught any nil values
|
||||
let files = magnetResp.files!.enumerated().map { index, magnetFile in
|
||||
DebridIAFile(id: index, name: magnetFile.name)
|
||||
for cloudMagnet in cloudMagnets {
|
||||
if cachedStatus.contains(cloudMagnet.status),
|
||||
sendMagnets.contains(where: { $0.hash == cloudMagnet.hash })
|
||||
{
|
||||
IAValues.append(
|
||||
DebridIA(
|
||||
magnet: Magnet(hash: cloudMagnet.hash, link: nil),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: []
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return DebridIA(
|
||||
magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
)
|
||||
}
|
||||
|
||||
IAValues += availableHashes
|
||||
}
|
||||
|
||||
// MARK: - Downloading
|
||||
|
|
@ -225,19 +223,49 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
func getRestrictedFile(magnet: Magnet, ia: DebridIA?, iaFile: DebridIAFile?) async throws -> (restrictedFile: DebridIAFile?, newIA: DebridIA?) {
|
||||
let selectedMagnetId: String
|
||||
|
||||
if let existingMagnet = cloudMagnets.first(where: { $0.hash == magnet.hash && cachedStatus.contains($0.status) }) {
|
||||
if let existingMagnet = cloudMagnets.first(where: {
|
||||
$0.hash == magnet.hash && cachedStatus.contains($0.status)
|
||||
}) {
|
||||
selectedMagnetId = existingMagnet.id
|
||||
} else {
|
||||
let magnetId = try await addMagnet(magnet: magnet)
|
||||
selectedMagnetId = String(magnetId)
|
||||
}
|
||||
|
||||
let lockedLink = try await fetchMagnetStatus(
|
||||
let rawResponse = try await fetchMagnetStatus(
|
||||
magnetId: selectedMagnetId,
|
||||
selectedIndex: iaFile?.id ?? 0
|
||||
)
|
||||
guard let magnets = rawResponse.magnets[safe: 0] else {
|
||||
throw DebridError.EmptyUserMagnets
|
||||
}
|
||||
|
||||
return (lockedLink, nil)
|
||||
// Batches require an unrestrict from the user
|
||||
if magnets.links.count > 1, iaFile == nil {
|
||||
var copiedIA = ia
|
||||
|
||||
copiedIA?.files = magnets.links.enumerated().compactMap { index, file in
|
||||
DebridIAFile(
|
||||
id: index,
|
||||
name: file.filename,
|
||||
streamUrlString: file.link
|
||||
)
|
||||
}
|
||||
|
||||
return (nil, copiedIA)
|
||||
}
|
||||
|
||||
if let cloudMagnetFile = magnets.links[safe: iaFile?.id ?? 0] {
|
||||
let restrictedFile = DebridIAFile(
|
||||
id: 0,
|
||||
name: cloudMagnetFile.filename,
|
||||
streamUrlString: cloudMagnetFile.link
|
||||
)
|
||||
|
||||
return (restrictedFile, nil)
|
||||
} else {
|
||||
throw DebridError.EmptyUserMagnets
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a magnet link to the user's AD account
|
||||
|
|
@ -246,7 +274,7 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
throw DebridError.FailedRequest(description: "The magnet link is invalid")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload"))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/magnet/upload"))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
|
|
@ -261,27 +289,26 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
let rawResponse = try jsonDecoder.decode(ADResponse<AddMagnetResponse>.self, from: data).data
|
||||
|
||||
if let magnet = rawResponse.magnets[safe: 0] {
|
||||
if !magnet.ready {
|
||||
throw DebridError.IsCaching
|
||||
}
|
||||
|
||||
return magnet.id
|
||||
} else {
|
||||
throw DebridError.InvalidResponse
|
||||
}
|
||||
}
|
||||
|
||||
func fetchMagnetStatus(magnetId: String, selectedIndex: Int?) async throws -> DebridIAFile {
|
||||
func fetchMagnetStatus(magnetId: String, selectedIndex: Int?) async throws -> MagnetStatusResponse {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "id", value: magnetId)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/magnet/status", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<MagnetStatusResponse>.self, from: data).data
|
||||
|
||||
// Better to fetch no link at all than the wrong link
|
||||
if let cloudMagnetFile = rawResponse.magnets[safe: 0]?.links[safe: selectedIndex ?? -1] {
|
||||
return DebridIAFile(id: 0, name: cloudMagnetFile.filename, streamUrlString: cloudMagnetFile.link)
|
||||
} else {
|
||||
throw DebridError.EmptyUserMagnets
|
||||
}
|
||||
return rawResponse
|
||||
}
|
||||
|
||||
// Known as unlockLink in AD's API
|
||||
|
|
@ -289,7 +316,7 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
let queryItems = [
|
||||
URLQueryItem(name: "link", value: restrictedFile.streamUrlString)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/link/unlock", queryItems: queryItems))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/link/unlock", queryItems: queryItems))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: "unlockLink")
|
||||
let rawResponse = try jsonDecoder.decode(ADResponse<UnlockLinkResponse>.self, from: data).data
|
||||
|
|
@ -301,7 +328,7 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
let queryItems = [
|
||||
URLQueryItem(name: "links[]", value: link)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links/save", queryItems: queryItems))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/user/links/save", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
|
@ -309,7 +336,7 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
// MARK: - Cloud methods
|
||||
|
||||
func getUserMagnets() async throws {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/status"))
|
||||
var request = try URLRequest(url: 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
|
||||
|
|
@ -333,13 +360,13 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
let queryItems = [
|
||||
URLQueryItem(name: "id", value: cloudMagnetId)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/magnet/delete", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
func getUserDownloads() async throws {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links"))
|
||||
var request = try URLRequest(url: 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
|
||||
|
|
@ -362,7 +389,7 @@ class AllDebrid: PollingDebridSource, ObservableObject {
|
|||
let queryItems = [
|
||||
URLQueryItem(name: "link", value: downloadId)
|
||||
]
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/user/links/delete", queryItems: queryItems))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/user/links/delete", queryItems: queryItems))
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class OffCloud: DebridSource, ObservableObject {
|
|||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cache"))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/cache"))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
|
|
@ -202,7 +202,7 @@ class OffCloud: DebridSource, ObservableObject {
|
|||
|
||||
// Called as "cloud" in offcloud's API
|
||||
private func offcloudDownload(magnet: Magnet) async throws -> CloudDownloadResponse {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cloud"))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/cloud"))
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ class OffCloud: DebridSource, ObservableObject {
|
|||
}
|
||||
|
||||
private func cloudExplore(requestId: String) async throws -> CloudExploreResponse {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cloud/explore/\(requestId)"))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/cloud/explore/\(requestId)"))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: "cloudExplore")
|
||||
let rawResponse = try jsonDecoder.decode(CloudExploreResponse.self, from: data)
|
||||
|
|
@ -245,7 +245,7 @@ class OffCloud: DebridSource, ObservableObject {
|
|||
func deleteUserDownload(downloadId: String) {}
|
||||
|
||||
func getUserMagnets() async throws {
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(baseApiUrl)/cloud/history"))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(baseApiUrl)/cloud/history"))
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: "cloudHistory")
|
||||
let rawResponse = try jsonDecoder.decode([CloudHistoryResponse].self, from: data)
|
||||
|
|
@ -271,7 +271,7 @@ class OffCloud: DebridSource, ObservableObject {
|
|||
throw DebridError.InvalidPostBody
|
||||
}
|
||||
|
||||
var request = URLRequest(url: try buildRequestURL(urlString: "\(website)/cloud/remove/\(cloudMagnetId)"))
|
||||
var request = try URLRequest(url: buildRequestURL(urlString: "\(website)/cloud/remove/\(cloudMagnetId)"))
|
||||
try await performRequest(request: &request, requestName: "cloudRemove")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ class RealDebrid: PollingDebridSource, ObservableObject {
|
|||
let id = "RealDebrid"
|
||||
let abbreviation = "RD"
|
||||
let website = "https://real-debrid.com"
|
||||
let description: String? = "RealDebrid is a debrid service that is used for downloads and media playback. " +
|
||||
"You must pay to access this service. \n\n" +
|
||||
"It is not recommended to use this service since media cache checks are not possible via the API. " +
|
||||
"Ferrite's instant availability solely looks at a user's magnet library. \n\n" +
|
||||
"If you must use this service, it is recommended to download search results manually using the context menu. \n\n" +
|
||||
"This service does not inform if a magnet link is a batch before downloading."
|
||||
|
||||
let cachedStatus: [String] = ["downloaded"]
|
||||
var authTask: Task<Void, Error>?
|
||||
|
||||
|
|
@ -245,7 +252,8 @@ class RealDebrid: PollingDebridSource, ObservableObject {
|
|||
|
||||
// MARK: - Instant availability
|
||||
|
||||
// Checks if the magnet is streamable on RD
|
||||
// Post-API changes
|
||||
// Use user magnets to check for IA instead
|
||||
func instantAvailability(magnets: [Magnet]) async throws {
|
||||
let now = Date().timeIntervalSince1970
|
||||
|
||||
|
|
@ -262,61 +270,21 @@ class RealDebrid: PollingDebridSource, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
if sendMagnets.isEmpty {
|
||||
return
|
||||
}
|
||||
// Fetch the user magnets to the latest version
|
||||
try await getUserMagnets()
|
||||
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(sendMagnets.compactMap(\.hash).joined(separator: "/"))")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
|
||||
let rawResponseDict = try jsonDecoder.decode([String: InstantAvailabilityResponse].self, from: data)
|
||||
|
||||
for (hash, response) in rawResponseDict {
|
||||
guard let data = response.data else {
|
||||
continue
|
||||
}
|
||||
|
||||
if data.rd.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle files array
|
||||
let batches = data.rd.map { fileDict in
|
||||
let batchFiles: [RealDebrid.IABatchFile] = fileDict.map { key, value in
|
||||
// Force unwrapped ID. Is safe because ID is guaranteed on a successful response
|
||||
RealDebrid.IABatchFile(id: Int(key)!, fileName: value.filename)
|
||||
}.sorted(by: { $0.id < $1.id })
|
||||
|
||||
return RealDebrid.IABatch(files: batchFiles)
|
||||
}
|
||||
|
||||
var files: [DebridIAFile] = []
|
||||
|
||||
for batch in batches {
|
||||
let batchFileIds = batch.files.map(\.id)
|
||||
|
||||
for batchFile in batch.files {
|
||||
if !files.contains(where: { $0.id == batchFile.id }) {
|
||||
files.append(
|
||||
DebridIAFile(
|
||||
id: batchFile.id,
|
||||
name: batchFile.fileName,
|
||||
batchIds: batchFileIds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TTL: 5 minutes
|
||||
IAValues.append(
|
||||
DebridIA(
|
||||
magnet: Magnet(hash: hash, link: nil),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files
|
||||
for cloudMagnet in cloudMagnets {
|
||||
if cachedStatus.contains(cloudMagnet.status),
|
||||
sendMagnets.contains(where: { $0.hash == cloudMagnet.hash })
|
||||
{
|
||||
IAValues.append(
|
||||
DebridIA(
|
||||
magnet: Magnet(hash: cloudMagnet.hash, link: nil),
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: []
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,7 +296,9 @@ class RealDebrid: PollingDebridSource, ObservableObject {
|
|||
|
||||
do {
|
||||
// Don't queue a new job if the magnet already exists in the user's library
|
||||
if let existingCloudMagnet = cloudMagnets.first(where: { $0.hash == magnet.hash && $0.status == "downloaded" }) {
|
||||
if let existingCloudMagnet = cloudMagnets.first(where: {
|
||||
$0.hash == magnet.hash && cachedStatus.contains($0.status)
|
||||
}) {
|
||||
selectedMagnetId = existingCloudMagnet.id
|
||||
} else {
|
||||
selectedMagnetId = try await addMagnet(magnet: magnet)
|
||||
|
|
@ -336,12 +306,33 @@ class RealDebrid: PollingDebridSource, ObservableObject {
|
|||
try await selectFiles(debridID: selectedMagnetId, fileIds: iaFile?.batchIds ?? [])
|
||||
}
|
||||
|
||||
// RealDebrid has 1 as the first ID for a file
|
||||
let restrictedFile = try await torrentInfo(
|
||||
debridID: selectedMagnetId,
|
||||
selectedFileId: iaFile?.id ?? 1
|
||||
)
|
||||
let response = try await torrentInfo(debridID: selectedMagnetId)
|
||||
let filteredFiles = response.files.filter { $0.selected == 1 }
|
||||
|
||||
// Need to return this to the user
|
||||
if filteredFiles.count > 1, iaFile == nil {
|
||||
var copiedIA = ia
|
||||
|
||||
copiedIA?.files = response.files.enumerated().compactMap { index, file in
|
||||
DebridIAFile(
|
||||
id: index,
|
||||
name: file.path,
|
||||
streamUrlString: response.links[safe: index]
|
||||
)
|
||||
}
|
||||
|
||||
return (nil, copiedIA)
|
||||
}
|
||||
|
||||
// RealDebrid has 1 as the first ID for a file
|
||||
let selectedFileId = iaFile?.id ?? 1
|
||||
let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedFileId })
|
||||
|
||||
guard let cloudMagnetLink = response.links[safe: linkIndex ?? -1] else {
|
||||
throw DebridError.EmptyUserMagnets
|
||||
}
|
||||
|
||||
let restrictedFile = DebridIAFile(id: 0, name: response.filename, streamUrlString: cloudMagnetLink)
|
||||
return (restrictedFile, nil)
|
||||
} catch {
|
||||
if case DebridError.EmptyUserMagnets = error, !selectedMagnetId.isEmpty {
|
||||
|
|
@ -395,24 +386,19 @@ class RealDebrid: PollingDebridSource, ObservableObject {
|
|||
}
|
||||
|
||||
// Gets the info of a torrent from a given ID
|
||||
func torrentInfo(debridID: String, selectedFileId: Int?) async throws -> DebridIAFile {
|
||||
func torrentInfo(debridID: String) async throws -> TorrentInfoResponse {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
|
||||
let filteredFiles = rawResponse.files.filter { $0.selected == 1 }
|
||||
let linkIndex = filteredFiles.firstIndex(where: { $0.id == selectedFileId })
|
||||
|
||||
// Let the user know if a magnet is downloading
|
||||
if let cloudMagnetLink = rawResponse.links[safe: linkIndex ?? -1], rawResponse.status == "downloaded" {
|
||||
return DebridIAFile(
|
||||
id: 0,
|
||||
name: rawResponse.filename,
|
||||
streamUrlString: cloudMagnetLink
|
||||
)
|
||||
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
|
||||
switch rawResponse.status {
|
||||
case "downloaded":
|
||||
return rawResponse
|
||||
case "downloading", "queued":
|
||||
throw DebridError.IsCaching
|
||||
} else {
|
||||
default:
|
||||
throw DebridError.EmptyUserMagnets
|
||||
}
|
||||
}
|
||||
|
|
@ -448,7 +434,7 @@ class RealDebrid: PollingDebridSource, ObservableObject {
|
|||
fileName: response.filename,
|
||||
status: response.status,
|
||||
hash: response.hash,
|
||||
links: response.links
|
||||
links: [response.id]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Array.swift
|
||||
// Set.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/26/22.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<string>Ferrite</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ferrite://</string>
|
||||
<string>ferrite</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MultipartFormDataRequest.swift
|
||||
// FormDataBody.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 6/12/24.
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ class DebridManager: ObservableObject {
|
|||
|
||||
return true
|
||||
} else {
|
||||
logManager?.error("DebridManager: Could not find the associated \(selectedSource.id) entry for magnet hash \(magnetHash)")
|
||||
logManager?.warn("DebridManager: Could not find the associated \(selectedSource.id) entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -323,6 +323,11 @@ class DebridManager: ObservableObject {
|
|||
func fetchDebridDownload(magnet: Magnet?, cloudInfo: String? = nil) async {
|
||||
defer {
|
||||
logManager?.hideIndeterminateToast()
|
||||
|
||||
if !requiresUnrestrict {
|
||||
clearSelectedDebridItems()
|
||||
}
|
||||
|
||||
currentDebridTask = nil
|
||||
}
|
||||
|
||||
|
|
@ -336,6 +341,9 @@ class DebridManager: ObservableObject {
|
|||
}
|
||||
|
||||
do {
|
||||
// Cleanup beforehand
|
||||
requiresUnrestrict = false
|
||||
|
||||
if let cloudInfo {
|
||||
downloadUrl = try await debridSource.checkUserDownloads(link: cloudInfo) ?? ""
|
||||
return
|
||||
|
|
@ -384,6 +392,7 @@ class DebridManager: ObservableObject {
|
|||
defer {
|
||||
logManager?.hideIndeterminateToast()
|
||||
requiresUnrestrict = false
|
||||
clearSelectedDebridItems()
|
||||
currentDebridTask = nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// ToastViewModel.swift
|
||||
// LoggingManager.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/19/22.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SourceManager.swift
|
||||
// PluginManager.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
|
|
|
|||
|
|
@ -21,13 +21,12 @@ struct IndeterminateProgressView: View {
|
|||
.foregroundColor(Color.accentColor)
|
||||
.frame(width: reader.size.width * 0.26, height: 6)
|
||||
.clipShape(Capsule())
|
||||
|
||||
.offset(x: -reader.size.width * 0.6, y: 0)
|
||||
.offset(x: reader.size.width * 1.2 * self.offset, y: 0)
|
||||
.animation(.default.repeatForever().speed(0.5), value: self.offset)
|
||||
.offset(x: reader.size.width * 1.2 * offset, y: 0)
|
||||
.animation(.default.repeatForever().speed(0.5), value: offset)
|
||||
.onAppear {
|
||||
withAnimation {
|
||||
self.offset = 1
|
||||
offset = 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,20 +56,27 @@ struct BookmarksView: View {
|
|||
.frame(height: 15)
|
||||
}
|
||||
.task {
|
||||
if !debridManager.enabledDebrids.isEmpty {
|
||||
let magnets = bookmarks.compactMap {
|
||||
if let magnetHash = $0.magnetHash {
|
||||
return Magnet(hash: magnetHash, link: $0.magnetLink)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
await matchAgainstIA()
|
||||
}
|
||||
.refreshable {
|
||||
await matchAgainstIA()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPredicate() {
|
||||
bookmarks.nsPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText)
|
||||
}
|
||||
|
||||
func matchAgainstIA() async {
|
||||
if !debridManager.enabledDebrids.isEmpty {
|
||||
let magnets = bookmarks.compactMap {
|
||||
if let magnetHash = $0.magnetHash {
|
||||
return Magnet(hash: magnetHash, link: $0.magnetLink)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
await debridManager.populateDebridIA(magnets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// InstalledSourceButtonView.swift
|
||||
// InstalledPluginButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/5/22.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SourceCatalogButtonView.swift
|
||||
// PluginCatalogButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/5/22.
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ struct PluginInfoMetaView<P: Plugin>: View {
|
|||
Group {
|
||||
Text("ID: \(selectedPlugin.id)")
|
||||
|
||||
if let pluginList = pluginLists.first(where: { $0.id == selectedPlugin.listId })
|
||||
{
|
||||
if let pluginList = pluginLists.first(where: { $0.id == selectedPlugin.listId }) {
|
||||
Text("List: \(pluginList.name)")
|
||||
Text("List ID: \(pluginList.id.uuidString)")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// PluginTagView.swift
|
||||
// PluginTagsView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 2/7/23.
|
||||
|
|
|
|||
|
|
@ -37,29 +37,7 @@ struct SearchResultButtonView: View {
|
|||
case .full:
|
||||
if debridManager.selectDebridResult(magnet: result.magnet) {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchDebridDownload(magnet: result.magnet)
|
||||
|
||||
// Bump to batch
|
||||
if debridManager.requiresUnrestrict {
|
||||
navModel.selectedHistoryInfo = historyEntry
|
||||
navModel.currentChoiceSheet = .batch
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyEntry.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyEntry, performSave: true)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
|
||||
if navModel.currentChoiceSheet != .action {
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
await downloadToDebrid()
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
|
|
@ -121,6 +99,33 @@ struct SearchResultButtonView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
if debridManager.currentDebridTask == nil {
|
||||
let foundIAResult = debridManager.selectDebridResult(magnet: result.magnet)
|
||||
|
||||
// Add a fake IA because we don't know if the magnet is cached at this point
|
||||
if !foundIAResult {
|
||||
debridManager.selectedDebridItem = DebridIA(
|
||||
magnet: result.magnet,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970,
|
||||
files: []
|
||||
)
|
||||
}
|
||||
|
||||
debridManager.currentDebridTask = Task {
|
||||
await downloadToDebrid()
|
||||
|
||||
// Re-populate the IA cache if a result wasn't initially found
|
||||
if !foundIAResult {
|
||||
await debridManager.populateDebridIA([result.magnet])
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Download to Debrid")
|
||||
Image(systemName: "arrow.down.circle")
|
||||
}
|
||||
}
|
||||
.alert("Caching file", isPresented: $debridManager.showDeleteAlert) {
|
||||
Button("Yes", role: .destructive) {
|
||||
|
|
@ -166,4 +171,35 @@ struct SearchResultButtonView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common function to download
|
||||
func downloadToDebrid() async {
|
||||
var historyEntry = HistoryEntryJson(
|
||||
name: result.title,
|
||||
source: result.source
|
||||
)
|
||||
|
||||
await debridManager.fetchDebridDownload(magnet: result.magnet)
|
||||
navModel.selectedTitle = result.title ?? ""
|
||||
|
||||
if debridManager.requiresUnrestrict {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyEntry.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyEntry, performSave: true)
|
||||
|
||||
pluginManager.runDefaultAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
navModel: navModel
|
||||
)
|
||||
|
||||
if navModel.currentChoiceSheet != .action {
|
||||
debridManager.downloadUrl = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SearchResultRDView.swift
|
||||
// SearchResultInfoView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/26/22.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// DefaultActionsPickerViews.swift
|
||||
// DefaultActionPickerView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/11/22.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SourceListEditorView.swift
|
||||
// PluginListEditorView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SettingsSourceListView.swift
|
||||
// SettingsPluginListView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// DebridInfoView.swift
|
||||
// SettingsDebridInfoView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 3/5/23.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// SettingsDebridLinkView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 11/27/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsDebridLinkView: View {
|
||||
var debridSource: DebridSource
|
||||
|
||||
// TODO: Use a roundabout state for now
|
||||
@State private var isLoggedIn = false
|
||||
|
||||
var body: some View {
|
||||
NavigationLink {
|
||||
SettingsDebridInfoView(debridSource: debridSource)
|
||||
} label: {
|
||||
HStack {
|
||||
Text(debridSource.id)
|
||||
Spacer()
|
||||
Text(isLoggedIn ? "Enabled" : "Disabled")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isLoggedIn = debridSource.isLoggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,16 +47,7 @@ struct SettingsView: View {
|
|||
Form {
|
||||
Section(header: InlineHeader("Debrid services")) {
|
||||
ForEach(debridManager.debridSources, id: \.id) { (debridSource: DebridSource) in
|
||||
NavigationLink {
|
||||
SettingsDebridInfoView(debridSource: debridSource)
|
||||
} label: {
|
||||
HStack {
|
||||
Text(debridSource.id)
|
||||
Spacer()
|
||||
Text(debridSource.isLoggedIn ? "Enabled" : "Disabled")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
SettingsDebridLinkView(debridSource: debridSource)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MagnetChoiceView.swift
|
||||
// ActionChoiceView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/20/22.
|
||||
|
|
@ -143,6 +143,8 @@ struct ActionChoiceView: View {
|
|||
}
|
||||
.onDisappear {
|
||||
debridManager.downloadUrl = ""
|
||||
debridManager.clearSelectedDebridItems()
|
||||
debridManager.requiresUnrestrict = false
|
||||
navModel.selectedTitle = ""
|
||||
navModel.selectedBatchTitle = ""
|
||||
navModel.resultFromCloud = false
|
||||
|
|
@ -153,8 +155,11 @@ struct ActionChoiceView: View {
|
|||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
debridManager.downloadUrl = ""
|
||||
debridManager.clearSelectedDebridItems()
|
||||
debridManager.requiresUnrestrict = false
|
||||
navModel.selectedTitle = ""
|
||||
navModel.selectedBatchTitle = ""
|
||||
navModel.resultFromCloud = false
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ struct BatchChoiceView: View {
|
|||
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.autocorrectionDisabled(!autocorrectSearch)
|
||||
.textInputAutocapitalization(autocorrectSearch ? .sentences : .never)
|
||||
.onDisappear {
|
||||
debridManager.clearSelectedDebridItems()
|
||||
debridManager.requiresUnrestrict = false
|
||||
}
|
||||
.navigationTitle("Select a file")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
<p align="left">
|
||||
<a href="https://testflight.apple.com/join/YohgCnC4"><img src="https://i.imgur.com/A5Kpowu.png" width="200"></a>
|
||||
</p>
|
||||
|
||||
A media search engine for iOS with a plugin API to extend its functionality.
|
||||
|
||||
## Screenshots
|
||||
|
|
|
|||
Loading…
Reference in a new issue