From 8be08702c1e05ab6390217a779de0bcaa95f4426 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 9 May 2026 16:48:55 -0500 Subject: [PATCH 1/3] Fix regain focus check server progress for local media items open in player & sync more recent --- components/app/AudioPlayer.vue | 31 +++--- components/app/AudioPlayerContainer.vue | 132 +++++++++++++++++------- plugins/server.js | 2 +- 3 files changed, 113 insertions(+), 52 deletions(-) diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 69c1188b..907a4587 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -69,22 +69,22 @@
- first_page -
+ first_page +
replay {{ jumpBackwardsLabel }}
- {{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }} + {{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}
-
+
forward_media {{ jumpForwardLabel }}
- last_page + last_page
@@ -94,7 +94,7 @@

{{ timeRemainingPretty }}

-
+
@@ -157,6 +157,7 @@ export default { lockUi: false }, isLoading: false, + isCheckingServerProgress: false, isDraggingCursor: false, draggingTouchStartX: 0, draggingTouchStartTime: 0, @@ -270,6 +271,9 @@ export default { return 190 * heightScale } }, + showLoadingState() { + return this.isLoading || this.isCheckingServerProgress + }, showCastBtn() { return this.$store.state.isCastAvailable }, @@ -461,13 +465,13 @@ export default { }, async jumpNextChapter() { await this.$hapticsImpact() - if (this.isLoading) return + if (this.showLoadingState) return if (!this.nextChapter) return this.seek(this.nextChapter.start) }, async jumpChapterStart() { await this.$hapticsImpact() - if (this.isLoading) return + if (this.showLoadingState) return if (!this.currentChapter) { return this.restart() } @@ -497,12 +501,12 @@ export default { }, async jumpBackwards() { await this.$hapticsImpact() - if (this.isLoading) return + if (this.showLoadingState) return AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime }) }, async jumpForward() { await this.$hapticsImpact() - if (this.isLoading) return + if (this.showLoadingState) return AbsAudioPlayer.seekForward({ value: this.jumpForwardTime }) }, setStreamReady() { @@ -606,7 +610,7 @@ export default { } }, seek(time) { - if (this.isLoading) return + if (this.showLoadingState) return if (this.seekLoading) { console.error('Already seek loading', this.seekedTime) return @@ -638,11 +642,14 @@ export default { }, async playPauseClick() { await this.$hapticsImpact() - if (this.isLoading) return + if (this.showLoadingState) return this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing this.isEnded = false }, + setIsCheckingServerProgress(value) { + this.isCheckingServerProgress = !!value + }, play() { AbsAudioPlayer.playPlayer() this.startPlayInterval() diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 1040e61b..1dce3f11 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -304,48 +304,102 @@ export default { this.$refs.audioPlayer?.seek(currentTime) }, /** - * When device gains focus then refresh the timestamps in the audio player + * Fetch the current user's media progress from the server for a given library item / episode. + * Returns the server media progress object, or null if the request fails, times out, or the + * response doesn't match the requested library item. + * + * The audio player's loading state is shown while the request is in flight so the user + * doesn't tap play before we have a chance to update the timestamps. The request timeout + * is 7 seconds so a slow/unresponsive server doesn't block the user for long. */ - deviceFocused(hasFocus) { - if (!this.$store.state.currentPlaybackSession) return + async getServerMediaProgress({ libraryItemId, episodeId }) { + if (!libraryItemId) return null + const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}` - if (hasFocus) { + this.$refs.audioPlayer?.setIsCheckingServerProgress(true) + try { + const data = await this.$nativeHttp.get(url, { connectTimeout: 7000, readTimeout: 7000 }) + if (!data || data.libraryItemId !== libraryItemId) return null + return data + } catch (error) { + console.error('[AudioPlayerContainer] Failed to get server media progress', error) + return null + } finally { + this.$refs.audioPlayer?.setIsCheckingServerProgress(false) + } + }, + /** + * When device gains focus then refresh the timestamps in the audio player + * if local item is open then fetch the server media progress and update if more recent + */ + async deviceFocused(hasFocus) { + if (!this.$store.state.currentPlaybackSession) return + if (!hasFocus) return + // dont refresh timestamps if player is playing + if (this.$refs.audioPlayer?.isPlaying) return + + const playbackSession = this.$store.state.currentPlaybackSession + if (this.$refs.audioPlayer.isLocalPlayMethod) { + const localLibraryItemId = playbackSession.localLibraryItem?.id + const localEpisodeId = playbackSession.localEpisodeId + if (!localLibraryItemId) { + console.error('[AudioPlayerContainer] device visibility: no local library item for session', JSON.stringify(playbackSession)) + return + } + const localMediaProgress = this.$store.state.globals.localMediaProgress.find((mp) => { + if (localEpisodeId) return mp.localEpisodeId === localEpisodeId + return mp.localLibraryItemId === localLibraryItemId + }) + if (!localMediaProgress) { + console.error('[AudioPlayerContainer] device visibility: Local media progress not found') + return + } + + console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime) + this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime + this.$refs.audioPlayer.timeupdate() + + // If the local item came from a server and we're connected, check whether the + // server's progress is more recent (e.g. user kept listening on the server) + // and if so, update the player time and sync the server progress to local DB. + const serverLibraryItemId = playbackSession.libraryItemId + const serverEpisodeId = playbackSession.episodeId + if (!serverLibraryItemId || !this.$store.state.user.user || !this.$store.state.networkConnected) return + + console.log('[AudioPlayerContainer] device visibility: checking server media progress for local item', serverLibraryItemId, serverEpisodeId) + const data = await this.getServerMediaProgress({ libraryItemId: serverLibraryItemId, episodeId: serverEpisodeId }) + if (!data || !data.lastUpdate || data.lastUpdate <= localMediaProgress.lastUpdate) return + + console.log('[AudioPlayerContainer] device visibility: server progress is more recent for local item', data.currentTime, 'vs local', localMediaProgress.currentTime, `(server lastUpdate=${data.lastUpdate} > local lastUpdate=${localMediaProgress.lastUpdate})`) + if (!this.$refs.audioPlayer?.isPlaying && data.currentTime !== localMediaProgress.currentTime) { + // Use seek() so the native audio player's current session is updated + this.$refs.audioPlayer.seek(data.currentTime) + } + + try { + const newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress({ + localMediaProgressId: localMediaProgress.id, + mediaProgress: data + }) + if (newLocalMediaProgress?.id) { + this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress) + } + } catch (error) { + console.error('[AudioPlayerContainer] device visibility: Failed to sync server progress to local', error) + } + } else { + // server item so fetch server media progress and update player time + const libraryItemId = playbackSession.libraryItemId + const episodeId = playbackSession.episodeId + console.log('[AudioPlayerContainer] device visibility: checking server media progress for server item', libraryItemId, episodeId) + const data = await this.getServerMediaProgress({ libraryItemId, episodeId }) + if (!data) return if (!this.$refs.audioPlayer?.isPlaying) { - const playbackSession = this.$store.state.currentPlaybackSession - if (this.$refs.audioPlayer.isLocalPlayMethod) { - const localLibraryItemId = playbackSession.localLibraryItem?.id - const localEpisodeId = playbackSession.localEpisodeId - if (!localLibraryItemId) { - console.error('[AudioPlayerContainer] device visibility: no local library item for session', JSON.stringify(playbackSession)) - return - } - const localMediaProgress = this.$store.state.globals.localMediaProgress.find((mp) => { - if (localEpisodeId) return mp.localEpisodeId === localEpisodeId - return mp.localLibraryItemId === localLibraryItemId - }) - if (localMediaProgress) { - console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime) - this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime - this.$refs.audioPlayer.timeupdate() - } else { - console.error('[AudioPlayerContainer] device visibility: Local media progress not found') - } - } else { - const libraryItemId = playbackSession.libraryItemId - const episodeId = playbackSession.episodeId - const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}` - this.$nativeHttp - .get(url) - .then((data) => { - if (!this.$refs.audioPlayer?.isPlaying && data.libraryItemId === libraryItemId) { - console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime) - this.$refs.audioPlayer.currentTime = data.currentTime - this.$refs.audioPlayer.timeupdate() - } - }) - .catch((error) => { - console.error('[AudioPlayerContainer] device visibility: Failed to get progress', error) - }) + console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime) + // Only seek if the difference is greater than 1 second + if (Math.abs(data.currentTime - this.currentTime) > 1) { + // Use seek() so the native audio player's current session is updated + this.$refs.audioPlayer.seek(data.currentTime) } } } diff --git a/plugins/server.js b/plugins/server.js index 90f6f715..fccc06b9 100644 --- a/plugins/server.js +++ b/plugins/server.js @@ -110,7 +110,7 @@ class ServerSocket extends EventEmitter { } onAuthFailed(data) { - console.log('[SOCKET] Auth failed', data) + console.log('[SOCKET] Auth failed: ' + (data?.message || 'Unknown reason')) this.isAuthenticated = false } From 68dafbba7577b28059cf5326c8404d81d4205455 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 10 May 2026 11:05:17 -0500 Subject: [PATCH 2/3] Sync local media open in player when socket reconnects after 30s+ --- components/app/AudioPlayerContainer.vue | 122 ++++++++++++++---------- layouts/default.vue | 28 +++++- 2 files changed, 101 insertions(+), 49 deletions(-) diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 1dce3f11..6be88763 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -48,6 +48,9 @@ export default { }, isIos() { return this.$platform === 'ios' + }, + currentPlaybackSession() { + return this.$store.state.currentPlaybackSession } }, methods: { @@ -312,7 +315,10 @@ export default { * doesn't tap play before we have a chance to update the timestamps. The request timeout * is 7 seconds so a slow/unresponsive server doesn't block the user for long. */ - async getServerMediaProgress({ libraryItemId, episodeId }) { + async getServerMediaProgressForCurrentSession() { + if (!this.$store.state.user.user || !this.$store.state.networkConnected) return null + const libraryItemId = this.currentPlaybackSession?.libraryItemId + const episodeId = this.currentPlaybackSession?.episodeId if (!libraryItemId) return null const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}` @@ -328,28 +334,75 @@ export default { this.$refs.audioPlayer?.setIsCheckingServerProgress(false) } }, + getLocalMediaProgressForCurrentSession() { + if (!this.currentPlaybackSession) return null + return this.$store.getters['globals/getLocalMediaProgressById'](this.currentPlaybackSession.localLibraryItem?.id, this.currentPlaybackSession.localEpisodeId) + }, /** - * When device gains focus then refresh the timestamps in the audio player + * Sync the server media progress with the local media progress + */ + async syncServerMediaProgressWithLocalMediaProgress(localMediaProgressId, serverMediaProgress) { + try { + const newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress({ + localMediaProgressId, + mediaProgress: serverMediaProgress + }) + if (newLocalMediaProgress?.id) { + this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress) + } + } catch (error) { + console.error('[AudioPlayerContainer] Failed to sync server progress with local media progress', error) + } + }, + /** + * Check if the server media progress is more recent than the local media progress and sync if so + */ + async checkSyncServerProgressWithLocalProgress(localMediaProgress) { + if (!localMediaProgress) return + console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: checking server media progress for local media item open in player') + const serverMediaProgress = await this.getServerMediaProgressForCurrentSession() + if (!serverMediaProgress?.lastUpdate || serverMediaProgress.lastUpdate <= localMediaProgress.lastUpdate) return + + console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: server progress is more recent than local progress. Server current time:', serverMediaProgress.currentTime, 'vs local', localMediaProgress.currentTime, `(server lastUpdate=${serverMediaProgress.lastUpdate} > local lastUpdate=${localMediaProgress.lastUpdate})`) + if (!this.$refs.audioPlayer?.isPlaying && serverMediaProgress.currentTime !== localMediaProgress.currentTime) { + // Use seek() so the native audio player's current session is updated + this.$refs.audioPlayer.seek(serverMediaProgress.currentTime) + } + + await this.syncServerMediaProgressWithLocalMediaProgress(localMediaProgress.id, serverMediaProgress) + }, + /** + * When socket is reconnected after a delay, if a local media item is open in the player (paused) + * we fetch the server media progress and sync it if it is more recent than the local progress + * + * If there is no socket connection we may have missed external progress updates + */ + async socketReconnected() { + if (!this.currentPlaybackSession) return + // dont update timestamps if player is playing + if (this.$refs.audioPlayer?.isPlaying) return + + if (this.$refs.audioPlayer.isLocalPlayMethod) { + const localMediaProgress = this.getLocalMediaProgressForCurrentSession() + if (!localMediaProgress) { + console.error('[AudioPlayerContainer] socket reconnected: Local media progress not found') + return + } + + await this.checkSyncServerProgressWithLocalProgress(localMediaProgress) + } + }, + /** + * When device re-gains focus then refresh the timestamps in the audio player * if local item is open then fetch the server media progress and update if more recent */ async deviceFocused(hasFocus) { - if (!this.$store.state.currentPlaybackSession) return - if (!hasFocus) return - // dont refresh timestamps if player is playing + if (!this.currentPlaybackSession || !hasFocus) return + // dont update timestamps if player is playing if (this.$refs.audioPlayer?.isPlaying) return - const playbackSession = this.$store.state.currentPlaybackSession if (this.$refs.audioPlayer.isLocalPlayMethod) { - const localLibraryItemId = playbackSession.localLibraryItem?.id - const localEpisodeId = playbackSession.localEpisodeId - if (!localLibraryItemId) { - console.error('[AudioPlayerContainer] device visibility: no local library item for session', JSON.stringify(playbackSession)) - return - } - const localMediaProgress = this.$store.state.globals.localMediaProgress.find((mp) => { - if (localEpisodeId) return mp.localEpisodeId === localEpisodeId - return mp.localLibraryItemId === localLibraryItemId - }) + const localMediaProgress = this.getLocalMediaProgressForCurrentSession() if (!localMediaProgress) { console.error('[AudioPlayerContainer] device visibility: Local media progress not found') return @@ -359,40 +412,11 @@ export default { this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime this.$refs.audioPlayer.timeupdate() - // If the local item came from a server and we're connected, check whether the - // server's progress is more recent (e.g. user kept listening on the server) - // and if so, update the player time and sync the server progress to local DB. - const serverLibraryItemId = playbackSession.libraryItemId - const serverEpisodeId = playbackSession.episodeId - if (!serverLibraryItemId || !this.$store.state.user.user || !this.$store.state.networkConnected) return - - console.log('[AudioPlayerContainer] device visibility: checking server media progress for local item', serverLibraryItemId, serverEpisodeId) - const data = await this.getServerMediaProgress({ libraryItemId: serverLibraryItemId, episodeId: serverEpisodeId }) - if (!data || !data.lastUpdate || data.lastUpdate <= localMediaProgress.lastUpdate) return - - console.log('[AudioPlayerContainer] device visibility: server progress is more recent for local item', data.currentTime, 'vs local', localMediaProgress.currentTime, `(server lastUpdate=${data.lastUpdate} > local lastUpdate=${localMediaProgress.lastUpdate})`) - if (!this.$refs.audioPlayer?.isPlaying && data.currentTime !== localMediaProgress.currentTime) { - // Use seek() so the native audio player's current session is updated - this.$refs.audioPlayer.seek(data.currentTime) - } - - try { - const newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress({ - localMediaProgressId: localMediaProgress.id, - mediaProgress: data - }) - if (newLocalMediaProgress?.id) { - this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress) - } - } catch (error) { - console.error('[AudioPlayerContainer] device visibility: Failed to sync server progress to local', error) - } + await this.checkSyncServerProgressWithLocalProgress(localMediaProgress) } else { // server item so fetch server media progress and update player time - const libraryItemId = playbackSession.libraryItemId - const episodeId = playbackSession.episodeId - console.log('[AudioPlayerContainer] device visibility: checking server media progress for server item', libraryItemId, episodeId) - const data = await this.getServerMediaProgress({ libraryItemId, episodeId }) + console.log('[AudioPlayerContainer] device visibility: checking server media progress for server media item open in player') + const data = await this.getServerMediaProgressForCurrentSession() if (!data) return if (!this.$refs.audioPlayer?.isPlaying) { console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime) @@ -422,6 +446,7 @@ export default { this.$eventBus.$on('user-settings', this.settingsUpdated) this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$on('device-focus-update', this.deviceFocused) + this.$eventBus.$on('socket-reconnected', this.socketReconnected) }, beforeDestroy() { this.onLocalMediaProgressUpdateListener?.remove() @@ -437,6 +462,7 @@ export default { this.$eventBus.$off('user-settings', this.settingsUpdated) this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$off('device-focus-update', this.deviceFocused) + this.$eventBus.$off('socket-reconnected', this.socketReconnected) } } diff --git a/layouts/default.vue b/layouts/default.vue index aa621721..bed658ea 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -24,6 +24,7 @@ export default { inittingLibraries: false, hasMounted: false, disconnectTime: 0, + socketDisconnectedTime: 0, timeLostFocus: 0, currentLang: null } @@ -44,7 +45,7 @@ export default { } else { var timeSinceDisconnect = Date.now() - this.disconnectTime if (timeSinceDisconnect > 5000) { - console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server') + console.log('[default] Time since disconnect was', timeSinceDisconnect, 'sync with server') setTimeout(() => { this.syncLocalSessions(false) }, 4000) @@ -55,6 +56,28 @@ export default { this.disconnectTime = Date.now() } } + }, + socketConnected: { + handler(newVal, oldVal) { + if (!this.hasMounted) { + // watcher runs before mount, handling libraries/connection should be handled in mount + return + } + if (newVal) { + // if we havent been receiving socket events then external progress updates may have been missed + const timeSinceDisconnect = Date.now() - this.socketDisconnectedTime + if (timeSinceDisconnect > 30000 && this.isPlayerOpen) { + console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms and player is open, triggering server media progress sync') + // used for triggering a server media progress sync if local media item is open in player + this.$eventBus.$emit('socket-reconnected') + } else { + console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms') + } + } else { + console.log('[default] socket disconnected') + this.socketDisconnectedTime = Date.now() + } + } } }, computed: { @@ -67,6 +90,9 @@ export default { networkConnected() { return this.$store.state.networkConnected }, + socketConnected() { + return this.$store.state.socketConnected + }, user() { return this.$store.state.user.user }, From ed2f9d2e45faba865b51dea93ca105ab140ec4af Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 10 May 2026 11:12:17 -0500 Subject: [PATCH 3/3] Prevent focus and socket reconnect from both fetching server media progress --- components/app/AudioPlayerContainer.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 6be88763..d15e7179 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -320,6 +320,12 @@ export default { const libraryItemId = this.currentPlaybackSession?.libraryItemId const episodeId = this.currentPlaybackSession?.episodeId if (!libraryItemId) return null + + if (this.$refs.audioPlayer?.isCheckingServerProgress) { + console.log('[AudioPlayerContainer] getServerMediaProgressForCurrentSession: already checking server progress') + return null + } + const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}` this.$refs.audioPlayer?.setIsCheckingServerProgress(true)