diff options
Diffstat (limited to 'Sora/Data/Danbooru')
| -rw-r--r-- | Sora/Data/Danbooru/DanbooruManager.swift | 114 | ||||
| -rw-r--r-- | Sora/Data/Danbooru/DanbooruPost.swift | 27 | ||||
| -rw-r--r-- | Sora/Data/Danbooru/DanbooruPostXMLParser.swift | 98 |
3 files changed, 239 insertions, 0 deletions
diff --git a/Sora/Data/Danbooru/DanbooruManager.swift b/Sora/Data/Danbooru/DanbooruManager.swift new file mode 100644 index 0000000..7bbb3a7 --- /dev/null +++ b/Sora/Data/Danbooru/DanbooruManager.swift @@ -0,0 +1,114 @@ +import SwiftUI + +@MainActor +class DanbooruManager: ObservableObject { + @Published var posts: [DanbooruPost] = [] + @Published var allTags: [BooruTag] = [] + @Published var isLoading: Bool = false + @Published var currentPage: Int = 1 + @Published var searchText = "" + @Published var endOfData: Bool = false + #if os(macOS) + @Published var selectedPost: MoebooruPost? + #endif + private var currentTask: Task<Void, Never>? + var tags: [String] { + if searchText.isEmpty { + return [] + } + + return searchText + .split(separator: " ") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + } + + init() { + fetchAllTags() + } + + func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async { + guard !isLoading else { return } + + currentTask?.cancel() + + currentTask = Task { + isLoading = true + + defer { isLoading = false } + + if replace { + self.posts = [] + self.currentPage = 1 + } + + guard let url = URL(string: "https://safebooru.org/index.php?page=dapi&s=post&q=index&pid=\(page)&limit=\(limit)&tags=\(tags.joined(separator: "+"))") else { return } + + do { + let (data, _) = try await URLSession.shared.data(from: url) + + if Task.isCancelled { return } + + DispatchQueue.main.async { + let newPosts = Array(Set(DanbooruPostXMLParser(data: data).parse())).sorted { $0.id > $1.id } + + if newPosts == [] { + self.endOfData = true + } else { + self.posts += newPosts + } + } + } catch { + if (error as? URLError)?.code != .cancelled { + #if DEBUG + print(error) + #endif + } + } + } + } + + func performSearch() { + Task { + await fetchPosts( + page: 1, + tags: tags, + replace: true + ) + } + } + + func loadNextPage() { + guard !isLoading else { return } + + Task { + await fetchPosts(page: currentPage + 1, tags: tags) + + DispatchQueue.main.async { + self.currentPage += 1 + } + } + } + + func fetchAllTags(limit: Int = 100_000) { + Task { + guard let url = URL(string: "https://safebooru.org/index.php?page=dapi&s=tag&q=index&limit=\(limit)") else { return } + + do { + let (data, _) = try await URLSession.shared.data(from: url) + + if Task.isCancelled { return } + + DispatchQueue.main.async { + self.allTags = (BooruTagXMLParser(data: data).parse()).sorted { $0.count > $1.count } + } + } catch { + if (error as? URLError)?.code != .cancelled { + #if DEBUG + print(error) + #endif + } + } + } + } +} diff --git a/Sora/Data/Danbooru/DanbooruPost.swift b/Sora/Data/Danbooru/DanbooruPost.swift new file mode 100644 index 0000000..17b0d0f --- /dev/null +++ b/Sora/Data/Danbooru/DanbooruPost.swift @@ -0,0 +1,27 @@ +import Foundation + +struct DanbooruPost: Identifiable, Hashable { + let id: String + let height: Int + let score: String + let fileURL: URL + let parentID: String + let sampleURL: URL + let sampleWidth: Int + let sampleHeight: Int + let previewURL: URL + let rating: String + let tags: [String] + let width: Int + let change: String + let md5: String + let creatorID: String + let hasChildren: Bool + let createdAt: String + let status: String + let source: String + let hasNotes: Bool + let hasComments: Bool + let previewWidth: Int + let previewHeight: Int +} diff --git a/Sora/Data/Danbooru/DanbooruPostXMLParser.swift b/Sora/Data/Danbooru/DanbooruPostXMLParser.swift new file mode 100644 index 0000000..3d99465 --- /dev/null +++ b/Sora/Data/Danbooru/DanbooruPostXMLParser.swift @@ -0,0 +1,98 @@ +import Foundation + +class DanbooruPostXMLParser: NSObject, XMLParserDelegate { + private var posts: [DanbooruPost] = [] + private var currentPost: DanbooruPost? + private var parser: XMLParser + + init(data: Data) { + parser = XMLParser(data: data) + + super.init() + + parser.delegate = self + } + + func parse() -> [DanbooruPost] { + parser.parse() + + return posts + } + + func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { + if elementName == "post" { + guard let id = attributeDict["id"], + let heightStr = attributeDict["height"], + let height = Int(heightStr), + let score = attributeDict["score"], + let fileUrl = attributeDict["file_url"], + let parentId = attributeDict["parent_id"], + let sampleUrl = attributeDict["sample_url"], + let sampleWidthStr = attributeDict["sample_width"], + let sampleWidth = Int(sampleWidthStr), + let sampleHeightStr = attributeDict["sample_height"], + let sampleHeight = Int(sampleHeightStr), + let previewUrl = attributeDict["preview_url"], + let rating = attributeDict["rating"], + let tags = attributeDict["tags"], + let widthStr = attributeDict["width"], + let width = Int(widthStr), + let change = attributeDict["change"], + let md5 = attributeDict["md5"], + let creatorId = attributeDict["creator_id"], + let hasChildrenStr = attributeDict["has_children"], + let createdAt = attributeDict["created_at"], + let status = attributeDict["status"], + let source = attributeDict["source"], + let hasNotesStr = attributeDict["has_notes"], + let hasCommentsStr = attributeDict["has_comments"], + let previewWidthStr = attributeDict["preview_width"], + let previewWidth = Int(previewWidthStr), + let previewHeightStr = attributeDict["preview_height"], + let previewHeight = Int(previewHeightStr) + else { + return + } + + currentPost = DanbooruPost( + id: id, + height: height, + score: score, + fileURL: URL(string: fileUrl)!, + parentID: parentId, + sampleURL: URL(string: sampleUrl)!, + sampleWidth: sampleWidth, + sampleHeight: sampleHeight, + previewURL: URL(string: previewUrl)!, + rating: rating, + tags: tags.components(separatedBy: " ").filter { !$0.isEmpty }, + width: width, + change: change, + md5: md5, + creatorID: creatorId, + hasChildren: hasChildrenStr == "true", + createdAt: createdAt, + status: status, + source: source, + hasNotes: hasNotesStr == "true", + hasComments: hasCommentsStr == "true", + previewWidth: previewWidth, + previewHeight: previewHeight + ) + } + } + + func parser(_: XMLParser, didEndElement elementName: String, namespaceURI _: String?, qualifiedName _: String?) { + if elementName == "post", let post = currentPost { + posts.append(post) + + currentPost = nil + } + } + + #if DEBUG + func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { + print(parseError) + } + #endif +} |