From 16e2a743a5cea6db8f9c523265ad66ead2ad3ce5 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 14 Aug 2026 13:15:34 +0600 Subject: [PATCH] fix: cached track audio not being invalidated causing alternative track source to not being played --- .../audioplayer/DeviceAudioPlayerQueue.kt | 10 +++-- .../dev/krtirtho/spotube/core/di/Modules.kt | 2 + .../server/AlternativeTracksRepository.kt | 2 + .../spotube/core/server/CacheManager.kt | 37 +++++++++++++++---- .../spotube/core/server/LocalServer.kt | 14 +------ .../spotube/core/server/StreamProxy.kt | 7 +++- .../modules/settings/SettingsRepository.kt | 14 ++++++- .../modules/settings/SettingsViewModel.kt | 6 +-- .../AlternativeTrackContentViewModel.kt | 5 ++- 9 files changed, 62 insertions(+), 35 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/audioplayer/DeviceAudioPlayerQueue.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/audioplayer/DeviceAudioPlayerQueue.kt index 2ae7e046..230b0c78 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/audioplayer/DeviceAudioPlayerQueue.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/audioplayer/DeviceAudioPlayerQueue.kt @@ -259,12 +259,16 @@ class DeviceAudioPlayerQueue( return } val queue = queueFlow.value + + // Using separate current index entry as the current track's source url has changed so it won't match val index = queue.indexOfFirst { entry -> - when { - entry is QueueEntry.StreamingTrack && current is QueueEntry.StreamingTrack -> + when (entry) { + is QueueEntry.StreamingTrack if current is QueueEntry.StreamingTrack -> entry.track.id == current.track.id - entry is QueueEntry.LocalTrack && current is QueueEntry.LocalTrack -> + + is QueueEntry.LocalTrack if current is QueueEntry.LocalTrack -> entry.url == current.url && entry.name == current.name + else -> false } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/di/Modules.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/di/Modules.kt index bf4f1eb0..a067476b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/di/Modules.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/di/Modules.kt @@ -27,6 +27,7 @@ import dev.krtirtho.spotube.core.discord.DiscordRpcService import dev.krtirtho.spotube.core.navigation.navigationModule import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper import dev.krtirtho.spotube.core.server.AlternativeTracksRepository +import dev.krtirtho.spotube.core.server.CacheManager import dev.krtirtho.spotube.core.server.LocalServer import dev.krtirtho.spotube.core.server.MatchedTracksRepository import dev.krtirtho.spotube.core.server.StreamingUrlRepository @@ -190,6 +191,7 @@ val sharedModules = module { } singleOf(::MatchedTracksRepository) singleOf(::StreamingUrlRepository) + singleOf(::CacheManager) singleOf(::AlternativeTracksRepository) singleOf(::LocalServer) withOptions { createdAtStart() diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/AlternativeTracksRepository.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/AlternativeTracksRepository.kt index 45ad3a3d..42e431af 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/AlternativeTracksRepository.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/AlternativeTracksRepository.kt @@ -29,6 +29,7 @@ class AlternativeTracksRepository( private val matchedTracksRepository: MatchedTracksRepository, private val streamingUrlRepository: StreamingUrlRepository, private val audioPlayerQueue: AudioPlayerQueue, + private val cacheManager: CacheManager, ) : KoinComponent { private val logger by injectLogger() @@ -78,6 +79,7 @@ class AlternativeTracksRepository( matchedTracksRepository.saveTrackSource(track, basic) streamingUrlRepository.invalidateCachedStreamUrl(trackId) streamingUrlRepository.invalidateCachedAlternatives(trackId) + cacheManager.invalidateCacheEntry(trackId) audioPlayerQueue.reloadCurrent() logger.d { "Alternative source selection complete for track $trackId" } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/CacheManager.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/CacheManager.kt index 2305cba5..c5db9b1d 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/CacheManager.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/CacheManager.kt @@ -19,6 +19,7 @@ package dev.krtirtho.spotube.core.server import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import dev.krtirtho.spotube.core.paths.Paths +import dev.krtirtho.spotube.modules.settings.SettingsRepository import io.ktor.http.ContentType import io.ktor.http.Headers import io.ktor.http.HeadersBuilder @@ -40,12 +41,12 @@ import okio.buffer import okio.use @Serializable -internal data class CacheIndex( +data class CacheIndex( val entries: List = emptyList() ) @Serializable -internal data class CacheEntry( +data class CacheEntry( val trackId: String, val filename: String, val sizeBytes: Long, @@ -53,18 +54,26 @@ internal data class CacheEntry( val contentType: String = "application/octet-stream", ) -internal class CacheManager( +class CacheManager( private val paths: Paths, - private val resolveCacheFolder: () -> String?, - private val resolveSizeLimitMB: () -> Long, - private val fileSystem: FileSystem = FileSystem.SYSTEM, - private val cacheMutex: Mutex = Mutex(), - private val json: Json = Json { ignoreUnknownKeys = true }, + private val settingsRepository: SettingsRepository, ) { + private val fileSystem: FileSystem = FileSystem.SYSTEM + private val cacheMutex: Mutex = Mutex() + private val json: Json = Json { ignoreUnknownKeys = true } + companion object { private val cacheIndexFileName = "cache_index.json".toPath() } + private fun resolveCacheFolder(): String? { + return settingsRepository.userSettings.value.cacheFolder + } + + private fun resolveSizeLimitMB(): Long { + return settingsRepository.userSettings.value.cacheSizeLimitMB + } + fun resolveCacheDir(): Path { val folder = resolveCacheFolder() return if (!folder.isNullOrBlank()) { @@ -101,6 +110,18 @@ internal class CacheManager( return if (fileSystem.exists(filePath)) filePath to entry else null } + suspend fun invalidateCacheEntry(trackId: String) { + cacheMutex.withLock { + val index = readCacheIndex() + val entry = index.entries.firstOrNull { it.trackId == trackId } ?: return + val filePath = resolveCacheDir() / entry.filename.toPath() + if (fileSystem.exists(filePath)) { + fileSystem.delete(filePath) + } + writeCacheIndex(index.copy(entries = index.entries.filter { it.trackId != trackId })) + } + } + fun resolveCacheFilename(track: MetadataTrack, contentType: String?): String { val artists = track.artists.joinToString(", ") { it.name }.sanitizeFilenamePart() val title = track.title.sanitizeFilenamePart() diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/LocalServer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/LocalServer.kt index 0b31ea27..6725f215 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/LocalServer.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/LocalServer.kt @@ -19,7 +19,6 @@ package dev.krtirtho.spotube.core.server import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import dev.krtirtho.spotube.core.di.injectLogger -import dev.krtirtho.spotube.core.paths.Paths import dev.krtirtho.spotube.modules.settings.SettingsViewModel import io.ktor.client.HttpClient import io.ktor.http.HttpMethod @@ -52,10 +51,10 @@ import kotlinx.coroutines.sync.withLock import org.koin.core.component.KoinComponent class LocalServer( - private val paths: Paths, settingsViewModel: SettingsViewModel, private val streamingUrlRepository: StreamingUrlRepository, private val audioPlayerQueue: AudioPlayerQueue, + private val cacheManager: CacheManager, ) : KoinComponent { val logger by injectLogger() @@ -76,14 +75,6 @@ class LocalServer( }.stateIn(scope, SharingStarted.WhileSubscribed(5_000), null) private val cachedCacheEnabled = MutableStateFlow(false) - private val cachedCacheFolder = MutableStateFlow(null) - private val cachedCacheSizeLimitMB = MutableStateFlow(0L) - - private val cacheManager = CacheManager( - paths = paths, - resolveCacheFolder = { cachedCacheFolder.value }, - resolveSizeLimitMB = { cachedCacheSizeLimitMB.value }, - ) private val streamProxy by lazy { StreamProxy( @@ -94,7 +85,6 @@ class LocalServer( isCachingEnabled = { cachedCacheEnabled.value }, activePort = { activePort.value }, scope = scope, - logger = logger, ) } @@ -118,8 +108,6 @@ class LocalServer( .collect { settings -> if (settings != null) { cachedCacheEnabled.value = settings.enableMusicCaching - cachedCacheFolder.value = settings.cacheFolder - cachedCacheSizeLimitMB.value = settings.cacheSizeLimitMB } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/StreamProxy.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/StreamProxy.kt index ed63e17e..a937bc65 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/StreamProxy.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/server/StreamProxy.kt @@ -21,6 +21,7 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import dev.krtirtho.spotube.core.audioplayer.QueueEntry +import dev.krtirtho.spotube.core.di.injectLogger import io.ktor.client.HttpClient import io.ktor.client.request.request import io.ktor.client.statement.HttpResponse @@ -51,6 +52,7 @@ import okio.Path.Companion.toPath import okio.SYSTEM import okio.buffer import okio.use +import org.koin.core.component.KoinComponent internal class StreamProxy( private val httpClient: HttpClient, @@ -60,8 +62,9 @@ internal class StreamProxy( private val isCachingEnabled: () -> Boolean, private val activePort: () -> Int?, private val scope: CoroutineScope, - private val logger: co.touchlab.kermit.Logger, -) { +): KoinComponent { + val logger by injectLogger() + companion object { private const val HOST = "127.0.0.1" } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsRepository.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsRepository.kt index 35fb777d..ac6a987f 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsRepository.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsRepository.kt @@ -20,6 +20,10 @@ package dev.krtirtho.spotube.modules.settings import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import dev.krtirtho.spotube.core.db.Database +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.* import kotlinx.serialization.json.Json @@ -28,14 +32,20 @@ class SettingsRepository(private val database: Database) { private val SETTINGS_KEY = stringPreferencesKey("user_settings") } - val userSettings: Flow = database.settingsDataStore.data.map { prefs -> + val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + val userSettings: StateFlow = database.settingsDataStore.data.map { prefs -> val json = prefs[SETTINGS_KEY] if (json != null) { Json.decodeFromString(json as String) } else { UserSettings() // Default value } - } + }.stateIn( + scope, + started = SharingStarted.Eagerly, + initialValue = UserSettings() // Default value + ) suspend fun updateSettings(newSettings: UserSettings) { database.settingsDataStore.edit { prefs -> diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsViewModel.kt index 77c0fdba..88b4878d 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsViewModel.kt @@ -32,11 +32,7 @@ interface SettingsProvider { class SettingsViewModel( private val repository: SettingsRepository ) : ViewModel(), SettingsProvider { - override val settingsState: StateFlow = repository.userSettings.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - null, - ) + override val settingsState = repository.userSettings fun updateSettings(transform: UserSettings.() -> UserSettings) { viewModelScope.launch { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContentViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContentViewModel.kt index 2bd49aa6..0e9bd5b6 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContentViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/alternative_track/AlternativeTrackContentViewModel.kt @@ -95,8 +95,9 @@ class AlternativeTrackContentViewModel( } fun selectAlternative(source: AudioSource) { - val currentTrack = (audioPlayerQueue.currentQueueEntryFlow.value as? QueueEntry.StreamingTrack)?.track - if (currentTrack == null) return + val currentTrack = + (audioPlayerQueue.currentQueueEntryFlow.value as? QueueEntry.StreamingTrack)?.track + ?: return viewModelScope.launch { alternativeTracksRepository.selectAlternative(currentTrack, source) activeSourceIdFlow.value = source.id