mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Fix suggestions cache miss handling for missing files (#880)
## Description Fix a bug where movie suggestions would get stuck loading when the on-disk cache file doesn't exist. ### Related issues Fixes #876 ### Testing Ran unit tests locally. ## Screenshots N/A ## AI or LLM usage Used AI to add testing for this scenario to unit test --------- Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
This commit is contained in:
parent
4161ea418c
commit
11fd780b31
5 changed files with 76 additions and 21 deletions
|
|
@ -6,6 +6,7 @@ import androidx.work.WorkManager
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
|
@ -49,21 +50,8 @@ class SuggestionService
|
||||||
.asFlow()
|
.asFlow()
|
||||||
.flatMapLatest { user ->
|
.flatMapLatest { user ->
|
||||||
val userId = user?.id ?: return@flatMapLatest flowOf(SuggestionsResource.Empty)
|
val userId = user?.id ?: return@flatMapLatest flowOf(SuggestionsResource.Empty)
|
||||||
val cachedIds = cache.get(userId, parentId, itemKind)?.ids.orEmpty()
|
val cachedSuggestions = cache.get(userId, parentId, itemKind)
|
||||||
if (cachedIds.isNotEmpty()) {
|
if (cachedSuggestions == null) {
|
||||||
flow {
|
|
||||||
try {
|
|
||||||
emit(
|
|
||||||
SuggestionsResource.Success(
|
|
||||||
fetchItemsByIds(cachedIds, itemKind),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Failed to fetch items")
|
|
||||||
emit(SuggestionsResource.Empty)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
workManager
|
workManager
|
||||||
.getWorkInfosForUniqueWorkFlow(SuggestionsWorker.WORK_NAME)
|
.getWorkInfosForUniqueWorkFlow(SuggestionsWorker.WORK_NAME)
|
||||||
.map { workInfos ->
|
.map { workInfos ->
|
||||||
|
|
@ -73,6 +61,23 @@ class SuggestionService
|
||||||
}
|
}
|
||||||
if (isActive) SuggestionsResource.Loading else SuggestionsResource.Empty
|
if (isActive) SuggestionsResource.Loading else SuggestionsResource.Empty
|
||||||
}
|
}
|
||||||
|
} else if (cachedSuggestions.ids.isEmpty()) {
|
||||||
|
flowOf(SuggestionsResource.Empty)
|
||||||
|
} else {
|
||||||
|
flow {
|
||||||
|
try {
|
||||||
|
emit(
|
||||||
|
SuggestionsResource.Success(
|
||||||
|
fetchItemsByIds(cachedSuggestions.ids, itemKind),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to fetch items")
|
||||||
|
emit(SuggestionsResource.Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,15 +60,20 @@ class SuggestionsCache
|
||||||
): CachedSuggestions? {
|
): CachedSuggestions? {
|
||||||
val key = cacheKey(userId, libraryId, itemKind)
|
val key = cacheKey(userId, libraryId, itemKind)
|
||||||
return memoryCache.getOrPut(key) {
|
return memoryCache.getOrPut(key) {
|
||||||
try {
|
mutex.withLock {
|
||||||
mutex.withLock {
|
try {
|
||||||
File(cacheDir, "$key.json").inputStream().use {
|
val cacheFile = File(cacheDir, "$key.json")
|
||||||
|
if (!cacheFile.exists()) {
|
||||||
|
return@withLock null
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFile.inputStream().use {
|
||||||
json.decodeFromStream<CachedSuggestions>(it)
|
json.decodeFromStream<CachedSuggestions>(it)
|
||||||
}
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Exception reading from disk cache")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
|
||||||
Timber.e(ex, "Exception reading from disk cache")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -207,6 +208,8 @@ class RecommendedMovieViewModel
|
||||||
}
|
}
|
||||||
update(R.string.suggestions, state)
|
update(R.string.suggestions, state)
|
||||||
}
|
}
|
||||||
|
} catch (ex: CancellationException) {
|
||||||
|
throw ex
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Failed to fetch suggestions")
|
Timber.e(ex, "Failed to fetch suggestions")
|
||||||
update(
|
update(
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
|
@ -254,6 +255,8 @@ class RecommendedTvShowViewModel
|
||||||
}
|
}
|
||||||
update(R.string.suggestions, state)
|
update(R.string.suggestions, state)
|
||||||
}
|
}
|
||||||
|
} catch (ex: CancellationException) {
|
||||||
|
throw ex
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Timber.e(ex, "Failed to fetch suggestions")
|
Timber.e(ex, "Failed to fetch suggestions")
|
||||||
update(
|
update(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -150,6 +151,44 @@ class SuggestionServiceTest {
|
||||||
assertEquals(SuggestionsResource.Empty, result)
|
assertEquals(SuggestionsResource.Empty, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getSuggestionsFlow_returnsEmpty_whenCachedIdsEmpty_evenIfWorkIsEnqueued() =
|
||||||
|
runTest {
|
||||||
|
val userId = UUID.randomUUID()
|
||||||
|
val parentId = UUID.randomUUID()
|
||||||
|
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
|
||||||
|
|
||||||
|
every { mockServerRepository.currentUser } returns currentUser
|
||||||
|
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returns CachedSuggestions(emptyList())
|
||||||
|
every { mockWorkManager.getWorkInfosForUniqueWorkFlow(any()) } returns
|
||||||
|
flowOf(listOf(mockWorkInfo(WorkInfo.State.ENQUEUED)))
|
||||||
|
|
||||||
|
val service = createService()
|
||||||
|
val result = service.getSuggestionsFlow(parentId, BaseItemKind.MOVIE).first()
|
||||||
|
|
||||||
|
assertEquals(SuggestionsResource.Empty, result)
|
||||||
|
verify(exactly = 0) { mockWorkManager.getWorkInfosForUniqueWorkFlow(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getSuggestionsFlow_returnsLoading_whenCacheMissing_andWorkIsEnqueued() =
|
||||||
|
runTest {
|
||||||
|
val userId = UUID.randomUUID()
|
||||||
|
val parentId = UUID.randomUUID()
|
||||||
|
val currentUser = MutableLiveData<JellyfinUser?>(mockUser(userId))
|
||||||
|
|
||||||
|
every { mockServerRepository.currentUser } returns currentUser
|
||||||
|
coEvery { mockCache.get(userId, parentId, BaseItemKind.MOVIE) } returns null
|
||||||
|
every { mockWorkManager.getWorkInfosForUniqueWorkFlow(any()) } returns
|
||||||
|
flowOf(listOf(mockWorkInfo(WorkInfo.State.ENQUEUED)))
|
||||||
|
|
||||||
|
val service = createService()
|
||||||
|
val result = service.getSuggestionsFlow(parentId, BaseItemKind.MOVIE).first()
|
||||||
|
|
||||||
|
assertEquals(SuggestionsResource.Loading, result)
|
||||||
|
verify(exactly = 1) { mockWorkManager.getWorkInfosForUniqueWorkFlow(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun passes_correct_arguments_to_cache() =
|
fun passes_correct_arguments_to_cache() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue