-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Completed the lazy thumbnail loading and added caching functionality
This update finalizes the thumbnail rendering feature in the SongFinder app. Primarily, we added the ability to lazily load thumbnail images depending on the PV availability. If a thumbnail fails to load, the app now automatically tries the next available PV until a successful thumbnail load occurs or all options are exhausted. In the latter case, a failure icon is displayed. Further, a caching mechanism has been introduced to speed up thumbnail retrieval. This cache is evicted every time the user move on to the next song.
- Loading branch information
Showing
4 changed files
with
165 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 57 additions & 5 deletions
62
songfinder-app/src/main/kotlin/mikufan/cx/songfinder/backend/service/FindThumbnailService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,83 @@ | ||
package mikufan.cx.songfinder.backend.service | ||
|
||
import kotlinx.coroutines.CancellationException | ||
import kotlinx.coroutines.withContext | ||
import mikufan.cx.inlinelogging.KInlineLogging | ||
import mikufan.cx.songfinder.backend.component.thumbnailfinder.ThumbnailException | ||
import mikufan.cx.songfinder.backend.component.thumbnailfinder.ThumbnailFinder | ||
import mikufan.cx.songfinder.backend.model.PVInfo | ||
import mikufan.cx.songfinder.backend.model.ThumbnailInfo | ||
import org.springframework.cache.CacheManager | ||
import org.springframework.cache.get | ||
import org.springframework.stereotype.Service | ||
|
||
@Service | ||
class FindThumbnailService( | ||
thumbnailFinders: List<ThumbnailFinder>, | ||
private val cacheManager: CacheManager, | ||
) { | ||
|
||
private val finderMap = thumbnailFinders.associateBy { it.matchedPvService } | ||
|
||
suspend fun tryGetThumbnail(pv: PVInfo): Result<ThumbnailInfo> { | ||
companion object { | ||
const val CACHE_NAME = "thumbnail" | ||
} | ||
|
||
// @Cacheable("thumbnail", key = "#pv.id + #pv.pvService") | ||
suspend fun tryGetThumbnail(pv: PVInfo): Result<ThumbnailInfo> = withContext(ThumbnailFinder.defaultDispatcher) { | ||
val cachedThumbnailInfoResult = cacheManager[CACHE_NAME]?.get<Result<ThumbnailInfo>>(pv.id + pv.pvService) | ||
if (cachedThumbnailInfoResult != null) { | ||
log.debug { "Found cached thumbnail info $cachedThumbnailInfoResult for PV $pv" } | ||
cachedThumbnailInfoResult | ||
} else { | ||
doGetAndSaveCacheConditionally(pv) | ||
} | ||
} | ||
|
||
private suspend fun doGetAndSaveCacheConditionally(pv: PVInfo): Result<ThumbnailInfo> { | ||
val finder = finderMap[pv.pvService] | ||
return if (finder == null) { | ||
Result.failure(IllegalArgumentException("No thumbnail finder for pv service ${pv.pvService}")) | ||
val r = Result.failure<ThumbnailInfo>(IllegalArgumentException("No thumbnail finder for pv service ${pv.pvService}")) | ||
cachePut(pv, r) | ||
r | ||
} else { | ||
try { | ||
Result.success(finder.findThumbnail(pv)) | ||
log.info { "First time searching thumbnail for PV $pv, try finding" } | ||
val thumbnailInfo = finder.findThumbnail(pv) | ||
log.info { "Successfully found thumbnail info $thumbnailInfo for PV $pv" } | ||
val r = Result.success(thumbnailInfo) | ||
cachePut(pv, r) | ||
r | ||
} catch (e: ThumbnailException) { | ||
log.warn { "Failed to find thumbnail info for PV $pv, exception: ${e.message}" } | ||
val r = Result.failure<ThumbnailInfo>(e) | ||
cachePut(pv, r) | ||
r | ||
} catch (e: CancellationException) { | ||
log.info { | ||
"Cancellation happens upon finding thumbnail info for PV $pv, " + | ||
"likely this happens when we are scrolling too fast. avoiding caching and returning. $e" | ||
} | ||
Result.failure(e) | ||
} catch (e: Exception) { | ||
log.warn(e) { | ||
"Failed to find thumbnail info for PV $pv due to unexpected exception, " + | ||
"avoiding caching and returning" | ||
} | ||
Result.failure(e) | ||
} | ||
} | ||
} | ||
|
||
private fun cachePut(pv: PVInfo, thumbnailInfoResult: Result<ThumbnailInfo>) { | ||
cacheManager[CACHE_NAME]?.put(pv.id + pv.pvService, thumbnailInfoResult) | ||
} | ||
|
||
// @CacheEvict("thumbnail", allEntries = true) | ||
fun evictCache() { | ||
TODO("return back here once spring cache is added") | ||
log.info { "Evicting all thumbnail cache" } | ||
cacheManager[CACHE_NAME]?.clear() | ||
} | ||
} | ||
} | ||
|
||
private val log = KInlineLogging.logger() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters