diff options
| author | Fuwn <[email protected]> | 2025-03-01 20:37:19 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-03-01 20:37:19 -0800 |
| commit | 6c13c3b884c4cd2ef87b09dd9de7b6d3f161f8f4 (patch) | |
| tree | b18857c8ef25a1bcb09ecb133319415d31bde364 /Sora | |
| parent | feat: Development commit (diff) | |
| download | sora-testing-6c13c3b884c4cd2ef87b09dd9de7b6d3f161f8f4.tar.xz sora-testing-6c13c3b884c4cd2ef87b09dd9de7b6d3f161f8f4.zip | |
feat: Development commit
Diffstat (limited to 'Sora')
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 253 | ||||
| -rw-r--r-- | Sora/Data/Booru/BooruProvider.swift | 22 |
2 files changed, 113 insertions, 162 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index 2b52b4b..d7adc5b 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -2,6 +2,7 @@ import SwiftUI @MainActor class BooruManager: ObservableObject { + // MARK: - Published Properties @Published var posts: [BooruPost] = [] @Published var allTags: [BooruTag] = [] @Published var isLoading = false @@ -12,56 +13,34 @@ class BooruManager: ObservableObject { @Published var selectedPost: BooruPost? @Published var flavor: BooruProviderFlavor @Published var domain: String - @Published var postIndexMap: [String: Int] = [:] - private var currentTask: Task<Void, Never>? - let provider: BooruProvider - var tags: [String] { - if searchText.isEmpty { - return [] - } + @Published private(set) var postIndexMap: [String: Int] = [:] + @Published var provider: BooruProvider - return - searchText - .split(separator: " ") - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - } - private var tagsCacheFileURL: URL? { - guard let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first - else { - return nil - } + // MARK: - Private Properties + private var currentTask: Task<Void, Never>? + private let tagsCacheFileURL: URL? = FileManager.default.urls( + for: .cachesDirectory, in: .userDomainMask + ).first? + .appendingPathComponent("\(BooruProvider.safebooru.asFileNameComponent())_tags.json") - return - directory - .appendingPathComponent("\(provider.asFileNameComponent())_tags.json") + // MARK: - Computed Properties + var tags: [String] { + searchText.isEmpty + ? [] + : searchText + .split(separator: " ") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } } + // MARK: - Initialization init(_ provider: BooruProvider) { self.provider = provider self.flavor = BooruProviderFlavor(provider: provider) - - switch provider { - case .yandere: - domain = "yande.re" - - case .konachan: - domain = "konachan.com" - - case .sakugabooru: - domain = "sakugabooru.com" - - case .safebooru: - domain = "safebooru.org" - - case .gelbooru: - domain = "gelbooru.com" - - case .danbooru: - domain = "danbooru.donmai.us" - } + self.domain = provider.domain } + // MARK: - Public Methods func initializeTags() { loadCachedTags() fetchAllTags() @@ -70,82 +49,38 @@ class BooruManager: ObservableObject { func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async { - guard !isLoading else { return } + guard !isLoading, + let url = urlForPosts(page: flavor == .gelbooru ? page - 1 : page, limit: limit, tags: tags) + else { return } + + isLoading = true currentTask?.cancel() currentTask = Task { - isLoading = true - defer { isLoading = false } - if replace { - self.posts = [] - self.currentPage = 1 - } - - guard - let url = urlForPosts( - page: self.flavor == .gelbooru ? page - 1 : page, limit: limit, tags: tags - ) - else { - return - } - do { let (data, _) = try await URLSession.shared.data(from: url) - if Task.isCancelled { return } - - DispatchQueue.main.async { - let newPosts = Array( - Set( - self.flavor == .danbooru - ? DanbooruPostParser(data: data).parse() - : BooruPostXMLParser(data: data, provider: self.provider).parse() - ) - ) - .sorted { lhs, rhs in - lhs.id > rhs.id - } - - if newPosts.isEmpty { - self.endOfData = true - } else { - self.posts += Array(Set(newPosts)) - - self.posts = self.posts.sorted { $0.id > $1.id } - self.postIndexMap.merge( - zip(newPosts.indices, newPosts.map(\.id)).reduce(into: [:]) { result, element in - result[element.1] = element.0 - } - ) { _, new in new } - } - } + guard !Task.isCancelled else { return } + + let newPosts = parsePosts(from: data).sorted { $0.id > $1.id } + + updatePosts(newPosts, replace: replace) } catch { - if (error as? URLError)?.code != .cancelled { - debugPrint("BooruManager.fetchPosts: \(error)") - } + if (error as? URLError)?.code != .cancelled { debugPrint("fetchPosts: \(error)") } } } } func performSearch(settings: SettingsManager? = nil) { - if let settings { - settings.appendToSearchHistory( - BooruSearchQuery( - provider: settings.preferredBooru, - tags: tags, - searchedAt: Date() - ) - ) - } - + settings?.appendToSearchHistory( + BooruSearchQuery(provider: settings!.preferredBooru, tags: tags, searchedAt: Date()) + ) currentTask?.cancel() - currentTask = Task { - await fetchPosts(page: 1, tags: tags, replace: true) - } + Task { await fetchPosts(page: 1, tags: tags, replace: true) } } func loadNextPage() { @@ -154,42 +89,42 @@ class BooruManager: ObservableObject { Task { await fetchPosts(page: currentPage + 1, tags: tags) - DispatchQueue.main.async { - self.currentPage += 1 - } + currentPage += 1 } } func fetchAllTags(limit: Int = 0) { - Task { - guard let url = urlForTags(limit: limit) else { return } + guard let url = urlForTags(limit: limit) else { return } + Task { do { let (data, _) = try await URLSession.shared.data(from: url) - if Task.isCancelled { return } + guard !Task.isCancelled else { return } + + allTags = BooruTagXMLParser(data: data).parse().sorted { $0.count > $1.count } - DispatchQueue.main.async { - self.allTags = BooruTagXMLParser(data: data).parse().sorted { $0.count > $1.count } - self.saveTagsToCache() - self.updateTagsCacheSize() - } + saveTagsToCache() } catch { - if (error as? URLError)?.code != .cancelled { - debugPrint("BooruManager.fetchAllTags: \(error)") - } + if (error as? URLError)?.code != .cancelled { debugPrint("fetchAllTags: \(error)") } } } } + func clearCachedTags() { + try? tagsCacheFileURL.map { url in + try FileManager.default.removeItem(at: url) + updateTagsCacheSize() + } + } + + // MARK: - Private Helpers private func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { let tagString = tags.joined(separator: "+") switch flavor { case .danbooru: - return URL( - string: "https://\(domain)/posts.json?page=\(page)&tags=\(tagString)" - ) + return URL(string: "https://\(domain)/posts.json?page=\(page)&tags=\(tagString)") case .moebooru: return URL(string: "https://\(domain)/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") @@ -205,78 +140,72 @@ class BooruManager: ObservableObject { private func urlForTags(limit: Int) -> URL? { switch flavor { case .moebooru: - URL(string: "https://\(domain)/tag.xml?limit=\(limit)") + return URL(string: "https://\(domain)/tag.xml?limit=\(limit)") case .gelbooru: - URL(string: "https://\(domain)/index.php?page=dapi&s=tag&q=index&limit=\(limit)") + return URL(string: "https://\(domain)/index.php?page=dapi&s=tag&q=index&limit=\(limit)") case .danbooru: - nil + return nil } } - private func saveTagsToCache() { - guard let url = tagsCacheFileURL else { return } - - do { - let data = try JSONEncoder().encode(allTags) + private func parsePosts(from data: Data) -> [BooruPost] { + Array( + Set( + flavor == .danbooru + ? DanbooruPostParser(data: data).parse() + : BooruPostXMLParser(data: data, provider: provider).parse() + ) + ) + } - try data.write(to: url) - updateTagsCacheSize() - } catch { - debugPrint("BooruManager.saveTagsToCache: \(error)") + private func updatePosts(_ newPosts: [BooruPost], replace: Bool) { + if replace { + posts = [] + currentPage = 1 } - } - private func loadCachedTags() { - guard let url = tagsCacheFileURL else { return } + endOfData = newPosts.isEmpty - do { - let data = try Data(contentsOf: url) - let cachedTags = try JSONDecoder().decode([BooruTag].self, from: data) + if !endOfData { + posts = (posts + newPosts).sorted { $0.id > $1.id } - DispatchQueue.main.async { - self.allTags = cachedTags - self.updateTagsCacheSize() - } - } catch { - debugPrint("BooruManager.loadCachedTags: \(error)") + postIndexMap.merge( + Dictionary(uniqueKeysWithValues: newPosts.enumerated().map { ($0.element.id, $0.offset) }) + ) { _, new in new } } } - func clearCachedTags() { - guard let url = tagsCacheFileURL else { return } - - do { - try FileManager.default.removeItem(at: url) + private func saveTagsToCache() { + try? tagsCacheFileURL.map { url in + try JSONEncoder().encode(allTags).write(to: url) updateTagsCacheSize() - } catch { - debugPrint("BooruManager.clearCachedTags: \(error)") } } - func updateTagsCacheSize() { - guard let url = tagsCacheFileURL else { - cacheSize = nil + private func loadCachedTags() { + guard let url = tagsCacheFileURL else { return } + + if let data = try? Data(contentsOf: url), + let tags = try? JSONDecoder().decode([BooruTag].self, from: data) + { + allTags = tags - return + updateTagsCacheSize() } + } - do { - cacheSize = ByteCountFormatter.string( + func updateTagsCacheSize() { + cacheSize = tagsCacheFileURL.flatMap { url in + ByteCountFormatter.string( fromByteCount: Int64( - (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int) ?? 0 + (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int) ?? 0 ), countStyle: .file ) - } catch { - cacheSize = nil - - debugPrint("BooruManager.updateCacheSize: \(error)") } } - deinit { - currentTask?.cancel() - } + deinit { currentTask?.cancel() } } diff --git a/Sora/Data/Booru/BooruProvider.swift b/Sora/Data/Booru/BooruProvider.swift index 5458223..aa3acf3 100644 --- a/Sora/Data/Booru/BooruProvider.swift +++ b/Sora/Data/Booru/BooruProvider.swift @@ -6,6 +6,28 @@ enum BooruProvider: String, CaseIterable, Decodable, Encodable { case sakugabooru = "sakugabooru" case yandere = "yande.re" + var domain: String { + switch self { + case .yandere: + return "yande.re" + + case .konachan: + return "konachan.com" + + case .sakugabooru: + return "sakugabooru.com" + + case .safebooru: + return "safebooru.org" + + case .gelbooru: + return "gelbooru.com" + + case .danbooru: + return "danbooru.donmai.us" + } + } + func asFileNameComponent() -> String { rawValue.lowercased().replacingOccurrences(of: ".", with: "_") } |