mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-09-20 14:44: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
|
return
|
||||||
}
|
}
|
||||||
val queue = queueFlow.value
|
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 ->
|
val index = queue.indexOfFirst { entry ->
|
||||||
when {
|
when (entry) {
|
||||||
entry is QueueEntry.StreamingTrack && current is QueueEntry.StreamingTrack ->
|
is QueueEntry.StreamingTrack if current is QueueEntry.StreamingTrack ->
|
||||||
entry.track.id == current.track.id
|
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
|
entry.url == current.url && entry.name == current.name
|
||||||
|
|
||||||
else -> false
|
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.navigation.navigationModule
|
||||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||||
import dev.krtirtho.spotube.core.server.AlternativeTracksRepository
|
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.LocalServer
|
||||||
import dev.krtirtho.spotube.core.server.MatchedTracksRepository
|
import dev.krtirtho.spotube.core.server.MatchedTracksRepository
|
||||||
import dev.krtirtho.spotube.core.server.StreamingUrlRepository
|
import dev.krtirtho.spotube.core.server.StreamingUrlRepository
|
||||||
@ -190,6 +191,7 @@ val sharedModules = module {
|
|||||||
}
|
}
|
||||||
singleOf(::MatchedTracksRepository)
|
singleOf(::MatchedTracksRepository)
|
||||||
singleOf(::StreamingUrlRepository)
|
singleOf(::StreamingUrlRepository)
|
||||||
|
singleOf(::CacheManager)
|
||||||
singleOf(::AlternativeTracksRepository)
|
singleOf(::AlternativeTracksRepository)
|
||||||
singleOf(::LocalServer) withOptions {
|
singleOf(::LocalServer) withOptions {
|
||||||
createdAtStart()
|
createdAtStart()
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class AlternativeTracksRepository(
|
|||||||
private val matchedTracksRepository: MatchedTracksRepository,
|
private val matchedTracksRepository: MatchedTracksRepository,
|
||||||
private val streamingUrlRepository: StreamingUrlRepository,
|
private val streamingUrlRepository: StreamingUrlRepository,
|
||||||
private val audioPlayerQueue: AudioPlayerQueue,
|
private val audioPlayerQueue: AudioPlayerQueue,
|
||||||
|
private val cacheManager: CacheManager,
|
||||||
) : KoinComponent {
|
) : KoinComponent {
|
||||||
|
|
||||||
private val logger by injectLogger<AlternativeTracksRepository>()
|
private val logger by injectLogger<AlternativeTracksRepository>()
|
||||||
@ -78,6 +79,7 @@ class AlternativeTracksRepository(
|
|||||||
matchedTracksRepository.saveTrackSource(track, basic)
|
matchedTracksRepository.saveTrackSource(track, basic)
|
||||||
streamingUrlRepository.invalidateCachedStreamUrl(trackId)
|
streamingUrlRepository.invalidateCachedStreamUrl(trackId)
|
||||||
streamingUrlRepository.invalidateCachedAlternatives(trackId)
|
streamingUrlRepository.invalidateCachedAlternatives(trackId)
|
||||||
|
cacheManager.invalidateCacheEntry(trackId)
|
||||||
audioPlayerQueue.reloadCurrent()
|
audioPlayerQueue.reloadCurrent()
|
||||||
logger.d { "Alternative source selection complete for track $trackId" }
|
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.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import dev.krtirtho.spotube.core.paths.Paths
|
import dev.krtirtho.spotube.core.paths.Paths
|
||||||
|
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.Headers
|
import io.ktor.http.Headers
|
||||||
import io.ktor.http.HeadersBuilder
|
import io.ktor.http.HeadersBuilder
|
||||||
@ -40,12 +41,12 @@ import okio.buffer
|
|||||||
import okio.use
|
import okio.use
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
internal data class CacheIndex(
|
data class CacheIndex(
|
||||||
val entries: List<CacheEntry> = emptyList()
|
val entries: List<CacheEntry> = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
internal data class CacheEntry(
|
data class CacheEntry(
|
||||||
val trackId: String,
|
val trackId: String,
|
||||||
val filename: String,
|
val filename: String,
|
||||||
val sizeBytes: Long,
|
val sizeBytes: Long,
|
||||||
@ -53,18 +54,26 @@ internal data class CacheEntry(
|
|||||||
val contentType: String = "application/octet-stream",
|
val contentType: String = "application/octet-stream",
|
||||||
)
|
)
|
||||||
|
|
||||||
internal class CacheManager(
|
class CacheManager(
|
||||||
private val paths: Paths,
|
private val paths: Paths,
|
||||||
private val resolveCacheFolder: () -> String?,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val resolveSizeLimitMB: () -> Long,
|
|
||||||
private val fileSystem: FileSystem = FileSystem.SYSTEM,
|
|
||||||
private val cacheMutex: Mutex = Mutex(),
|
|
||||||
private val json: Json = Json { ignoreUnknownKeys = true },
|
|
||||||
) {
|
) {
|
||||||
|
private val fileSystem: FileSystem = FileSystem.SYSTEM
|
||||||
|
private val cacheMutex: Mutex = Mutex()
|
||||||
|
private val json: Json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val cacheIndexFileName = "cache_index.json".toPath()
|
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 {
|
fun resolveCacheDir(): Path {
|
||||||
val folder = resolveCacheFolder()
|
val folder = resolveCacheFolder()
|
||||||
return if (!folder.isNullOrBlank()) {
|
return if (!folder.isNullOrBlank()) {
|
||||||
@ -101,6 +110,18 @@ internal class CacheManager(
|
|||||||
return if (fileSystem.exists(filePath)) filePath to entry else null
|
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 {
|
fun resolveCacheFilename(track: MetadataTrack, contentType: String?): String {
|
||||||
val artists = track.artists.joinToString(", ") { it.name }.sanitizeFilenamePart()
|
val artists = track.artists.joinToString(", ") { it.name }.sanitizeFilenamePart()
|
||||||
val title = track.title.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.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.di.injectLogger
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
import dev.krtirtho.spotube.core.paths.Paths
|
|
||||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.http.HttpMethod
|
||||||
@ -52,10 +51,10 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
class LocalServer(
|
class LocalServer(
|
||||||
private val paths: Paths,
|
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
private val streamingUrlRepository: StreamingUrlRepository,
|
private val streamingUrlRepository: StreamingUrlRepository,
|
||||||
private val audioPlayerQueue: AudioPlayerQueue,
|
private val audioPlayerQueue: AudioPlayerQueue,
|
||||||
|
private val cacheManager: CacheManager,
|
||||||
) : KoinComponent {
|
) : KoinComponent {
|
||||||
|
|
||||||
val logger by injectLogger<LocalServer>()
|
val logger by injectLogger<LocalServer>()
|
||||||
@ -76,14 +75,6 @@ class LocalServer(
|
|||||||
}.stateIn(scope, SharingStarted.WhileSubscribed(5_000), null)
|
}.stateIn(scope, SharingStarted.WhileSubscribed(5_000), null)
|
||||||
|
|
||||||
private val cachedCacheEnabled = MutableStateFlow(false)
|
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 {
|
private val streamProxy by lazy {
|
||||||
StreamProxy(
|
StreamProxy(
|
||||||
@ -94,7 +85,6 @@ class LocalServer(
|
|||||||
isCachingEnabled = { cachedCacheEnabled.value },
|
isCachingEnabled = { cachedCacheEnabled.value },
|
||||||
activePort = { activePort.value },
|
activePort = { activePort.value },
|
||||||
scope = scope,
|
scope = scope,
|
||||||
logger = logger,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,8 +108,6 @@ class LocalServer(
|
|||||||
.collect { settings ->
|
.collect { settings ->
|
||||||
if (settings != null) {
|
if (settings != null) {
|
||||||
cachedCacheEnabled.value = settings.enableMusicCaching
|
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.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.request
|
import io.ktor.client.request.request
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
@ -51,6 +52,7 @@ import okio.Path.Companion.toPath
|
|||||||
import okio.SYSTEM
|
import okio.SYSTEM
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.use
|
import okio.use
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
internal class StreamProxy(
|
internal class StreamProxy(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
@ -60,8 +62,9 @@ internal class StreamProxy(
|
|||||||
private val isCachingEnabled: () -> Boolean,
|
private val isCachingEnabled: () -> Boolean,
|
||||||
private val activePort: () -> Int?,
|
private val activePort: () -> Int?,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val logger: co.touchlab.kermit.Logger,
|
): KoinComponent {
|
||||||
) {
|
val logger by injectLogger<StreamProxy>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val HOST = "127.0.0.1"
|
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.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import dev.krtirtho.spotube.core.db.Database
|
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.coroutines.flow.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
@ -28,14 +32,20 @@ class SettingsRepository(private val database: Database) {
|
|||||||
private val SETTINGS_KEY = stringPreferencesKey("user_settings")
|
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]
|
val json = prefs[SETTINGS_KEY]
|
||||||
if (json != null) {
|
if (json != null) {
|
||||||
Json.decodeFromString<UserSettings>(json as String)
|
Json.decodeFromString<UserSettings>(json as String)
|
||||||
} else {
|
} else {
|
||||||
UserSettings() // Default value
|
UserSettings() // Default value
|
||||||
}
|
}
|
||||||
}
|
}.stateIn(
|
||||||
|
scope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = UserSettings() // Default value
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun updateSettings(newSettings: UserSettings) {
|
suspend fun updateSettings(newSettings: UserSettings) {
|
||||||
database.settingsDataStore.edit { prefs ->
|
database.settingsDataStore.edit { prefs ->
|
||||||
|
|||||||
@ -32,11 +32,7 @@ interface SettingsProvider {
|
|||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
private val repository: SettingsRepository
|
private val repository: SettingsRepository
|
||||||
) : ViewModel(), SettingsProvider {
|
) : ViewModel(), SettingsProvider {
|
||||||
override val settingsState: StateFlow<UserSettings?> = repository.userSettings.stateIn(
|
override val settingsState = repository.userSettings
|
||||||
viewModelScope,
|
|
||||||
SharingStarted.WhileSubscribed(5000),
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun updateSettings(transform: UserSettings.() -> UserSettings) {
|
fun updateSettings(transform: UserSettings.() -> UserSettings) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
@ -95,8 +95,9 @@ class AlternativeTrackContentViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectAlternative(source: AudioSource) {
|
fun selectAlternative(source: AudioSource) {
|
||||||
val currentTrack = (audioPlayerQueue.currentQueueEntryFlow.value as? QueueEntry.StreamingTrack)?.track
|
val currentTrack =
|
||||||
if (currentTrack == null) return
|
(audioPlayerQueue.currentQueueEntryFlow.value as? QueueEntry.StreamingTrack)?.track
|
||||||
|
?: return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
alternativeTracksRepository.selectAlternative(currentTrack, source)
|
alternativeTracksRepository.selectAlternative(currentTrack, source)
|
||||||
activeSourceIdFlow.value = source.id
|
activeSourceIdFlow.value = source.id
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user