@@ -16,6 +16,11 @@ import java.util.Base64
1616
1717class DeArrowUnavailableException : RuntimeException (" DeArrow is temporarily unavailable" )
1818
19+ data class DeArrowThumbnail (
20+ val bytes : ByteArray ,
21+ val fallback : Boolean ,
22+ )
23+
1924class DeArrowService (
2025 private val cache : CacheService ,
2126 private val client : DeArrowRemote = DeArrowClient (),
@@ -35,17 +40,27 @@ class DeArrowService(
3540 return item
3641 }
3742
38- suspend fun thumbnail (videoId : String , timestamp : Double ): ByteArray ? {
43+ suspend fun thumbnail (videoId : String , timestamp : Double ): DeArrowThumbnail ? {
3944 if (! isValidVideoId(videoId) || ! timestamp.isFinite() || timestamp < 0.0 ) return null
4045 val normalizedTime = " %.3f" .format(java.util.Locale .ROOT , timestamp)
4146 val key = " dearrow:thumbnail:$videoId :$normalizedTime "
42- cache.get(key)?.let { return runCatching { Base64 .getDecoder().decode(it) }.getOrNull( ) }
47+ cache.get(key)?.let { return decodeThumbnail(it ) }
4348 val generated = client.thumbnail(videoId, timestamp)?.takeIf { it.size <= MAX_THUMBNAIL_BYTES }
4449 val fallback = generated ? : client.fallbackThumbnail(videoId)?.takeIf { it.size <= MAX_THUMBNAIL_BYTES }
4550 val bytes = fallback ? : return null
46- val ttl = if (generated != null ) THUMBNAIL_TTL_SECONDS else FALLBACK_THUMBNAIL_TTL_SECONDS
47- cache.set(key, Base64 .getEncoder().encodeToString(bytes), ttl)
48- return bytes
51+ val isFallback = generated == null
52+ val ttl = if (isFallback) FALLBACK_THUMBNAIL_TTL_SECONDS else THUMBNAIL_TTL_SECONDS
53+ val prefix = if (isFallback) FALLBACK_CACHE_PREFIX else GENERATED_CACHE_PREFIX
54+ cache.set(key, prefix + Base64 .getEncoder().encodeToString(bytes), ttl)
55+ return DeArrowThumbnail (bytes, isFallback)
56+ }
57+
58+ private fun decodeThumbnail (value : String ): DeArrowThumbnail ? {
59+ val fallback = value.startsWith(FALLBACK_CACHE_PREFIX )
60+ val payload = value.removePrefix(FALLBACK_CACHE_PREFIX ).removePrefix(GENERATED_CACHE_PREFIX )
61+ return runCatching {
62+ DeArrowThumbnail (Base64 .getDecoder().decode(payload), fallback)
63+ }.getOrNull()
4964 }
5065
5166 private fun parse (videoId : String , raw : String ): DeArrowItem {
@@ -123,6 +138,8 @@ class DeArrowService(
123138 private const val THUMBNAIL_TTL_SECONDS = 604_800L
124139 private const val FALLBACK_THUMBNAIL_TTL_SECONDS = 900L
125140 private const val MAX_THUMBNAIL_BYTES = 2 * 1024 * 1024
141+ private const val FALLBACK_CACHE_PREFIX = " fallback:"
142+ private const val GENERATED_CACHE_PREFIX = " generated:"
126143 private val VIDEO_ID_REGEX = Regex (" ^[A-Za-z0-9_-]{11}$" )
127144 private val TITLE_MARKER_REGEX = Regex (" >(?=\\ S)" )
128145 }
0 commit comments