mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-07-26 06:28:43 +02:00
[PR #1833] [CLOSED] Android: improve download throughput, safety, and concurrency #1715
Reference in New Issue
Block a user
📋 Pull Request Information
Original PR: https://github.com/advplyr/audiobookshelf-app/pull/1833
Author: @ricetim
Created: 3/28/2026
Status: ❌ Closed
Base:
master← Head:improve/download-perf-on-1587📝 Commits (10+)
feef527Delet unused fileee0ab43Auto formattingAbsDownloader.kt4a09238RefactorAbsDownloader19b1f3bRemove: unused json file from FolderScanner5d8574eSimplify local library item creation in folder scannerf31a8f0Revert "RefactorAbsDownloader"841527eSimplifyAbsDownloaderdownload item creation640d8cdCombine cover download logic7e2ac27Fix: cover printout for booke11b8cbInitial internal download continue incomplete download📊 Changes
8 files changed (+811 additions, -868 deletions)
View changed files
📝
android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt(+133 -113)➖
android/app/src/main/java/com/audiobookshelf/app/data/FolderScanResult.kt(+0 -15)📝
android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt(+189 -415)📝
android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt(+103 -135)📝
android/app/src/main/java/com/audiobookshelf/app/managers/InternalDownloadManager.kt(+104 -47)📝
android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt(+69 -50)📝
android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt(+212 -92)📝
strings/en-us.json(+1 -1)📄 Description
Brief summary
Improves Android download speed, correctness, and concurrent download behaviour. This PR is based on top of #1587 and is intended to complement that refactor — if #1587 merges first, the diff here collapses to just the two files changed in the final commit.
Which issue is fixed?
Related to #1167, #1428, #1479 (same issues referenced in #1587).
Pull Request Type
Android backend only. No frontend or iOS changes.
In-depth Description
All changes are in
InternalDownloadManager.ktandDownloadItemManager.kt.InternalDownloadManagerShared
OkHttpClientsingletonPreviously a new
OkHttpClientwas constructed perInternalDownloadManagerinstance, so each file download paid a fresh TLS handshake cost even when downloading multiple files from the same server. Moving it to acompanion objectwith aConnectionPool(5)reuses TCP connections across files.readTimeout(30s)Without a read timeout, if a server stops sending data while keeping the TCP connection open (e.g. a hung reverse proxy), the OkHttp dispatcher thread blocks on
inputStream.read()indefinitely — even after the stall detector inDownloadItemManagerhas already removed the part from the active list. 30 seconds is generous enough that even very slow connections never false-fire (sinceread()returns as soon as any bytes arrive, not when the full buffer is filled), while ensuring the thread is eventually released viaSocketTimeoutException.Buffer 8 KiB → 256 KiB, remove
BufferedInputStreamOkHttp/Okio already provides internal buffering on the response body stream; wrapping it in a
BufferedInputStreamwith an 8 KiB window adds a redundant copy. A 256 KiB read buffer reduces the number ofread()/write()syscall pairs by 32× for large audio files, which is the dominant overhead on a fast LAN connection.Catch
IOExceptioninsidewrite()The outer
try/catch(Exception)indownload()wraps the synchronous setup beforeenqueue(), not the async callback. IfoutputStream.write()throws (e.g. disk full mid-transfer), the exception propagates through the OkHttp dispatcher which catches and logs it silently —progressCallback.onComplete()is never called and the download slot is permanently leaked, preventing any further downloads from starting. CatchingIOExceptioninsidewrite()ensuresonComplete(true)always fires.Call
onProgressevery chunk — no throttlingThe Capacitor bridge is crossed in the watcher loop on its own 500ms schedule, not in
onProgress. ThrottlingonProgresswould starvelastUpdateTimeupdates and cause the stall detector (introduced in #1587) to false-fire mid-transfer at moderate download speeds.DownloadItemManagerCopyOnWriteArrayListfor both listsmutableListOf()is not thread-safe. The watcher coroutine iteratescurrentDownloadItemPartswhile OkHttp callback threads andDispatchers.Maincoroutines calladd()/remove(), which can produceConcurrentModificationExceptionin practice when downloading many files simultaneously.@Volatile isDownloadingWithout
@Volatile, the JIT can cache the value in a register. A coroutine that setsisDownloading = falseat the end of the watcher loop may never have that write seen by the thread checking it instartWatchingDownloads, causing a second watcher to silently never launch after the first one exits.@SynchronizedonaddDownloadItemandcheckUpdateDownloadQueueWhen a large series (e.g. 90 files) is queued, metadata callbacks for all 90 items can complete near-simultaneously, causing multiple threads to call
checkUpdateDownloadQueueconcurrently. Each thread seesdownloadId == nullfor the sameDownloadItemPartand starts a duplicateInternalDownloadManagerfor it — two OkHttp calls writing interleaved bytes to the same file, corrupting it, while progress oscillates as two independent byte counters advance.Round-robin slot distribution
With sequential slot filling, all available download slots are assigned to the first queued book before any slot is given to the second. Round-robin distributes one slot per book per pass, so all queued books make progress simultaneously.
How have you tested this?
Built the APK from this branch using GitHub Actions and sideloaded it onto an Android device. Downloaded a large multi-file audiobook series over LAN from a local Audiobookshelf instance and verified that:
Screenshots
N/A — backend only changes.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.