import Foundation /// A named "row + fields" extraction recipe, auto-applied when a page's URL /// matches. The Discogs profile mirrors the selectors in the scraperrr notes. struct ScrapeProfile { let name: String let urlPattern: String let rowSelector: String let fields: [(name: String, selector: String)] func matchesURL(_ url: String) -> Bool { url.range(of: urlPattern, options: [.regularExpression, .caseInsensitive]) != nil } static let discogsMarketplace = ScrapeProfile( name: "discogs_marketplace", urlPattern: #"discogs\.com/(sell/list|sell/mywants|seller/[^/]+/profile)"#, rowSelector: "tr.shortcut_navigable", fields: [ ("item_id", "a.cart-button@data-item-id"), ("release_id", "tr@data-release-id"), ("title", "a.item_description_title"), ("listing_url", "a.item_description_title@href"), ("media_condition", ".item_condition span[!class]"), ("sleeve_condition", "span.item_sleeve_condition"), ("seller_username", ".seller_block a"), ("seller_id", "li.seller_mywants@data-seller-id"), // Co-located with seller_id on the same element — a reliable // username fallback so a seller is never referenced unmapped. ("seller_username_alt", "li.seller_mywants@data-username"), ("price_value", "span.price@data-pricevalue"), ("price_currency", "span.price@data-currency"), ("shipping", "span.item_shipping"), ] ) static let builtIn: [ScrapeProfile] = [discogsMarketplace] static func matching(url: String?) -> ScrapeProfile? { guard let url else { return nil } return builtIn.first { $0.matchesURL(url) } } } struct DetectedPage { let url: String? let html: String } enum PageDetector { /// Recognizes the "URL on the first line, then pasted HTML" convention /// (URL optional). Returns nil when the document doesn't look like HTML. static func detect(in text: String) -> DetectedPage? { var url: String? var html = String(text.drop(while: { $0.isWhitespace })) if html.hasPrefix("http://") || html.hasPrefix("https://") { if let newline = html.firstIndex(of: "\n") { let candidate = String(html[..