diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt index a3c3493f..2e3aa204 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt @@ -127,6 +127,11 @@ class DownloadItemManager( val internalProgressCallback = object : InternalProgressCallback { override fun onProgress(totalBytesWritten: Long, progress: Long) { + // Store time progress was last changed to prevent stale downloads from getting + // stuck in an infinite loop + if (downloadItemPart.bytesDownloaded != totalBytesWritten) { + downloadItemPart.lastUpdateTime = System.currentTimeMillis() + } downloadItemPart.bytesDownloaded = totalBytesWritten downloadItemPart.progress = progress } @@ -215,6 +220,21 @@ class DownloadItemManager( downloadItem?.let { checkDownloadItemFinished(it) } } currentDownloadItemParts.remove(downloadItemPart) + } else { + // Check for stalled downloads + val currentTimeMillis = System.currentTimeMillis() + val lastUpdateTime = downloadItemPart.lastUpdateTime ?: currentTimeMillis + val timeSinceLastUpdate = currentTimeMillis - lastUpdateTime + val stallTimeoutMillis = 10 * 1000 // 10 seconds + if (timeSinceLastUpdate > stallTimeoutMillis) { + Log.w( + tag, + "Download stalled for ${downloadItemPart.filename}, removing from download queue." + ) + downloadItemPart.failed = true + clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart) + currentDownloadItemParts.remove(downloadItemPart) + } } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt index 814b8544..ffd3548c 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt @@ -12,32 +12,45 @@ import com.fasterxml.jackson.annotation.JsonIgnore import java.io.File data class DownloadItemPart( - val id: String, - val downloadItemId: String, - val filename: String, - val fileSize: Long, - val finalDestinationPath:String, - val serverPath: String, - val localFolderName: String, - val localFolderUrl: String, - val localFolderId: String, - val ebookFile: EBookFile?, - val audioTrack: AudioTrack?, - val episode: PodcastEpisode?, - var completed:Boolean, - var moved:Boolean, - var isMoving:Boolean, - var failed:Boolean, - @JsonIgnore val uri: Uri, - @JsonIgnore val destinationUri: Uri, - @JsonIgnore val finalDestinationUri: Uri, - val finalDestinationSubfolder: String, - var downloadId: Long?, - var progress: Long, - var bytesDownloaded: Long + val id: String, + val downloadItemId: String, + val filename: String, + val fileSize: Long, + val finalDestinationPath: String, + val serverPath: String, + val localFolderName: String, + val localFolderUrl: String, + val localFolderId: String, + val ebookFile: EBookFile?, + val audioTrack: AudioTrack?, + val episode: PodcastEpisode?, + var completed: Boolean, + var moved: Boolean, + var isMoving: Boolean, + var failed: Boolean, + @JsonIgnore val uri: Uri, + @JsonIgnore val destinationUri: Uri, + @JsonIgnore val finalDestinationUri: Uri, + val finalDestinationSubfolder: String, + var downloadId: Long?, + var lastUpdateTime: Long?, + var progress: Long, + var bytesDownloaded: Long ) { companion object { - fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart { + fun make( + downloadItemId: String, + filename: String, + fileSize: Long, + destinationFile: File, + finalDestinationFile: File, + subfolder: String, + serverPath: String, + localFolder: LocalFolder, + ebookFile: EBookFile?, + audioTrack: AudioTrack?, + episode: PodcastEpisode? + ): DownloadItemPart { val destinationUri = Uri.fromFile(destinationFile) val finalDestinationUri = Uri.fromFile(finalDestinationFile) @@ -47,40 +60,46 @@ data class DownloadItemPart( } val downloadUri = Uri.parse(downloadUrl) - Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri") + Log.d( + "DownloadItemPart", + "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri" + ) return DownloadItemPart( - id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath), - downloadItemId, - filename = filename, - fileSize = fileSize, - finalDestinationPath = finalDestinationFile.absolutePath, - serverPath = serverPath, - localFolderName = localFolder.name, - localFolderUrl = localFolder.contentUrl, - localFolderId = localFolder.id, - ebookFile = ebookFile, - audioTrack = audioTrack, - episode = episode, - completed = false, - moved = false, - isMoving = false, - failed = false, - uri = downloadUri, - destinationUri = destinationUri, - finalDestinationUri = finalDestinationUri, - finalDestinationSubfolder = subfolder, - downloadId = null, - progress = 0, - bytesDownloaded = 0 + id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath), + downloadItemId, + filename = filename, + fileSize = fileSize, + finalDestinationPath = finalDestinationFile.absolutePath, + serverPath = serverPath, + localFolderName = localFolder.name, + localFolderUrl = localFolder.contentUrl, + localFolderId = localFolder.id, + ebookFile = ebookFile, + audioTrack = audioTrack, + episode = episode, + completed = false, + moved = false, + isMoving = false, + failed = false, + uri = downloadUri, + destinationUri = destinationUri, + finalDestinationUri = finalDestinationUri, + finalDestinationSubfolder = subfolder, + downloadId = null, + lastUpdateTime = null, + progress = 0, + bytesDownloaded = 0 ) } } @get:JsonIgnore - val isInternalStorage get() = localFolderId.startsWith("internal-") + val isInternalStorage + get() = localFolderId.startsWith("internal-") @get:JsonIgnore - val serverUrl get() = uri.toString() + val serverUrl + get() = uri.toString() @JsonIgnore fun getDownloadRequest(): DownloadManager.Request {