summaryrefslogtreecommitdiff
path: root/Sora/Data/Danbooru
diff options
context:
space:
mode:
Diffstat (limited to 'Sora/Data/Danbooru')
-rw-r--r--Sora/Data/Danbooru/DanbooruManager.swift114
-rw-r--r--Sora/Data/Danbooru/DanbooruPost.swift27
-rw-r--r--Sora/Data/Danbooru/DanbooruPostXMLParser.swift98
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
+}