@@ -358,6 +358,9 @@ class MusicService :
358358 // URL cache for stream URLs - class-level so it can be invalidated on errors
359359 private val songUrlCache = HashMap <String , Pair <String , Long >>()
360360
361+ // Flag to bypass cache when quality changes - forces fresh stream fetch
362+ private val bypassCacheForQualityChange = mutableSetOf<String >()
363+
361364 // Enhanced error tracking for strict retry management
362365 private var currentMediaIdRetryCount = mutableMapOf<String , Int >()
363366 private val MAX_RETRY_PER_SONG = 3
@@ -538,6 +541,63 @@ class MusicService :
538541 }
539542 }
540543
544+ // Watch for audio quality setting changes
545+ var isFirstQualityEmit = true
546+ scope.launch {
547+ dataStore.data
548+ .map { it[AudioQualityKey ]?.let { value ->
549+ com.metrolist.music.constants.AudioQuality .entries.find { it.name == value }
550+ } ? : com.metrolist.music.constants.AudioQuality .AUTO }
551+ .distinctUntilChanged()
552+ .collect { newQuality ->
553+ val oldQuality = audioQuality
554+ audioQuality = newQuality
555+
556+ // Skip reload on first emit (app startup)
557+ if (isFirstQualityEmit) {
558+ isFirstQualityEmit = false
559+ Timber .tag(" MusicService" ).i(" QUALITY INIT: $newQuality " )
560+ return @collect
561+ }
562+
563+ Timber .tag(" MusicService" ).i(" QUALITY CHANGED: $oldQuality -> $newQuality " )
564+
565+ // Reload current song with new quality
566+ val mediaId = player.currentMediaItem?.mediaId ? : return @collect
567+ val currentPosition = player.currentPosition
568+ val wasPlaying = player.isPlaying
569+ val currentIndex = player.currentMediaItemIndex
570+
571+ Timber .tag(" MusicService" ).i(" RELOADING STREAM: $mediaId at position ${currentPosition} ms" )
572+
573+ // Clear cached URL to force fresh fetch
574+ songUrlCache.remove(mediaId)
575+
576+ // CRITICAL: Clear caches synchronously to prevent format parsing errors
577+ runBlocking(Dispatchers .IO ) {
578+ try {
579+ playerCache.removeResource(mediaId)
580+ downloadCache.removeResource(mediaId)
581+ Timber .tag(" MusicService" ).d(" Cleared player and download cache for $mediaId " )
582+ } catch (e: Exception ) {
583+ Timber .tag(" MusicService" ).e(e, " Failed to clear cache for $mediaId " )
584+ }
585+ }
586+
587+ // Set bypass flag so resolver skips cache checks
588+ bypassCacheForQualityChange.add(mediaId)
589+ Timber .tag(" MusicService" ).d(" Set bypass cache flag for $mediaId " )
590+
591+ // Reload player at same position
592+ player.stop()
593+ player.seekTo(currentIndex, currentPosition)
594+ player.prepare()
595+ if (wasPlaying) {
596+ player.play()
597+ }
598+ }
599+ }
600+
541601 combine(playerVolume, isMuted) { volume, muted ->
542602 if (muted) 0f else volume
543603 }.collectLatest(scope) {
@@ -2564,22 +2624,30 @@ class MusicService :
25642624 return ResolvingDataSource .Factory (createCacheDataSource()) { dataSpec ->
25652625 val mediaId = dataSpec.key ? : error(" No media id" )
25662626
2567- if (downloadCache.isCached(
2568- mediaId,
2569- dataSpec.position,
2570- if (dataSpec.length >= 0 ) dataSpec.length else 1
2571- ) ||
2572- playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH )
2573- ) {
2574- scope.launch(Dispatchers .IO ) { recoverSong(mediaId) }
2575- return @Factory dataSpec
2576- }
2627+ // Check if we need to bypass cache for quality change
2628+ val shouldBypassCache = bypassCacheForQualityChange.contains(mediaId)
25772629
2578- songUrlCache[mediaId]?.takeIf { it.second > System .currentTimeMillis() }?.let {
2579- scope.launch(Dispatchers .IO ) { recoverSong(mediaId) }
2580- return @Factory dataSpec.withUri(it.first.toUri())
2630+ if (! shouldBypassCache) {
2631+ if (downloadCache.isCached(
2632+ mediaId,
2633+ dataSpec.position,
2634+ if (dataSpec.length >= 0 ) dataSpec.length else 1
2635+ ) ||
2636+ playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH )
2637+ ) {
2638+ scope.launch(Dispatchers .IO ) { recoverSong(mediaId) }
2639+ return @Factory dataSpec
2640+ }
2641+
2642+ songUrlCache[mediaId]?.takeIf { it.second > System .currentTimeMillis() }?.let {
2643+ scope.launch(Dispatchers .IO ) { recoverSong(mediaId) }
2644+ return @Factory dataSpec.withUri(it.first.toUri())
2645+ }
2646+ } else {
2647+ Timber .tag(" MusicService" ).i(" BYPASSING CACHE for $mediaId due to quality change" )
25812648 }
25822649
2650+ Timber .tag(" MusicService" ).i(" FETCHING STREAM: $mediaId | quality=$audioQuality " )
25832651 val playbackData = runBlocking(Dispatchers .IO ) {
25842652 YTPlayerUtils .playerResponseForPlayback(
25852653 mediaId,
@@ -2645,6 +2713,11 @@ class MusicService :
26452713 }
26462714 scope.launch(Dispatchers .IO ) { recoverSong(mediaId, nonNullPlayback) }
26472715
2716+ // Clear bypass flag now that we've fetched fresh stream
2717+ if (bypassCacheForQualityChange.remove(mediaId)) {
2718+ Timber .tag(" MusicService" ).d(" Cleared bypass cache flag for $mediaId after fresh fetch" )
2719+ }
2720+
26482721 val streamUrl = nonNullPlayback.streamUrl
26492722
26502723 songUrlCache[mediaId] =
0 commit comments