mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-27 22:04:08 +02:00
Remove retry stubs
This commit is contained in:
@@ -38,7 +38,8 @@ class DownloadItemManager(
|
|||||||
private val lastPersistTime = mutableMapOf<String, Long>()
|
private val lastPersistTime = mutableMapOf<String, Long>()
|
||||||
private var watcherRunning = false
|
private var watcherRunning = false
|
||||||
private val jacksonMapper =
|
private val jacksonMapper =
|
||||||
jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
jacksonObjectMapper()
|
||||||
|
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
var downloadItemQueue: MutableList<DownloadItem> = mutableListOf()
|
var downloadItemQueue: MutableList<DownloadItem> = mutableListOf()
|
||||||
private set
|
private set
|
||||||
@@ -114,13 +115,6 @@ class DownloadItemManager(
|
|||||||
notifyQueueChanged()
|
notifyQueueChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun retryAll() {
|
|
||||||
downloadItemQueue.forEach(::retryDownloadItem)
|
|
||||||
checkUpdateDownloadQueue()
|
|
||||||
notifyQueueChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun retryDownloadItem(item: DownloadItem) {
|
private fun retryDownloadItem(item: DownloadItem) {
|
||||||
item.terminalFailureAt = null
|
item.terminalFailureAt = null
|
||||||
IncompleteDownloadCleanup.cancel(context, item.id)
|
IncompleteDownloadCleanup.cancel(context, item.id)
|
||||||
@@ -182,34 +176,41 @@ class DownloadItemManager(
|
|||||||
part.lastUpdateTime = System.currentTimeMillis()
|
part.lastUpdateTime = System.currentTimeMillis()
|
||||||
currentDownloadItemParts.add(part)
|
currentDownloadItemParts.add(part)
|
||||||
persist(item, force = true)
|
persist(item, force = true)
|
||||||
|
val activeConfig = DeviceManager.serverConnectionConfig
|
||||||
val token =
|
val token =
|
||||||
DeviceManager.deviceData.serverConnectionConfigs
|
if (activeConfig?.id == item.serverConnectionConfigId) activeConfig.token
|
||||||
.find { it.id == item.serverConnectionConfigId }?.token ?: DeviceManager.token
|
else
|
||||||
|
DeviceManager.getServerConnectionConfig(item.serverConnectionConfigId)?.token
|
||||||
|
?: DeviceManager.token
|
||||||
activeCalls[part.id] =
|
activeCalls[part.id] =
|
||||||
InternalDownloadManager(stagingFile, part.fileSize, object : InternalProgressCallback {
|
InternalDownloadManager(
|
||||||
override fun onProgress(totalBytesWritten: Long, progress: Long) {
|
stagingFile,
|
||||||
synchronized(this@DownloadItemManager) {
|
part.fileSize,
|
||||||
if (part !in currentDownloadItemParts) return
|
object : InternalProgressCallback {
|
||||||
part.bytesDownloaded = totalBytesWritten
|
override fun onProgress(totalBytesWritten: Long, progress: Long) {
|
||||||
part.progress = progress
|
synchronized(this@DownloadItemManager) {
|
||||||
part.lastUpdateTime = System.currentTimeMillis()
|
if (part !in currentDownloadItemParts) return
|
||||||
persist(item)
|
part.bytesDownloaded = totalBytesWritten
|
||||||
}
|
part.progress = progress
|
||||||
}
|
part.lastUpdateTime = System.currentTimeMillis()
|
||||||
|
persist(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onComplete(failed: Boolean) {
|
override fun onComplete(failed: Boolean) {
|
||||||
synchronized(this@DownloadItemManager) {
|
synchronized(this@DownloadItemManager) {
|
||||||
if (part !in currentDownloadItemParts) return
|
if (part !in currentDownloadItemParts) return
|
||||||
part.failed = failed
|
part.failed = failed
|
||||||
part.completed = !failed
|
part.completed = !failed
|
||||||
part.lastUpdateTime = System.currentTimeMillis()
|
part.lastUpdateTime = System.currentTimeMillis()
|
||||||
activeCalls.remove(part.id)
|
activeCalls.remove(part.id)
|
||||||
persist(item, force = true)
|
persist(item, force = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { hasAvailableSpace(part) }).download(
|
},
|
||||||
serverUrl(item, part),
|
{ hasAvailableSpace(part) }
|
||||||
token)
|
)
|
||||||
|
.download(serverUrl(item, part), token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -218,7 +219,8 @@ class DownloadItemManager(
|
|||||||
watcherRunning = true
|
watcherRunning = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
val activeParts = synchronized(this@DownloadItemManager) { currentDownloadItemParts.toList() }
|
val activeParts =
|
||||||
|
synchronized(this@DownloadItemManager) { currentDownloadItemParts.toList() }
|
||||||
activeParts.forEach(::handlePartUpdate)
|
activeParts.forEach(::handlePartUpdate)
|
||||||
synchronized(this@DownloadItemManager) {
|
synchronized(this@DownloadItemManager) {
|
||||||
checkUpdateDownloadQueue()
|
checkUpdateDownloadQueue()
|
||||||
@@ -235,10 +237,12 @@ class DownloadItemManager(
|
|||||||
|
|
||||||
private fun handlePartUpdate(part: DownloadItemPart) {
|
private fun handlePartUpdate(part: DownloadItemPart) {
|
||||||
clientEventEmitter.onDownloadItemPartUpdate(part)
|
clientEventEmitter.onDownloadItemPartUpdate(part)
|
||||||
val item = synchronized(this) { downloadItemQueue.find { it.id == part.downloadItemId } } ?: run {
|
val item =
|
||||||
removeActivePart(part)
|
synchronized(this) { downloadItemQueue.find { it.id == part.downloadItemId } }
|
||||||
return
|
?: run {
|
||||||
}
|
removeActivePart(part)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!part.completed && !part.failed) {
|
if (!part.completed && !part.failed) {
|
||||||
val lastUpdate = part.lastUpdateTime ?: return
|
val lastUpdate = part.lastUpdateTime ?: return
|
||||||
if (System.currentTimeMillis() - lastUpdate > STALL_TIMEOUT_MS) {
|
if (System.currentTimeMillis() - lastUpdate > STALL_TIMEOUT_MS) {
|
||||||
@@ -287,7 +291,8 @@ class DownloadItemManager(
|
|||||||
val backup = File(finalFile.parentFile, ".${finalFile.name}.abs-backup")
|
val backup = File(finalFile.parentFile, ".${finalFile.name}.abs-backup")
|
||||||
try {
|
try {
|
||||||
if (backup.exists() && !backup.delete()) throw IllegalStateException("Could not clear backup")
|
if (backup.exists() && !backup.delete()) throw IllegalStateException("Could not clear backup")
|
||||||
if (finalFile.exists() && !finalFile.renameTo(backup)) throw IllegalStateException("Could not protect existing file")
|
if (finalFile.exists() && !finalFile.renameTo(backup))
|
||||||
|
throw IllegalStateException("Could not protect existing file")
|
||||||
if (!stagingFile.renameTo(finalFile)) {
|
if (!stagingFile.renameTo(finalFile)) {
|
||||||
if (backup.exists()) backup.renameTo(finalFile)
|
if (backup.exists()) backup.renameTo(finalFile)
|
||||||
throw IllegalStateException("Could not finalize internal staging file")
|
throw IllegalStateException("Could not finalize internal staging file")
|
||||||
@@ -303,34 +308,42 @@ class DownloadItemManager(
|
|||||||
|
|
||||||
private fun moveDownloadedFile(item: DownloadItem, part: DownloadItemPart) {
|
private fun moveDownloadedFile(item: DownloadItem, part: DownloadItemPart) {
|
||||||
if (part.moved || part.isMoving) return
|
if (part.moved || part.isMoving) return
|
||||||
val root = DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl))
|
val root =
|
||||||
?: return failFinalization(item, part, "Could not resolve SAF destination")
|
DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl))
|
||||||
|
?: return failFinalization(item, part, "Could not resolve SAF destination")
|
||||||
part.isMoving = true
|
part.isMoving = true
|
||||||
persist(item, force = true)
|
persist(item, force = true)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
if (!hasAvailableSpace(part)) throw IllegalStateException("Insufficient storage for SAF copy")
|
if (!hasAvailableSpace(part))
|
||||||
|
throw IllegalStateException("Insufficient storage for SAF copy")
|
||||||
val folderKey = "${root.uri}/${part.finalDestinationSubfolder}"
|
val folderKey = "${root.uri}/${part.finalDestinationSubfolder}"
|
||||||
val folderLock = safFolderLocks.computeIfAbsent(folderKey) { Any() }
|
val folderLock = safFolderLocks.computeIfAbsent(folderKey) { Any() }
|
||||||
val folder = synchronized(folderLock) {
|
val folder =
|
||||||
getOrCreateFolder(root, part.finalDestinationSubfolder)
|
synchronized(folderLock) { getOrCreateFolder(root, part.finalDestinationSubfolder) }
|
||||||
} ?: throw IllegalStateException("Could not create SAF destination folder")
|
?: throw IllegalStateException("Could not create SAF destination folder")
|
||||||
val temporaryName = ".${part.filename}.${part.id.hashCode()}.part"
|
val temporaryName = ".${part.filename}.${part.id.hashCode()}.part"
|
||||||
folder.findFile(temporaryName)?.delete()
|
folder.findFile(temporaryName)?.delete()
|
||||||
val temporary = folder.createFile(mimeTypeFor(part), temporaryName)
|
val temporary =
|
||||||
?: throw IllegalStateException("Could not create SAF temporary file")
|
folder.createFile(mimeTypeFor(part), temporaryName)
|
||||||
|
?: throw IllegalStateException("Could not create SAF temporary file")
|
||||||
val staging = File(part.destinationPath)
|
val staging = File(part.destinationPath)
|
||||||
FileInputStream(staging).use { input ->
|
FileInputStream(staging).use { input ->
|
||||||
context.contentResolver.openOutputStream(temporary.uri, "w")?.use { input.copyTo(it) }
|
context.contentResolver.openOutputStream(temporary.uri, "w")?.use { input.copyTo(it) }
|
||||||
?: throw IllegalStateException("Could not open SAF output stream")
|
?: throw IllegalStateException("Could not open SAF output stream")
|
||||||
}
|
}
|
||||||
if (temporary.length() != staging.length()) throw IllegalStateException("SAF copy size mismatch")
|
if (temporary.length() != staging.length())
|
||||||
|
throw IllegalStateException("SAF copy size mismatch")
|
||||||
val existing = folder.findFile(part.filename)
|
val existing = folder.findFile(part.filename)
|
||||||
if (existing != null && !existing.delete()) throw IllegalStateException("Could not replace existing file")
|
if (existing != null && !existing.delete())
|
||||||
if (!temporary.renameTo(part.filename)) throw IllegalStateException("Could not finalize SAF temporary file")
|
throw IllegalStateException("Could not replace existing file")
|
||||||
val destination = folder.findFile(part.filename)
|
if (!temporary.renameTo(part.filename))
|
||||||
?: throw IllegalStateException("Could not reopen finalized SAF file")
|
throw IllegalStateException("Could not finalize SAF temporary file")
|
||||||
if (destination.length() != staging.length()) throw IllegalStateException("SAF final size mismatch")
|
val destination =
|
||||||
|
folder.findFile(part.filename)
|
||||||
|
?: throw IllegalStateException("Could not reopen finalized SAF file")
|
||||||
|
if (destination.length() != staging.length())
|
||||||
|
throw IllegalStateException("SAF final size mismatch")
|
||||||
if (!staging.delete()) Log.w(tag, "Could not remove staging file ${staging.name}")
|
if (!staging.delete()) Log.w(tag, "Could not remove staging file ${staging.name}")
|
||||||
part.completedDestinationUri = destination.uri.toString()
|
part.completedDestinationUri = destination.uri.toString()
|
||||||
completePart(item, part)
|
completePart(item, part)
|
||||||
@@ -364,12 +377,17 @@ class DownloadItemManager(
|
|||||||
if (!item.isDownloadFinished) return
|
if (!item.isDownloadFinished) return
|
||||||
scope.launch {
|
scope.launch {
|
||||||
folderScanner.scanDownloadItem(item) { scanResult ->
|
folderScanner.scanDownloadItem(item) { scanResult ->
|
||||||
val event = JSObject().apply {
|
val event =
|
||||||
put("libraryItemId", item.id)
|
JSObject().apply {
|
||||||
put("localFolderId", item.localFolder.id)
|
put("libraryItemId", item.id)
|
||||||
scanResult?.localLibraryItem?.let { put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(it))) }
|
put("localFolderId", item.localFolder.id)
|
||||||
scanResult?.localMediaProgress?.let { put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(it))) }
|
scanResult?.localLibraryItem?.let {
|
||||||
}
|
put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(it)))
|
||||||
|
}
|
||||||
|
scanResult?.localMediaProgress?.let {
|
||||||
|
put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(it)))
|
||||||
|
}
|
||||||
|
}
|
||||||
clientEventEmitter.onDownloadItemComplete(event)
|
clientEventEmitter.onDownloadItemComplete(event)
|
||||||
synchronized(this@DownloadItemManager) {
|
synchronized(this@DownloadItemManager) {
|
||||||
downloadItemQueue.remove(item)
|
downloadItemQueue.remove(item)
|
||||||
@@ -385,7 +403,8 @@ class DownloadItemManager(
|
|||||||
val staging = File(part.destinationPath)
|
val staging = File(part.destinationPath)
|
||||||
staging.parentFile?.mkdirs()
|
staging.parentFile?.mkdirs()
|
||||||
val expectedSize = if (part.fileSize > 0L) part.fileSize else UNKNOWN_PART_RESERVATION_BYTES
|
val expectedSize = if (part.fileSize > 0L) part.fileSize else UNKNOWN_PART_RESERVATION_BYTES
|
||||||
val remaining = (expectedSize - (staging.takeIf(File::exists)?.length() ?: 0L)).coerceAtLeast(0L)
|
val remaining =
|
||||||
|
(expectedSize - (staging.takeIf(File::exists)?.length() ?: 0L)).coerceAtLeast(0L)
|
||||||
val required = if (part.isInternalStorage) remaining else remaining + expectedSize
|
val required = if (part.isInternalStorage) remaining else remaining + expectedSize
|
||||||
val key = storageKey(staging)
|
val key = storageKey(staging)
|
||||||
val fs = statFsFor(staging)
|
val fs = statFsFor(staging)
|
||||||
@@ -410,7 +429,8 @@ class DownloadItemManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun storageKey(file: File): String =
|
private fun storageKey(file: File): String =
|
||||||
if (file.absolutePath.startsWith(context.filesDir.absolutePath)) "internal" else "external"
|
if (file.absolutePath.startsWith(context.filesDir.absolutePath)) "internal"
|
||||||
|
else "external"
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun removeActivePart(part: DownloadItemPart) {
|
private fun removeActivePart(part: DownloadItemPart) {
|
||||||
@@ -425,7 +445,9 @@ class DownloadItemManager(
|
|||||||
DeviceManager.dbManager.saveDownloadItem(item)
|
DeviceManager.dbManager.saveDownloadItem(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyQueueChanged() { clientEventEmitter.onQueueChanged(hasWork()) }
|
private fun notifyQueueChanged() {
|
||||||
|
clientEventEmitter.onQueueChanged(hasWork())
|
||||||
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
activeCalls.values.forEach(Call::cancel)
|
activeCalls.values.forEach(Call::cancel)
|
||||||
@@ -443,11 +465,12 @@ class DownloadItemManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun mimeTypeFor(part: DownloadItemPart): String =
|
private fun mimeTypeFor(part: DownloadItemPart): String =
|
||||||
part.audioTrack?.mimeType ?: when (part.ebookFile?.ebookFormat?.lowercase()) {
|
part.audioTrack?.mimeType
|
||||||
"epub" -> "application/epub+zip"
|
?: when (part.ebookFile?.ebookFormat?.lowercase()) {
|
||||||
"pdf" -> "application/pdf"
|
"epub" -> "application/epub+zip"
|
||||||
else -> "image/jpeg"
|
"pdf" -> "application/pdf"
|
||||||
}
|
else -> "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
private fun serverUrl(item: DownloadItem, part: DownloadItemPart): String {
|
private fun serverUrl(item: DownloadItem, part: DownloadItemPart): String {
|
||||||
val rawCover = if (part.serverPath.endsWith("/cover")) "?raw=1" else ""
|
val rawCover = if (part.serverPath.endsWith("/cover")) "?raw=1" else ""
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class DownloadService : Service() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_CANCEL -> DownloadServiceHost.cancelAll(this)
|
ACTION_CANCEL -> DownloadServiceHost.cancelAll(this)
|
||||||
ACTION_RETRY -> DownloadServiceHost.retryAll(this)
|
|
||||||
else -> DownloadServiceHost.ensure(this)
|
else -> DownloadServiceHost.ensure(this)
|
||||||
}
|
}
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
@@ -54,8 +53,6 @@ class DownloadService : Service() {
|
|||||||
private fun notification(text: String, progress: Int = 0, determinate: Boolean = false): Notification {
|
private fun notification(text: String, progress: Int = 0, determinate: Boolean = false): Notification {
|
||||||
val cancelIntent = PendingIntent.getService(
|
val cancelIntent = PendingIntent.getService(
|
||||||
this, 1, Intent(this, DownloadService::class.java).setAction(ACTION_CANCEL), pendingIntentFlags())
|
this, 1, Intent(this, DownloadService::class.java).setAction(ACTION_CANCEL), pendingIntentFlags())
|
||||||
val retryIntent = PendingIntent.getService(
|
|
||||||
this, 2, Intent(this, DownloadService::class.java).setAction(ACTION_RETRY), pendingIntentFlags())
|
|
||||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.icon)
|
.setSmallIcon(R.drawable.icon)
|
||||||
.setContentTitle("Audiobookshelf downloads")
|
.setContentTitle("Audiobookshelf downloads")
|
||||||
@@ -64,7 +61,6 @@ class DownloadService : Service() {
|
|||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setProgress(100, progress, !determinate)
|
.setProgress(100, progress, !determinate)
|
||||||
.addAction(0, "Cancel", cancelIntent)
|
.addAction(0, "Cancel", cancelIntent)
|
||||||
.addAction(0, "Retry", retryIntent)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +75,6 @@ class DownloadService : Service() {
|
|||||||
private const val CHANNEL_ID = "downloads"
|
private const val CHANNEL_ID = "downloads"
|
||||||
private const val NOTIFICATION_ID = 4102
|
private const val NOTIFICATION_ID = 4102
|
||||||
private const val ACTION_CANCEL = "com.audiobookshelf.app.download.CANCEL"
|
private const val ACTION_CANCEL = "com.audiobookshelf.app.download.CANCEL"
|
||||||
private const val ACTION_RETRY = "com.audiobookshelf.app.download.RETRY"
|
|
||||||
fun intent(context: Context) = Intent(context, DownloadService::class.java)
|
fun intent(context: Context) = Intent(context, DownloadService::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,12 +55,6 @@ object DownloadServiceHost {
|
|||||||
startService(context)
|
startService(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun retryAll(context: Context) {
|
|
||||||
startService(context)
|
|
||||||
ensure(context).retryAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun cancelAll(context: Context) { ensure(context).cancelAll() }
|
fun cancelAll(context: Context) { ensure(context).cancelAll() }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user