diff options
| author | Fuwn <[email protected]> | 2026-02-23 09:14:04 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-23 13:33:42 -0800 |
| commit | 0f53ed0fc04952fb2fb43518be11c545da535f5b (patch) | |
| tree | 8b16594291ff7a5e7dd289193eaa1998030edefe /Sora/Data/Booru | |
| parent | (no commit message) (diff) | |
| download | sora-testing-0f53ed0fc04952fb2fb43518be11c545da535f5b.tar.xz sora-testing-0f53ed0fc04952fb2fb43518be11c545da535f5b.zip | |
fix: harden danbooru decoding and pagination flow
Diffstat (limited to 'Sora/Data/Booru')
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 53 |
1 files changed, 50 insertions, 3 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index 2459f2d..5753089 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -114,12 +114,14 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng private func fetchPostsWithRetry(url: URL) async -> [BooruPost] { let maxAttempts = 4 + var requestURL = url + var retriedDanbooruWithoutCredentials = false for attempt in 1...maxAttempts { if Task.isCancelled { return [] } do { - let data = try await requestURL(url) + let data = try await self.requestURL(requestURL) if Task.isCancelled { return [] } @@ -149,6 +151,23 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } } catch { if !Task.isCancelled { + if flavor == .danbooru, + !retriedDanbooruWithoutCredentials, + (error as? AFError)?.responseCode == 401, + let unauthenticatedURL = Self.removingDanbooruCredentials(from: requestURL), + unauthenticatedURL != requestURL + { + retriedDanbooruWithoutCredentials = true + requestURL = unauthenticatedURL + + debugPrint( + "BooruManager.fetchPosts(\(attempt)): unauthorized credentials for \(domain)," + + " retrying without API credentials." + ) + + continue + } + self.error = error debugPrint("BooruManager.fetchPosts(\(attempt)): \(error)") @@ -161,6 +180,21 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng return [] } + private static func removingDanbooruCredentials(from url: URL) -> URL? { + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return nil + } + guard let existingQueryItems = components.queryItems else { + return components.url + } + + components.queryItems = existingQueryItems.filter { queryItem in + queryItem.name != "login" && queryItem.name != "api_key" + } + + return components.url + } + func clearCachedPages() { pageCache.removeAllObjects() urlCache.removeAll() @@ -278,9 +312,20 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } // MARK: - Private Methods + private func danbooruPageToken(for page: Int) -> String { + guard page > 1 else { return "1" } + + guard let minimumPostID = posts.lazy.compactMap({ Int($0.id) }).min() else { + return String(page) + } + + return "b\(minimumPostID)" + } + func url(forPosts page: Int, limit: Int, tags: [String]) -> URL? { let tagString = tags.joined(separator: "+") - let cacheKey = "posts_\(page)_\(limit)_\(tagString.hashValue)" + let pageCacheValue = flavor == .danbooru ? danbooruPageToken(for: page) : String(page) + let cacheKey = "posts_\(pageCacheValue)_\(limit)_\(tagString.hashValue)" if let cachedURL = urlCache[cacheKey] { return cachedURL @@ -297,7 +342,8 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng components.path = "/posts.json" var queryItems = [ - URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "page", value: danbooruPageToken(for: page)), + URLQueryItem(name: "limit", value: String(limit)), URLQueryItem(name: "tags", value: tagString), ] @@ -493,6 +539,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng func requestURL(_ url: URL) async throws -> Data { try await AF.request(url, headers: ["User-Agent": userAgent]) + .validate(statusCode: 200..<300) .serializingData() .value } |