mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-09-20 06:34:00 +00:00
fix: cached track audio not being invalidated causing alternative track source to not being played
This commit is contained in:
parent
b6615bdbc8
commit
16e2a743a5
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<AlternativeTracksRepository>()
|
||||
@ -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" }
|
||||
}
|
||||
|
||||
@ -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<CacheEntry> = 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()
|
||||
|
||||
@ -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<LocalServer>()
|
||||
@ -76,14 +75,6 @@ class LocalServer(
|
||||
}.stateIn(scope, SharingStarted.WhileSubscribed(5_000), null)
|
||||
|
||||
private val cachedCacheEnabled = MutableStateFlow(false)
|
||||
private val cachedCacheFolder = MutableStateFlow<String?>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<StreamProxy>()
|
||||
|
||||
companion object {
|
||||
private const val HOST = "127.0.0.1"
|
||||
}
|
||||
|
||||
@ -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<UserSettings> = database.settingsDataStore.data.map { prefs ->
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
val userSettings: StateFlow<UserSettings> = database.settingsDataStore.data.map { prefs ->
|
||||
val json = prefs[SETTINGS_KEY]
|
||||
if (json != null) {
|
||||
Json.decodeFromString<UserSettings>(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 ->
|
||||
|
||||
@ -32,11 +32,7 @@ interface SettingsProvider {
|
||||
class SettingsViewModel(
|
||||
private val repository: SettingsRepository
|
||||
) : ViewModel(), SettingsProvider {
|
||||
override val settingsState: StateFlow<UserSettings?> = repository.userSettings.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
null,
|
||||
)
|
||||
override val settingsState = repository.userSettings
|
||||
|
||||
fun updateSettings(transform: UserSettings.() -> UserSettings) {
|
||||
viewModelScope.launch {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user