package coffee.liz.rookery.images import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject /** One image search result: a small [thumbnail] to show in the picker and the [full] image to keep. */ data class ImageHit(val title: String, val thumbnail: String, val full: String, val source: String) /** Brave Search image API: request building and response parsing (the HTTP call itself is the host's job). */ object BraveImages { const val KEY_HEADER = "X-Subscription-Token" fun url(query: String, count: Int = 12): String = "https://api.search.brave.com/res/v1/images/search?q=${encode(query)}&count=$count&safesearch=strict" fun parse(body: String): List { val root = Json.parseToJsonElement(body).jsonObject return (root["results"] as? JsonArray).orEmpty().mapNotNull { e -> val o = e as? JsonObject ?: return@mapNotNull null val thumb = (o["thumbnail"] as? JsonObject)?.str("src") val full = (o["properties"] as? JsonObject)?.str("url") val any = thumb ?: full ?: return@mapNotNull null if (!any.startsWith("https://")) return@mapNotNull null ImageHit(o.str("title").orEmpty(), thumb ?: any, full?.takeIf { it.startsWith("https://") } ?: any, o.str("source").orEmpty()) } } private fun JsonObject.str(k: String) = (this[k] as? JsonPrimitive)?.contentOrNull private fun encode(s: String): String = buildString { for (b in s.encodeToByteArray()) { val c = b.toInt().toChar() if (c.isLetterOrDigit() && b >= 0 || c in "-_.~") append(c) else append('%').append((b.toInt() and 0xFF).toString(16).uppercase().padStart(2, '0')) } } }