When streaming an audiobook from the server and the network connection drops, playback progress is lost and the app does not automatically switch to a downloaded local copy. This PR fixes both issues by saving server progress locally as a fallback and by automatically switching to a local copy when the network is lost.
There are two problems when streaming from the server and the network drops mid-playback:
Problem 1: Progress is lost
In MediaProgressSyncer.sync(), the server-item code path only syncs progress to the server. When the network is down, it logs "Not sending progress to server" and discards the progress entirely -- nothing is saved locally. This means the user loses their listening position and has to manually find their place again.
Fix: When a server item cannot sync (network down or server sync failure), the progress is now saved to the local database as a LocalMediaProgress entry with a server-offline- prefixed ID. This ensures the listening position is always persisted. These offline entries are cleaned up naturally by the existing DB cleanup cycle after the progress is synced back to the server.
Problem 2: No automatic fallback to local copy
When the network drops during server streaming, the player stalls and eventually errors out. The only existing fallback is direct play to transcode (HLS), which also requires a server connection. There is no fallback to a downloaded local copy of the same audiobook.
Fix: A new switchToLocalCopyIfAvailable() method checks if a local copy of the currently streaming audiobook exists (via getLocalLibraryItemByLId). If found, it seamlessly switches playback to the local copy at the same position. This is triggered in two places:
Proactively in the networkCallback.onLost() -- when the system detects network loss, the app immediately switches to the local copy before the player buffers run out.
Reactively in handlePlayerPlaybackError() -- if a playback error occurs on a server item, the app tries switching to a local copy before falling back to transcode or failing.
For podcasts, the correct local episode is matched via serverEpisodeId. The playback position (currentTime) is preserved across the switch since both server and local sessions use the same absolute time coordinate system.
Files changed
MediaProgressSyncer.kt -- Added saveServerProgressLocally() method; called when network is down or server sync fails in the non-local branch
PlayerNotificationService.kt -- Added switchToLocalCopyIfAvailable() method; added onLost to networkCallback; modified handlePlayerPlaybackError() to try local copy first
How have you tested this?
Stream an audiobook from the server that also has a downloaded local copy
Disable network (airplane mode / WiFi off) during playback
Verify: playback switches seamlessly to the local copy at the same position
Verify: progress is saved locally and visible in the app
Re-enable network
Verify: progress syncs back to the server
Also tested:
Streaming without a local copy available: player behaves as before (stalls/errors, no crash)
Network loss while not playing: no unnecessary switch attempt
Podcast episode streaming with local copy: correct episode matched and switched
Screenshots
N/A -- backend changes only, no UI modifications.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/advplyr/audiobookshelf-app/pull/1835
**Author:** [@ckbaker10](https://github.com/ckbaker10)
**Created:** 3/30/2026
**Status:** 🔄 Open
**Base:** `master` ← **Head:** `fix-progress-save-error`
---
### 📝 Commits (2)
- [`c3f952f`](https://github.com/advplyr/audiobookshelf-app/commit/c3f952f290d0dc5a5b97daa765b0dbd61f8641d2) Fix local saving during network loss and playback switch
- [`7d70fa6`](https://github.com/advplyr/audiobookshelf-app/commit/7d70fa6f89ca52a88ea850ed08d0539dd65e1055) Wrong thread
### 📊 Changes
**2 files changed** (+114 additions, -1 deletions)
<details>
<summary>View changed files</summary>
📝 `android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt` (+54 -0)
📝 `android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt` (+60 -1)
</details>
### 📄 Description
## Brief summary
When streaming an audiobook from the server and the network connection drops, playback progress is lost and the app does not automatically switch to a downloaded local copy. This PR fixes both issues by saving server progress locally as a fallback and by automatically switching to a local copy when the network is lost.
## Which issue is fixed?
<!-- TODO: Link issue number if applicable, e.g. Fixes #1234 -->
## Pull Request Type
- Affects: Android only
- Changes: Backend (native Kotlin player/sync logic)
## In-depth Description
There are two problems when streaming from the server and the network drops mid-playback:
### Problem 1: Progress is lost
In `MediaProgressSyncer.sync()`, the server-item code path only syncs progress to the server. When the network is down, it logs "Not sending progress to server" and discards the progress entirely -- nothing is saved locally. This means the user loses their listening position and has to manually find their place again.
**Fix:** When a server item cannot sync (network down or server sync failure), the progress is now saved to the local database as a `LocalMediaProgress` entry with a `server-offline-` prefixed ID. This ensures the listening position is always persisted. These offline entries are cleaned up naturally by the existing DB cleanup cycle after the progress is synced back to the server.
### Problem 2: No automatic fallback to local copy
When the network drops during server streaming, the player stalls and eventually errors out. The only existing fallback is direct play to transcode (HLS), which also requires a server connection. There is no fallback to a downloaded local copy of the same audiobook.
**Fix:** A new `switchToLocalCopyIfAvailable()` method checks if a local copy of the currently streaming audiobook exists (via `getLocalLibraryItemByLId`). If found, it seamlessly switches playback to the local copy at the same position. This is triggered in two places:
1. **Proactively** in the `networkCallback.onLost()` -- when the system detects network loss, the app immediately switches to the local copy before the player buffers run out.
2. **Reactively** in `handlePlayerPlaybackError()` -- if a playback error occurs on a server item, the app tries switching to a local copy before falling back to transcode or failing.
For podcasts, the correct local episode is matched via `serverEpisodeId`. The playback position (`currentTime`) is preserved across the switch since both server and local sessions use the same absolute time coordinate system.
### Files changed
- `MediaProgressSyncer.kt` -- Added `saveServerProgressLocally()` method; called when network is down or server sync fails in the non-local branch
- `PlayerNotificationService.kt` -- Added `switchToLocalCopyIfAvailable()` method; added `onLost` to `networkCallback`; modified `handlePlayerPlaybackError()` to try local copy first
## How have you tested this?
1. Stream an audiobook from the server that also has a downloaded local copy
2. Disable network (airplane mode / WiFi off) during playback
3. Verify: playback switches seamlessly to the local copy at the same position
4. Verify: progress is saved locally and visible in the app
5. Re-enable network
6. Verify: progress syncs back to the server
Also tested:
- Streaming without a local copy available: player behaves as before (stalls/errors, no crash)
- Network loss while not playing: no unnecessary switch attempt
- Podcast episode streaming with local copy: correct episode matched and switched
## Screenshots
N/A -- backend changes only, no UI modifications.
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/advplyr/audiobookshelf-app/pull/1835
Author: @ckbaker10
Created: 3/30/2026
Status: 🔄 Open
Base:
master← Head:fix-progress-save-error📝 Commits (2)
c3f952fFix local saving during network loss and playback switch7d70fa6Wrong thread📊 Changes
2 files changed (+114 additions, -1 deletions)
View changed files
📝
android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt(+54 -0)📝
android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt(+60 -1)📄 Description
Brief summary
When streaming an audiobook from the server and the network connection drops, playback progress is lost and the app does not automatically switch to a downloaded local copy. This PR fixes both issues by saving server progress locally as a fallback and by automatically switching to a local copy when the network is lost.
Which issue is fixed?
Pull Request Type
In-depth Description
There are two problems when streaming from the server and the network drops mid-playback:
Problem 1: Progress is lost
In
MediaProgressSyncer.sync(), the server-item code path only syncs progress to the server. When the network is down, it logs "Not sending progress to server" and discards the progress entirely -- nothing is saved locally. This means the user loses their listening position and has to manually find their place again.Fix: When a server item cannot sync (network down or server sync failure), the progress is now saved to the local database as a
LocalMediaProgressentry with aserver-offline-prefixed ID. This ensures the listening position is always persisted. These offline entries are cleaned up naturally by the existing DB cleanup cycle after the progress is synced back to the server.Problem 2: No automatic fallback to local copy
When the network drops during server streaming, the player stalls and eventually errors out. The only existing fallback is direct play to transcode (HLS), which also requires a server connection. There is no fallback to a downloaded local copy of the same audiobook.
Fix: A new
switchToLocalCopyIfAvailable()method checks if a local copy of the currently streaming audiobook exists (viagetLocalLibraryItemByLId). If found, it seamlessly switches playback to the local copy at the same position. This is triggered in two places:networkCallback.onLost()-- when the system detects network loss, the app immediately switches to the local copy before the player buffers run out.handlePlayerPlaybackError()-- if a playback error occurs on a server item, the app tries switching to a local copy before falling back to transcode or failing.For podcasts, the correct local episode is matched via
serverEpisodeId. The playback position (currentTime) is preserved across the switch since both server and local sessions use the same absolute time coordinate system.Files changed
MediaProgressSyncer.kt-- AddedsaveServerProgressLocally()method; called when network is down or server sync fails in the non-local branchPlayerNotificationService.kt-- AddedswitchToLocalCopyIfAvailable()method; addedonLosttonetworkCallback; modifiedhandlePlayerPlaybackError()to try local copy firstHow have you tested this?
Also tested:
Screenshots
N/A -- backend changes only, no UI modifications.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.