From e42fa12dafe264c665d2574c93b54ddafe7f2e1f Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sat, 22 Feb 2025 00:07:44 -0800 Subject: feat: Development commit --- Sora/Data/Booru/BooruManager.swift | 224 ++++++++++++++++--------------- Sora/Data/Booru/BooruPost.swift | 46 +++---- Sora/Data/Booru/BooruPostFileType.swift | 6 +- Sora/Data/Booru/BooruPostXMLParser.swift | 208 ++++++++++++++-------------- Sora/Data/Booru/BooruProvider.swift | 18 +-- Sora/Data/Booru/BooruTag.swift | 10 +- Sora/Data/Booru/BooruTagXMLParser.swift | 92 +++++++------ 7 files changed, 312 insertions(+), 292 deletions(-) (limited to 'Sora/Data/Booru') diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index dd63367..f86e3a9 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -2,143 +2,151 @@ import SwiftUI @MainActor class BooruManager: ObservableObject { - @Published var posts: [BooruPost] = [] - @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: BooruPost? - #endif - private var currentTask: Task? - let provider: BooruProvider? - var tags: [String] { - if searchText.isEmpty { - return [] - } - - return searchText - .split(separator: " ") - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } + @Published var posts: [BooruPost] = [] + @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: BooruPost? + #endif + private var currentTask: Task? + let provider: BooruProvider? + var tags: [String] { + if searchText.isEmpty { + return [] } - init(_ provider: BooruProvider? = nil) { - self.provider = provider + return + searchText + .split(separator: " ") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + } - fetchAllTags() - } + init(_ provider: BooruProvider? = nil) { + self.provider = provider + + fetchAllTags() + } - func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async { - guard !isLoading else { return } - guard provider != nil else { return } + func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async + { + guard !isLoading else { return } + guard provider != nil else { return } - currentTask?.cancel() + currentTask?.cancel() - currentTask = Task { - isLoading = true + currentTask = Task { + isLoading = true - defer { isLoading = false } + defer { isLoading = false } - if replace { - self.posts = [] - self.currentPage = 1 - } + if replace { + self.posts = [] + self.currentPage = 1 + } - guard let url = urlForPosts(page: self.provider == .safebooru ? page - 1 : page, limit: limit, tags: tags) else { - return - } + guard + let url = urlForPosts( + page: self.provider == .safebooru ? page - 1 : page, limit: limit, tags: tags) + else { + return + } - do { - let (data, _) = try await URLSession.shared.data(from: url) + do { + let (data, _) = try await URLSession.shared.data(from: url) - if Task.isCancelled { return } + if Task.isCancelled { return } - DispatchQueue.main.async { - let newPosts = Array(Set(BooruPostXMLParser(data: data).parse())).sorted { $0.id > $1.id } + DispatchQueue.main.async { + let newPosts = Array(Set(BooruPostXMLParser(data: data).parse())).sorted { $0.id > $1.id } - if newPosts.isEmpty { - self.endOfData = true - } else { - self.posts += Array(Set(newPosts)) - } - } - } catch { - if (error as? URLError)?.code != .cancelled { - #if DEBUG - print("fetchPosts: \(error)") - #endif - } - } + if newPosts.isEmpty { + self.endOfData = true + } else { + self.posts += Array(Set(newPosts)) + } } + } catch { + if (error as? URLError)?.code != .cancelled { + #if DEBUG + print("fetchPosts: \(error)") + #endif + } + } } + } - func performSearch() { - currentTask?.cancel() + func performSearch() { + currentTask?.cancel() - currentTask = Task { - await fetchPosts(page: 1, tags: tags, replace: true) - } + currentTask = Task { + await fetchPosts(page: 1, tags: tags, replace: true) } + } - func loadNextPage() { - guard !isLoading else { return } + func loadNextPage() { + guard !isLoading else { return } - Task { - await fetchPosts(page: currentPage + 1, tags: tags) + Task { + await fetchPosts(page: currentPage + 1, tags: tags) - DispatchQueue.main.async { - self.currentPage += 1 - } - } + DispatchQueue.main.async { + self.currentPage += 1 + } } + } - func fetchAllTags(limit: Int = 100_000) { - guard provider != nil else { return } + func fetchAllTags(limit: Int = 100_000) { + guard provider != nil else { return } - Task { - guard let url = urlForTags(limit: limit) else { return } + Task { + guard let url = urlForTags(limit: limit) else { return } - do { - let (data, _) = try await URLSession.shared.data(from: url) + do { + let (data, _) = try await URLSession.shared.data(from: url) - if Task.isCancelled { return } + 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("fetchAllTags: \(error)") - #endif - } - } + DispatchQueue.main.async { + self.allTags = BooruTagXMLParser(data: data).parse().sorted { $0.count > $1.count } } - } - - private func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { - let tagString = tags.joined(separator: "+") - - switch provider { - case .yandere: - return URL(string: "https://yande.re/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") - case .safebooru: - return URL(string: "https://safebooru.org/index.php?page=dapi&s=post&q=index&pid=\(page)&limit=\(limit)&tags=\(tagString)") - default: - return nil + } catch { + if (error as? URLError)?.code != .cancelled { + #if DEBUG + print("fetchAllTags: \(error)") + #endif } + } } - - private func urlForTags(limit: Int) -> URL? { - switch provider { - case .yandere: - URL(string: "https://yande.re/tag.xml?limit=\(limit)") - case .safebooru: - URL(string: "https://safebooru.org/index.php?page=dapi&s=tag&q=index&limit=\(limit)") - default: - nil - } + } + + private func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { + let tagString = tags.joined(separator: "+") + + switch provider { + case .yandere: + return URL(string: "https://yande.re/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") + case .safebooru: + return URL( + string: + "https://safebooru.org/index.php?page=dapi&s=post&q=index&pid=\(page)&limit=\(limit)&tags=\(tagString)" + ) + default: + return nil + } + } + + private func urlForTags(limit: Int) -> URL? { + switch provider { + case .yandere: + URL(string: "https://yande.re/tag.xml?limit=\(limit)") + case .safebooru: + URL(string: "https://safebooru.org/index.php?page=dapi&s=tag&q=index&limit=\(limit)") + default: + nil } + } } diff --git a/Sora/Data/Booru/BooruPost.swift b/Sora/Data/Booru/BooruPost.swift index 59b8952..7f995a3 100644 --- a/Sora/Data/Booru/BooruPost.swift +++ b/Sora/Data/Booru/BooruPost.swift @@ -1,27 +1,27 @@ import Foundation struct BooruPost: 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: Date - let status: String - let source: String - let hasNotes: Bool - let hasComments: Bool - let previewWidth: Int - let previewHeight: Int + 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: Date + let status: String + let source: String + let hasNotes: Bool + let hasComments: Bool + let previewWidth: Int + let previewHeight: Int } diff --git a/Sora/Data/Booru/BooruPostFileType.swift b/Sora/Data/Booru/BooruPostFileType.swift index 71d0352..62900db 100644 --- a/Sora/Data/Booru/BooruPostFileType.swift +++ b/Sora/Data/Booru/BooruPostFileType.swift @@ -1,5 +1,5 @@ enum BooruPostFileType: String, CaseIterable { - case original - case sample - case preview + case original + case sample + case preview } diff --git a/Sora/Data/Booru/BooruPostXMLParser.swift b/Sora/Data/Booru/BooruPostXMLParser.swift index 30207bc..79827aa 100644 --- a/Sora/Data/Booru/BooruPostXMLParser.swift +++ b/Sora/Data/Booru/BooruPostXMLParser.swift @@ -1,116 +1,122 @@ import Foundation class BooruPostXMLParser: NSObject, XMLParserDelegate { - private var posts: [BooruPost] = [] - private var currentPost: BooruPost? - private var parser: XMLParser - - init(data: Data) { - parser = XMLParser(data: data) - - super.init() - - parser.delegate = self + private var posts: [BooruPost] = [] + private var currentPost: BooruPost? + private var parser: XMLParser + + init(data: Data) { + parser = XMLParser(data: data) + + super.init() + + parser.delegate = self + } + + func parse() -> [BooruPost] { + 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 previewWidthStr = attributeDict["preview_width"], + let previewWidth = Int(previewWidthStr), + let previewHeightStr = attributeDict["preview_height"], + let previewHeight = Int(previewHeightStr) + else { + return + } + + let hasNotesStr = attributeDict["has_notes"] ?? "false" + let hasCommentsStr = attributeDict["has_comments"] ?? "false" + + currentPost = BooruPost( + 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: parseCreatedAt(createdAt)!, + status: status, + source: source, + hasNotes: hasNotesStr == "true", + hasComments: hasCommentsStr == "true", + previewWidth: previewWidth, + previewHeight: previewHeight + ) } + } - func parse() -> [BooruPost] { - parser.parse() - - return posts - } + func parser( + _: XMLParser, didEndElement elementName: String, namespaceURI _: String?, + qualifiedName _: String? + ) { + if elementName == "post", let post = currentPost { + posts.append(post) - 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 previewWidthStr = attributeDict["preview_width"], - let previewWidth = Int(previewWidthStr), - let previewHeightStr = attributeDict["preview_height"], - let previewHeight = Int(previewHeightStr) - else { - return - } - - let hasNotesStr = attributeDict["has_notes"] ?? "false" - let hasCommentsStr = attributeDict["has_comments"] ?? "false" - - currentPost = BooruPost( - 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: parseCreatedAt(createdAt)!, - status: status, - source: source, - hasNotes: hasNotesStr == "true", - hasComments: hasCommentsStr == "true", - previewWidth: previewWidth, - previewHeight: previewHeight - ) - } + currentPost = nil } + } - 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("parser: \(parseError)") } + #endif - #if DEBUG - func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { - print("parser: \(parseError)") - } - #endif - - func parseCreatedAt(_ input: String) -> Date? { - let dateFormatter = DateFormatter() - - dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" - dateFormatter.locale = Locale(identifier: "en_US_POSIX") + func parseCreatedAt(_ input: String) -> Date? { + let dateFormatter = DateFormatter() - if let date = dateFormatter.date(from: input) { - return date - } + dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") - if let timestamp = Double(input) { - return Date(timeIntervalSince1970: timestamp) - } + if let date = dateFormatter.date(from: input) { + return date + } - return nil + if let timestamp = Double(input) { + return Date(timeIntervalSince1970: timestamp) } + + return nil + } } diff --git a/Sora/Data/Booru/BooruProvider.swift b/Sora/Data/Booru/BooruProvider.swift index 73bb4f2..994e4e3 100644 --- a/Sora/Data/Booru/BooruProvider.swift +++ b/Sora/Data/Booru/BooruProvider.swift @@ -1,13 +1,13 @@ enum BooruProvider: String, CaseIterable, Decodable, Encodable { - case yandere - case safebooru + case yandere + case safebooru - func formatted() -> String { - switch self { - case .yandere: - "yande.re" - case .safebooru: - "Safebooru" - } + func formatted() -> String { + switch self { + case .yandere: + "yande.re" + case .safebooru: + "Safebooru" } + } } diff --git a/Sora/Data/Booru/BooruTag.swift b/Sora/Data/Booru/BooruTag.swift index b98bc75..d603e9f 100644 --- a/Sora/Data/Booru/BooruTag.swift +++ b/Sora/Data/Booru/BooruTag.swift @@ -1,9 +1,9 @@ import Foundation struct BooruTag: Identifiable, Hashable { - let id: String - let name: String - let count: Int - let type: Int - let ambiguous: Bool + let id: String + let name: String + let count: Int + let type: Int + let ambiguous: Bool } diff --git a/Sora/Data/Booru/BooruTagXMLParser.swift b/Sora/Data/Booru/BooruTagXMLParser.swift index 0dfebc3..bba26c5 100644 --- a/Sora/Data/Booru/BooruTagXMLParser.swift +++ b/Sora/Data/Booru/BooruTagXMLParser.swift @@ -1,58 +1,64 @@ import Foundation class BooruTagXMLParser: NSObject, XMLParserDelegate { - private var tags: [BooruTag] = [] - private var currentTag: BooruTag? - private var parser: XMLParser + private var tags: [BooruTag] = [] + private var currentTag: BooruTag? + private var parser: XMLParser - init(data: Data) { - parser = XMLParser(data: data) + init(data: Data) { + parser = XMLParser(data: data) - super.init() + super.init() - parser.delegate = self - } + parser.delegate = self + } - func parse() -> [BooruTag] { - parser.parse() + func parse() -> [BooruTag] { + parser.parse() - return tags - } + return tags + } - func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { - if elementName == "tag" { - guard let id = attributeDict["id"], - let name = attributeDict["name"], - let countStr = attributeDict["count"], - let count = Int(countStr), - let typeStr = attributeDict["type"], - let type = Int(typeStr), - let ambiguousStr = attributeDict["ambiguous"] - else { - return - } - - currentTag = BooruTag( - id: id, - name: name, - count: count, - type: type, - ambiguous: ambiguousStr == "true" - ) - } + func parser( + _: XMLParser, didStartElement elementName: String, namespaceURI _: String?, + qualifiedName _: String?, attributes attributeDict: [String: String] = [:] + ) { + if elementName == "tag" { + guard let id = attributeDict["id"], + let name = attributeDict["name"], + let countStr = attributeDict["count"], + let count = Int(countStr), + let typeStr = attributeDict["type"], + let type = Int(typeStr), + let ambiguousStr = attributeDict["ambiguous"] + else { + return + } + + currentTag = BooruTag( + id: id, + name: name, + count: count, + type: type, + ambiguous: ambiguousStr == "true" + ) } + } - func parser(_: XMLParser, didEndElement elementName: String, namespaceURI _: String?, qualifiedName _: String?) { - if elementName == "tag", let tag = currentTag { - tags.append(tag) + func parser( + _: XMLParser, didEndElement elementName: String, namespaceURI _: String?, + qualifiedName _: String? + ) { + if elementName == "tag", let tag = currentTag { + tags.append(tag) - currentTag = nil - } + currentTag = nil } + } - #if DEBUG - func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { - print(parseError) - } - #endif + #if DEBUG + func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { + print(parseError) + } + #endif } -- cgit v1.2.3