summaryrefslogtreecommitdiff
path: root/Sora/Data/Booru
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-23 09:14:04 -0800
committerFuwn <[email protected]>2026-02-23 13:33:42 -0800
commit0f53ed0fc04952fb2fb43518be11c545da535f5b (patch)
tree8b16594291ff7a5e7dd289193eaa1998030edefe /Sora/Data/Booru
parent(no commit message) (diff)
downloadsora-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.swift53
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
}