diff options
Diffstat (limited to 'Sora/Data/Booru')
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 152 | ||||
| -rw-r--r-- | Sora/Data/Booru/Post/BooruPostXMLParser.swift | 63 |
2 files changed, 171 insertions, 44 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index f8aa9d0..db4d8f9 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -1,3 +1,5 @@ +// swiftlint:disable file_length + import Alamofire import SwiftUI @@ -27,6 +29,8 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng private let userAgent: String private var urlCache: [String: URL] = [:] private var lastPostCount = 0 + private var xmlParserPool: [BooruPostXMLParser] = [] + private let parserPoolLock = NSLock() // MARK: - Computed Properties var tags: [String] { @@ -75,8 +79,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng let pageValue = flavor == .gelbooru ? page - 1 : page guard let url = urlForPosts(page: pageValue, limit: limit, tags: tags) else { return } - - let cacheKey = "\(url.absoluteString)_\(replace)" as NSString // swiftlint:disable:this legacy_objc_type + let cacheKey = "\(url.absoluteString.hashValue)_\(replace)" as NSString // swiftlint:disable:this legacy_objc_type if let cachedEntry = pageCache.object(forKey: cacheKey), !cachedEntry.isExpired @@ -243,7 +246,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng // MARK: - Private Methods func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { let tagString = tags.joined(separator: "+") - let cacheKey = "posts_\(page)_\(limit)_\(tagString)" + let cacheKey = "posts_\(page)_\(limit)_\(tagString.hashValue)" if let cachedURL = urlCache[cacheKey] { return cachedURL @@ -253,23 +256,56 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng switch flavor { case .danbooru: - url = URL(string: "https://\(domain)/posts.json?page=\(page)&tags=\(tagString)") + var components = URLComponents() + + components.scheme = "https" + components.host = domain + components.path = "/posts.json" + components.queryItems = [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "tags", value: tagString), + ] + url = components.url case .moebooru: - url = URL(string: "https://\(domain)/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") + var components = URLComponents() + + components.scheme = "https" + components.host = domain + components.path = "/post.xml" + components.queryItems = [ + URLQueryItem(name: "page", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + URLQueryItem(name: "tags", value: tagString), + ] + url = components.url case .gelbooru: - var urlString = - "https://\(domain)/index.php?page=dapi&s=post&q=index&pid=\(page)&limit=\(limit)&tags=\(tagString)" + var components = URLComponents() + + components.scheme = "https" + components.host = domain + components.path = "/index.php" + + var queryItems = [ + URLQueryItem(name: "page", value: "dapi"), + URLQueryItem(name: "s", value: "post"), + URLQueryItem(name: "q", value: "index"), + URLQueryItem(name: "pid", value: String(page)), + URLQueryItem(name: "limit", value: String(limit)), + URLQueryItem(name: "tags", value: tagString), + ] if let validCredentials = credentials, !validCredentials.apiKey.isEmpty, validCredentials.userID != 0 { - urlString += "&api_key=\(validCredentials.apiKey)&user_id=\(validCredentials.userID)" + queryItems.append(URLQueryItem(name: "api_key", value: validCredentials.apiKey)) + queryItems.append(URLQueryItem(name: "user_id", value: String(validCredentials.userID))) } - url = URL(string: urlString) + components.queryItems = queryItems + url = components.url } if let constructedURL = url { @@ -282,13 +318,33 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng private func urlForTags(limit: Int, order: String = "count") -> URL? { switch flavor { case .moebooru: - return URL(string: "https://\(domain)/tag.xml?limit=\(limit)&order=\(order)") + var components = URLComponents() + + components.scheme = "https" + components.host = domain + components.path = "/tag.xml" + components.queryItems = [ + URLQueryItem(name: "limit", value: String(limit)), + URLQueryItem(name: "order", value: order), + ] + + return components.url case .gelbooru: - return URL( - string: - "https://\(domain)/index.php?page=dapi&s=tag&q=index&limit=\(limit)&orderby=\(order)" - ) + var components = URLComponents() + + components.scheme = "https" + components.host = domain + components.path = "/index.php" + components.queryItems = [ + URLQueryItem(name: "page", value: "dapi"), + URLQueryItem(name: "s", value: "tag"), + URLQueryItem(name: "q", value: "index"), + URLQueryItem(name: "limit", value: String(limit)), + URLQueryItem(name: "orderby", value: order), + ] + + return components.url case .danbooru: return nil @@ -298,20 +354,44 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng private func urlForTagsSearch(name: String) -> URL? { switch flavor { case .moebooru: - return URL(string: "https://\(domain)/tag.xml?name_pattern=\(name)&order=count") + var components = URLComponents() + + components.scheme = "https" + components.host = domain + components.path = "/tag.xml" + components.queryItems = [ + URLQueryItem(name: "name_pattern", value: name), + URLQueryItem(name: "order", value: "count"), + ] + + return components.url case .gelbooru: - var urlString = - "https://\(domain)/index.php?page=dapi&s=tag&q=index&name_pattern=%\(name)%&orderby=count" + var components = URLComponents() + + components.scheme = "https" + components.host = domain + components.path = "/index.php" + + var queryItems = [ + URLQueryItem(name: "page", value: "dapi"), + URLQueryItem(name: "s", value: "tag"), + URLQueryItem(name: "q", value: "index"), + URLQueryItem(name: "name_pattern", value: "%\(name)%"), + URLQueryItem(name: "orderby", value: "count"), + ] if let validCredentials = credentials, !validCredentials.apiKey.isEmpty, validCredentials.userID != 0 { - urlString += "&api_key=\(validCredentials.apiKey)&user_id=\(validCredentials.userID)" + queryItems.append(URLQueryItem(name: "api_key", value: validCredentials.apiKey)) + queryItems.append(URLQueryItem(name: "user_id", value: String(validCredentials.userID))) } - return URL(string: urlString) + components.queryItems = queryItems + + return components.url case .danbooru: return nil @@ -327,15 +407,35 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng flavor == .danbooru ? DanbooruPostParser(data: data).parse() : BooruPostXMLParser(data: data, provider: provider).parse() - var uniquePosts: [BooruPost] = [] - var seenIDs: Set<String> = [] + var uniquePosts: [String: BooruPost] = [:] + + for post in parsedPosts { + uniquePosts[post.id] = post + } + + return Array(uniquePosts.values) + } + + private func getXMLParser(for provider: BooruProvider) -> BooruPostXMLParser { + parserPoolLock.lock() - for post in parsedPosts where !seenIDs.contains(post.id) { - uniquePosts.append(post) - seenIDs.insert(post.id) + defer { parserPoolLock.unlock() } + + if let parser = xmlParserPool.popLast() { + return parser } - return uniquePosts + return BooruPostXMLParser(data: Data(), provider: provider) + } + + private func returnXMLParser(_ parser: BooruPostXMLParser) { + parserPoolLock.lock() + + defer { parserPoolLock.unlock() } + + if xmlParserPool.count < 3 { + xmlParserPool.append(parser) + } } private func updatePosts(_ newPosts: [BooruPost], replace: Bool) { @@ -380,7 +480,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } // MARK: - Deinitialisation - deinit { + nonisolated deinit { currentTask?.cancel() urlCache.removeAll() } diff --git a/Sora/Data/Booru/Post/BooruPostXMLParser.swift b/Sora/Data/Booru/Post/BooruPostXMLParser.swift index 89fceb7..8b6260e 100644 --- a/Sora/Data/Booru/Post/BooruPostXMLParser.swift +++ b/Sora/Data/Booru/Post/BooruPostXMLParser.swift @@ -18,6 +18,19 @@ class BooruPostXMLParser: NSObject, XMLParserDelegate { parser.delegate = self } + func reset(with data: Data) { + posts.removeAll() + + currentPost = nil + + currentPostData.removeAll() + + currentElementName = nil + currentText = "" + parser = XMLParser(data: data) + parser.delegate = self + } + func parse() -> [BooruPost] { parser.parse() @@ -26,30 +39,41 @@ class BooruPostXMLParser: NSObject, XMLParserDelegate { private func makePost(from dict: [String: String]) -> BooruPost? { guard let id = dict["id"], - let heightStr = dict["height"], let height = Int(heightStr), let score = dict["score"], - let fileUrlString = dict["file_url"], let fileUrl = URL(string: fileUrlString), let parentId = dict["parent_id"], - let sampleUrlString = dict["sample_url"], let sampleUrl = URL(string: sampleUrlString), - let sampleWidthString = dict["sample_width"], let sampleWidth = Int(sampleWidthString), - let sampleHeightString = dict["sample_height"], let sampleHeight = Int(sampleHeightString), - let previewUrlString = dict["preview_url"], let previewUrl = URL(string: previewUrlString), let rating = dict["rating"], let tagsString = dict["tags"], - let widthString = dict["width"], let width = Int(widthString), let change = dict["change"], let md5 = dict["md5"], let creatorId = dict["creator_id"], let createdAtString = dict["created_at"], - let createdAt = parseCreatedAt(createdAtString), let status = dict["status"], - let source = dict["source"], - let previewWidthString = dict["preview_width"], let previewWidth = Int(previewWidthString), - let previewHeightString = dict["preview_height"], let previewHeight = Int(previewHeightString) + let source = dict["source"] else { return nil } + guard let height = Int(dict["height"] ?? ""), + let sampleWidth = Int(dict["sample_width"] ?? ""), + let sampleHeight = Int(dict["sample_height"] ?? ""), + let width = Int(dict["width"] ?? ""), + let previewWidth = Int(dict["preview_width"] ?? ""), + let previewHeight = Int(dict["preview_height"] ?? "") + else { + return nil + } + + guard let fileUrl = URL(string: dict["file_url"] ?? ""), + let sampleUrl = URL(string: dict["sample_url"] ?? ""), + let previewUrl = URL(string: dict["preview_url"] ?? ""), + let createdAt = parseCreatedAt(createdAtString) + else { + return nil + } + + let tags = tagsString.components(separatedBy: .whitespacesAndNewlines) + .filter { !$0.isEmpty } + return BooruPost( id: id, height: height, @@ -61,8 +85,7 @@ class BooruPostXMLParser: NSObject, XMLParserDelegate { sampleHeight: sampleHeight, previewURL: previewUrl, rating: BooruRating(rating), - tags: tagsString.components(separatedBy: CharacterSet.whitespacesAndNewlines) - .filter { !$0.isEmpty }, + tags: tags, width: width, change: change, md5: md5, @@ -141,13 +164,17 @@ class BooruPostXMLParser: NSObject, XMLParserDelegate { } #endif - func parseCreatedAt(_ input: String) -> Date? { - let dateFormatter = DateFormatter() + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() - dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" - dateFormatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" + formatter.locale = Locale(identifier: "en_US_POSIX") - if let date = dateFormatter.date(from: input) { + return formatter + }() + + func parseCreatedAt(_ input: String) -> Date? { + if let date = Self.dateFormatter.date(from: input) { return date } |