Skip to content

Commit acfe3fe

Browse files
committed
fix: recently played stats not updated for first played song #58
1 parent 7fa9986 commit acfe3fe

2 files changed

Lines changed: 63 additions & 6 deletions

File tree

engine/src/main/java/app/simple/felicity/engine/managers/MediaPlaybackManager.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,9 @@ object MediaPlaybackManager {
137137
* the first real [notifyCurrentPosition] for the new queue is processed normally.
138138
*
139139
* All access is on the main thread.
140-
*
141-
* @author Hamza417
142140
*/
143-
private var isQueueBeingReplaced: Boolean = false
141+
var isQueueBeingReplaced: Boolean = false
142+
private set
144143

145144
/**
146145
* The most recently emitted playback state constant from [notifyPlaybackState].

engine/src/main/java/app/simple/felicity/engine/services/FelicityPlayerService.kt

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ class FelicityPlayerService : MediaLibraryService(), SharedPreferences.OnSharedP
125125
*/
126126
private var wasPlayingBeforeTransition: Boolean = false
127127

128+
/**
129+
* Holds the media ID of a song that needs its play recorded once the player actually
130+
* starts playing. This happens when a new queue is set — ExoPlayer fires the item
131+
* transition callback before [Player.play] is called, so [Player.playWhenReady] is
132+
* still false at that moment and the play would be silently missed. We park the ID here
133+
* and flush it as soon as playback begins.
134+
*/
135+
private var pendingPlayRecordMediaId: String? = null
136+
128137
/**
129138
* The duration of the song that is CURRENTLY loaded and ready to play, kept fresh by
130139
* reading [player.duration] once the player reaches STATE_READY (the only state where
@@ -838,6 +847,24 @@ class FelicityPlayerService : MediaLibraryService(), SharedPreferences.OnSharedP
838847
}
839848

840849
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
850+
if (playWhenReady) {
851+
// If there's a song that was waiting for playback to start, record its play now.
852+
// This covers the case where a new queue is created and play() is called after
853+
// the item transition callback already fired (with playWhenReady still false).
854+
val deferredMediaId = pendingPlayRecordMediaId
855+
if (deferredMediaId != null) {
856+
pendingPlayRecordMediaId = null
857+
val audioId = deferredMediaId.toLongOrNull()
858+
if (audioId != null) {
859+
serviceScope.launch(Dispatchers.IO) {
860+
val audio = audioRepository.getAudioById(audioId) ?: return@launch
861+
songStatRepository.recordPlay(audio.hash)
862+
Log.d(TAG, "Deferred play recorded for: ${audio.title}")
863+
}
864+
}
865+
}
866+
}
867+
841868
if (AudioPreferences.isGaplessPlaybackEnabled().not()) {
842869
if (!playWhenReady && reason == Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM) {
843870
// The track ended and the player paused itself automatically.
@@ -999,7 +1026,13 @@ class FelicityPlayerService : MediaLibraryService(), SharedPreferences.OnSharedP
9991026
// Also check whether this was a backward navigation and count it as a replay if so.
10001027
mediaItem?.let { item ->
10011028
previousItemMediaId = item.mediaId
1002-
if (player.playWhenReady) {
1029+
// While a queue replacement is in flight, ExoPlayer fires intermediate transitions
1030+
// (e.g. the first item of the new list lands before seekTo moves to the real target).
1031+
// We must not record those as plays — instead park the ID so it can be flushed once
1032+
// the replacement is complete and the intended song actually starts playing.
1033+
val replacingQueue = MediaPlaybackManager.isQueueBeingReplaced
1034+
if (player.playWhenReady && !replacingQueue) {
1035+
pendingPlayRecordMediaId = null
10031036
val audioId = item.mediaId.toLongOrNull() ?: return@let
10041037
val isBackwardNavigation = !MediaPlaybackManager.lastNavigationDirection
10051038
serviceScope.launch(Dispatchers.IO) {
@@ -1014,11 +1047,36 @@ class FelicityPlayerService : MediaLibraryService(), SharedPreferences.OnSharedP
10141047
}
10151048
}
10161049
} else {
1017-
Log.d(TAG, "Track changed while paused — skipping play stat for: ${item.mediaId}")
1050+
// Either the player isn't set to play yet, or the queue is still being
1051+
// replaced — park the ID and wait for playback to actually begin.
1052+
pendingPlayRecordMediaId = item.mediaId
1053+
Log.d(TAG, "Deferring play stat for: ${item.mediaId} (playWhenReady=${player.playWhenReady}, replacingQueue=$replacingQueue)")
10181054
}
1019-
} ?: run { previousItemMediaId = null }
1055+
} ?: run {
1056+
previousItemMediaId = null
1057+
pendingPlayRecordMediaId = null
1058+
}
10201059

10211060
MediaPlaybackManager.notifyCurrentPosition(player.currentMediaItemIndex)
1061+
1062+
// After notifyCurrentPosition runs, the queue-replacement guard may have just been
1063+
// lifted (it clears itself once ExoPlayer confirms the intended seek position).
1064+
// If the player is already set to play, and we still have a parked ID, this is our
1065+
// window to flush it — onPlayWhenReadyChanged won't fire because playWhenReady
1066+
// never went false during an already-playing queue swap.
1067+
val deferredId = pendingPlayRecordMediaId
1068+
if (deferredId != null && player.playWhenReady && !MediaPlaybackManager.isQueueBeingReplaced) {
1069+
pendingPlayRecordMediaId = null
1070+
val audioId = deferredId.toLongOrNull()
1071+
if (audioId != null) {
1072+
serviceScope.launch(Dispatchers.IO) {
1073+
val audio = audioRepository.getAudioById(audioId) ?: return@launch
1074+
songStatRepository.recordPlay(audio.hash)
1075+
Log.d(TAG, "Deferred play flushed (post-replace) for: ${audio.title}")
1076+
}
1077+
}
1078+
}
1079+
10221080
savePlaybackStateToDatabase() // Save when track changes
10231081
buildAndPushSnapshot()
10241082
broadcastWidgetUpdate()

0 commit comments

Comments
 (0)