import Foundation class BooruPostXMLParser: NSObject, XMLParserDelegate { private var posts: [BooruPost] = [] private var currentPost: BooruPost? private var parser: XMLParser private let provider: BooruProvider private var currentPostData: [String: String] = [:] private var currentElementName: String? private var currentText: String = "" init(data: Data, provider: BooruProvider) { parser = XMLParser(data: data) self.provider = provider super.init() parser.delegate = self } func parse() -> [BooruPost] { parser.parse() return posts } func parser( _ parser: XMLParser, // swiftlint:disable:this unused_parameter didStartElement elementName: String, namespaceURI: String?, // swiftlint:disable:this unused_parameter qualifiedName qName: String?, // swiftlint:disable:this unused_parameter attributes attributeDict: [String: String] = [:] ) { if provider == .gelbooru { if elementName == "post" { currentPostData = [:] } else { currentElementName = elementName currentText = "" } } else { 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 } 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: BooruRating(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: (attributeDict["has_notes"] ?? "false") == "true", hasComments: (attributeDict["has_comments"] ?? "false") == "true", previewWidth: previewWidth, previewHeight: previewHeight ) } } } func parser( _ parser: XMLParser, // swiftlint:disable:this unused_parameter foundCharacters string: String ) { if provider == .gelbooru, currentElementName != nil { currentText += string } } func parser( _ parser: XMLParser, // swiftlint:disable:this unused_parameter didEndElement elementName: String, namespaceURI: String?, // swiftlint:disable:this unused_parameter qualifiedName qName: String? // swiftlint:disable:this unused_parameter ) { if provider == .gelbooru { if elementName == "post" { guard let id = currentPostData["id"], let heightStr = currentPostData["height"], let height = Int(heightStr), let score = currentPostData["score"], let fileUrlStr = currentPostData["file_url"], let fileUrl = URL(string: fileUrlStr), let parentId = currentPostData["parent_id"], let sampleUrlStr = currentPostData["sample_url"], let sampleUrl = URL(string: sampleUrlStr), let sampleWidthStr = currentPostData["sample_width"], let sampleWidth = Int(sampleWidthStr), let sampleHeightStr = currentPostData["sample_height"], let sampleHeight = Int(sampleHeightStr), let previewUrlStr = currentPostData["preview_url"], let previewUrl = URL(string: previewUrlStr), let rating = currentPostData["rating"], let tagsString = currentPostData["tags"], let widthStr = currentPostData["width"], let width = Int(widthStr), let change = currentPostData["change"], let md5 = currentPostData["md5"], let creatorId = currentPostData["creator_id"], let hasChildrenStr = currentPostData["has_children"], let createdAtStr = currentPostData["created_at"], let createdAt = parseCreatedAt(createdAtStr), let status = currentPostData["status"], let source = currentPostData["source"], let previewWidthStr = currentPostData["preview_width"], let previewWidth = Int(previewWidthStr), let previewHeightStr = currentPostData["preview_height"], let previewHeight = Int(previewHeightStr) else { return } posts.append( BooruPost( id: id, height: height, score: score, fileURL: fileUrl, parentID: parentId, sampleURL: sampleUrl, sampleWidth: sampleWidth, sampleHeight: sampleHeight, previewURL: previewUrl, rating: BooruRating(rating), tags: tagsString.components(separatedBy: CharacterSet.whitespacesAndNewlines) .filter { !$0.isEmpty }, width: width, change: change, md5: md5, creatorID: creatorId, hasChildren: hasChildrenStr == "true", createdAt: createdAt, status: status, source: source, hasNotes: (currentPostData["has_notes"] ?? "false") == "true", hasComments: (currentPostData["has_comments"] ?? "false") == "true", previewWidth: previewWidth, previewHeight: previewHeight ) ) } else if let currentElement = currentElementName { currentPostData[currentElement] = currentText.trimmingCharacters( in: .whitespacesAndNewlines ) currentElementName = nil currentText = "" } } else { if elementName == "post", let post = currentPost { posts.append(post) currentPost = nil } } } #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") if let date = dateFormatter.date(from: input) { return date } if let timestamp = Double(input) { return Date(timeIntervalSince1970: timestamp) } return nil } }