ImageSearchService.kt
package nl.johnvanweel.kikker
import org.slf4j.LoggerFactory
import tools.jackson.databind.ObjectMapper
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
import java.io.File
import java.security.MessageDigest
import java.util.Base64
@Service
class ImageSearchService(private val restClient: RestClient) {
private val logger = LoggerFactory.getLogger(javaClass)
private val fallbackImageBase64 by lazy {
val bytes = this::class.java.getResourceAsStream("/static/boot.jpg")?.readAllBytes()
?: throw RuntimeException("Fallback image not found")
Base64.getEncoder().encodeToString(bytes)
}
fun searchRandomImageBase64(query: String): String {
return try {
performSearch(query)
} catch (e: Exception) {
val cachedImage = getFallbackFromCache()
if (cachedImage != null) {
logger.info("Image search failed: {}. Using random image from cache.", e.message)
Base64.getEncoder().encodeToString(cachedImage)
} else {
println("[ERROR] Image search failed: ${e.message}. No cached images available. Falling back to default image.")
fallbackImageBase64
}
}
}
private fun getFallbackFromCache(): ByteArray? {
val cacheDir = File("/tmp/kikker")
if (!cacheDir.exists() || !cacheDir.isDirectory) {
return null
}
val files = cacheDir.listFiles()?.filter { it.isFile }
if (files.isNullOrEmpty()) {
return null
}
return files.random().readBytes()
}
private fun performSearch(query: String): String {
// Step 1: Get VQD token
val initialResponse = restClient.get()
.uri("https://duckduckgo.com/?q={query}", query)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.retrieve()
.body(String::class.java) ?: ""
val vqdRegex = "vqd=[\"']?([^\"'&]+)".toRegex()
val vqd = vqdRegex.find(initialResponse)
?.groupValues?.get(1)
?: throw RuntimeException("Could not find vqd token")
// Step 2: Search for images
val searchResponse = restClient.get()
.uri("https://duckduckgo.com/i.js?q={query}&o=json&vqd={vqd}", query, vqd)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.header("Referer", "https://duckduckgo.com/")
.header("Accept", "*/*")
.retrieve()
.body(String::class.java) ?: throw RuntimeException("Empty search response")
val objectMapper = ObjectMapper()
val bodyMap = objectMapper.readValue(searchResponse, Map::class.java)
val results = bodyMap?.get("results") as? List<Map<String, Any>>
val randomImageUrl = results?.randomOrNull()?.get("image") as? String
?: throw RuntimeException("No image found")
// Step 3: Fetch the image bytes (with cache)
val imageBytes = getImageBytes(randomImageUrl)
return Base64.getEncoder().encodeToString(imageBytes)
}
private fun getImageBytes(imageUrl: String): ByteArray {
val hash = sha256(imageUrl)
val cacheDir = File("/tmp/kikker")
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
val cacheFile = File(cacheDir, hash)
if (cacheFile.exists()) {
logger.info("Cache hit for image: {}", imageUrl)
return cacheFile.readBytes()
}
logger.info("Cache miss for image: {}. Downloading...", imageUrl)
val imageBytes = restClient.get()
.uri(imageUrl)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.retrieve()
.body(ByteArray::class.java) ?: throw RuntimeException("Could not fetch image")
cacheFile.writeBytes(imageBytes)
logger.info("Image cached: {} -> {}", imageUrl, cacheFile.absolutePath)
return imageBytes
}
private fun sha256(input: String): String {
val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
return bytes.joinToString("") { "%02x".format(it) }
}
}