From 8bab4ae383dd429405bd412c8226d77bfb0825ef Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 28 Jan 2023 11:58:16 -0600 Subject: [PATCH] Update:More accurate progress percentage using bytes, download 1 audio file at a time & currently downloading page #251 #360 #515 #274 --- .../app/device/FolderScanner.kt | 12 +- .../app/managers/DownloadItemManager.kt | 40 ++++--- .../app/models/DownloadItemPart.kt | 13 ++- .../app/plugins/AbsDownloader.kt | 17 ++- components/widgets/CircleProgress.vue | 6 +- .../widgets/DownloadProgressIndicator.vue | 103 +++++++----------- pages/downloading.vue | 43 ++++++++ pages/item/_id/index.vue | 8 +- store/globals.js | 25 +++++ 9 files changed, 161 insertions(+), 106 deletions(-) create mode 100644 pages/downloading.vue diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index cfdd169d..7f1e99d5 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -220,7 +220,7 @@ class FolderScanner(var ctx: Context) { } // Scan item after download and create local library item - fun scanDownloadItem(downloadItem: DownloadItem):DownloadItemScanResult? { + fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) { val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl)) val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf() @@ -241,13 +241,13 @@ class FolderScanner(var ctx: Context) { if (itemFolderUrl == "") { Log.d(tag, "scanDownloadItem failed to find media folder") - return null + return cb(null) } val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl)) if (df == null) { Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}") - return null + return cb(null) } val localLibraryItemId = getLocalLibraryItemId(itemFolderId) @@ -283,7 +283,7 @@ class FolderScanner(var ctx: Context) { } } else if (itemPart.audioTrack != null) { // Is audio track val audioTrackFromServer = itemPart.audioTrack - Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}") + Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}") val localFileId = DeviceManager.getBase64Id(docFile.id) val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) @@ -314,7 +314,7 @@ class FolderScanner(var ctx: Context) { if (audioTracks.isEmpty()) { Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}") - return null + return cb(null) } // For books sort audio tracks then set @@ -364,7 +364,7 @@ class FolderScanner(var ctx: Context) { DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem) - return downloadItemScanResult + cb(downloadItemScanResult) } fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? { 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 547f5e74..1272a1dd 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 @@ -25,7 +25,7 @@ import kotlinx.coroutines.launch class DownloadItemManager(var downloadManager:DownloadManager, var folderScanner: FolderScanner, var mainActivity: MainActivity, var clientEventEmitter:DownloadEventEmitter) { val tag = "DownloadItemManager" - private val maxSimultaneousDownloads = 5 + private val maxSimultaneousDownloads = 1 private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) enum class DownloadCheckStatus { @@ -98,11 +98,11 @@ class DownloadItemManager(var downloadManager:DownloadManager, var folderScanner handleDownloadItemPartCheck(downloadCheckStatus, downloadItemPart) } + delay(500) + if (currentDownloadItemParts.size < maxSimultaneousDownloads) { checkUpdateDownloadQueue() } - - delay(500) } Log.d(tag, "Finished watching downloads") @@ -122,12 +122,14 @@ class DownloadItemManager(var downloadManager:DownloadManager, var folderScanner val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0 val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0 - val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getInt(bytesDownloadedColumnIndex) else 0 + val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getLong(bytesDownloadedColumnIndex) else 0 Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus") if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) { Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Successful") downloadItemPart.completed = true + downloadItemPart.progress = 1 + downloadItemPart.bytesDownloaded = bytesDownloadedSoFar return DownloadCheckStatus.Successful } else if (downloadStatus == DownloadManager.STATUS_FAILED) { Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed") @@ -139,6 +141,7 @@ class DownloadItemManager(var downloadManager:DownloadManager, var folderScanner val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0 Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%") downloadItemPart.progress = percentProgress + downloadItemPart.bytesDownloaded = bytesDownloadedSoFar return DownloadCheckStatus.InProgress } } else { @@ -214,23 +217,24 @@ class DownloadItemManager(var downloadManager:DownloadManager, var folderScanner if (downloadItem.isDownloadFinished) { Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}") - val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem) - Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}") + folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult -> + Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}") - val jsobj = JSObject() - jsobj.put("libraryItemId", downloadItem.id) - jsobj.put("localFolderId", downloadItem.localFolder.id) + val jsobj = JSObject() + jsobj.put("libraryItemId", downloadItem.id) + jsobj.put("localFolderId", downloadItem.localFolder.id) - downloadItemScanResult?.localLibraryItem?.let { localLibraryItem -> - jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem))) + downloadItemScanResult?.localLibraryItem?.let { localLibraryItem -> + jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem))) + } + downloadItemScanResult?.localMediaProgress?.let { localMediaProgress -> + jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) + } + + clientEventEmitter.onDownloadItemComplete(jsobj) + downloadItemQueue.remove(downloadItem) + DeviceManager.dbManager.removeDownloadItem(downloadItem.id) } - downloadItemScanResult?.localMediaProgress?.let { localMediaProgress -> - jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) - } - - clientEventEmitter.onDownloadItemComplete(jsobj) - downloadItemQueue.remove(downloadItem) - DeviceManager.dbManager.removeDownloadItem(downloadItem.id) } } } 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 17baf455..2d34a2d7 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 @@ -15,6 +15,7 @@ data class DownloadItemPart( val id: String, val downloadItemId: String, val filename: String, + val fileSize: Long, val finalDestinationPath:String, val serverPath: String, val localFolderName: String, @@ -31,10 +32,11 @@ data class DownloadItemPart( @JsonIgnore val finalDestinationUri: Uri, val finalDestinationSubfolder: String, var downloadId: Long?, - var progress: Long + var progress: Long, + var bytesDownloaded: Long ) { companion object { - fun make(downloadItemId:String, filename:String, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart { + fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart { val destinationUri = Uri.fromFile(destinationFile) val finalDestinationUri = Uri.fromFile(finalDestinationFile) @@ -46,6 +48,7 @@ data class DownloadItemPart( id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath), downloadItemId, filename = filename, + fileSize = fileSize, finalDestinationPath = finalDestinationFile.absolutePath, serverPath = serverPath, localFolderName = localFolder.name, @@ -62,14 +65,12 @@ data class DownloadItemPart( finalDestinationUri = finalDestinationUri, finalDestinationSubfolder = subfolder, downloadId = null, - progress = 0 + progress = 0, + bytesDownloaded = 0 ) } } - @get:JsonIgnore - val fileSize get() = audioTrack?.metadata?.size ?: 0 - @JsonIgnore fun getDownloadRequest(): DownloadManager.Request { val dlRequest = DownloadManager.Request(uri) diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt index 1b5bcd30..4110cedd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt @@ -145,6 +145,7 @@ class AbsDownloader : Plugin() { // Create download item part for each audio track tracks.forEach { audioTrack -> + val fileSize = audioTrack.metadata?.size ?: 0 val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}" val destinationFilename = getFilenameFromRelPath(audioTrack.relPath) Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}") @@ -162,13 +163,16 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,audioTrack,null) + val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,audioTrack,null) downloadItem.downloadItemParts.add(downloadItemPart) } if (downloadItem.downloadItemParts.isNotEmpty()) { // Add cover download item if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) { + val coverLibraryFile = libraryItem.libraryFiles?.find { it.metadata.path == libraryItem.media.coverPath } + val coverFileSize = coverLibraryFile?.metadata?.size ?: 0 + val serverPath = "/api/items/${libraryItem.id}/cover" val destinationFilename = "cover-${libraryItem.id}.jpg" val destinationFile = File("$tempFolderPath/$destinationFilename") @@ -184,7 +188,7 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null) + val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null) downloadItem.downloadItemParts.add(downloadItemPart) } @@ -195,6 +199,8 @@ class AbsDownloader : Plugin() { val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title) val audioTrack = episode?.audioTrack + val fileSize = audioTrack?.metadata?.size ?: 0 + Log.d(tag, "Starting podcast episode download") val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle val downloadItemId = "${libraryItem.id}-${episode?.id}" @@ -211,10 +217,13 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode) + var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode) downloadItem.downloadItemParts.add(downloadItemPart) if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) { + val coverLibraryFile = libraryItem.libraryFiles?.find { it.metadata.path == libraryItem.media.coverPath } + val coverFileSize = coverLibraryFile?.metadata?.size ?: 0 + serverPath = "/api/items/${libraryItem.id}/cover" destinationFilename = "cover.jpg" @@ -224,7 +233,7 @@ class AbsDownloader : Plugin() { if (finalDestinationFile.exists()) { Log.d(tag, "Podcast cover already exists - not downloading cover again") } else { - downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null) + downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null) downloadItem.downloadItemParts.add(downloadItemPart) } } diff --git a/components/widgets/CircleProgress.vue b/components/widgets/CircleProgress.vue index 7cc49a4b..8d199d9d 100644 --- a/components/widgets/CircleProgress.vue +++ b/components/widgets/CircleProgress.vue @@ -34,12 +34,12 @@ export default { computed: {}, methods: { updateProgress() { - var progbar = this.$refs.progressbar - var circle = this.$refs.circle + const progbar = this.$refs.progressbar + const circle = this.$refs.circle if (!progbar || !circle) return clearTimeout(this.updateTimeout) - var progress = Math.min(this.value || 0, 1) + const progress = Math.min(this.value || 0, 1) progbar.style.setProperty('--progress-percent-before', this.lastProgress) progbar.style.setProperty('--progress-percent', progress) diff --git a/components/widgets/DownloadProgressIndicator.vue b/components/widgets/DownloadProgressIndicator.vue index 43bc6d54..8bcebae3 100644 --- a/components/widgets/DownloadProgressIndicator.vue +++ b/components/widgets/DownloadProgressIndicator.vue @@ -1,6 +1,6 @@ @@ -10,73 +10,38 @@ import { AbsDownloader } from '@/plugins/capacitor' export default { data() { return { - updateListener: null, + downloadItemListener: null, completeListener: null, - itemDownloadingMap: {} + itemPartUpdateListener: null } }, computed: { - numItemPartsComplete() { - var total = 0 - Object.values(this.itemDownloadingMap).map((item) => (total += item.partsCompleted)) - return total + downloadItems() { + return this.$store.state.globals.itemDownloads }, - numPartsRemaining() { - return this.numTotalParts - this.numItemPartsComplete + downloadItemParts() { + let parts = [] + this.downloadItems.forEach((di) => parts.push(...di.downloadItemParts)) + return parts }, - numTotalParts() { - var total = 0 - Object.values(this.itemDownloadingMap).map((item) => (total += item.totalParts)) - return total + downloadItemPartsRemaining() { + return this.downloadItemParts.filter((dip) => !dip.completed) }, progress() { - var numItems = Object.keys(this.itemDownloadingMap).length - if (!numItems) return 0 - var totalProg = 0 - Object.values(this.itemDownloadingMap).map((item) => (totalProg += item.itemProgress)) - return totalProg / numItems + let totalBytes = 0 + let totalBytesDownloaded = 0 + this.downloadItemParts.forEach((dip) => { + totalBytes += dip.fileSize + totalBytesDownloaded += dip.bytesDownloaded + }) + + if (!totalBytes) return 0 + return Math.min(1, totalBytesDownloaded / totalBytes) } }, methods: { - onItemDownloadUpdate(data) { - console.log('DownloadProgressIndicator onItemDownloadUpdate', JSON.stringify(data)) - if (!data || !data.downloadItemParts) { - console.error('Invalid item update payload') - return - } - var downloadItemParts = data.downloadItemParts - var partsCompleted = 0 - var totalPartsProgress = 0 - var partsRemaining = 0 - downloadItemParts.forEach((dip) => { - if (dip.completed) { - totalPartsProgress += 1 - partsCompleted++ - } else { - var progPercent = dip.progress / 100 - totalPartsProgress += progPercent - partsRemaining++ - } - }) - var itemProgress = totalPartsProgress / downloadItemParts.length - - var update = { - id: data.id, - libraryItemId: data.libraryItemId, - partsRemaining, - partsCompleted, - totalParts: downloadItemParts.length, - itemProgress - } - data.itemProgress = itemProgress - data.episodes = downloadItemParts.filter((dip) => dip.episode).map((dip) => dip.episode) - - console.log('[download] Saving item update download payload', JSON.stringify(update)) - console.log('[download] Download Progress indicator data', JSON.stringify(data)) - - this.$set(this.itemDownloadingMap, update.id, update) - - this.$store.commit('globals/addUpdateItemDownload', data) + clickedIt() { + this.$router.push('/downloading') }, onItemDownloadComplete(data) { console.log('DownloadProgressIndicator onItemDownloadComplete', JSON.stringify(data)) @@ -85,11 +50,6 @@ export default { return } - if (this.itemDownloadingMap[data.libraryItemId]) { - delete this.itemDownloadingMap[data.libraryItemId] - } else { - console.warn('Item download complete but not found in item downloading map', data.libraryItemId) - } if (!data.localLibraryItem) { this.$toast.error('Item download complete but failed to create library item') } else { @@ -103,15 +63,28 @@ export default { } this.$store.commit('globals/removeItemDownload', data.libraryItemId) + }, + onDownloadItem(downloadItem) { + console.log('DownloadProgressIndicator onDownloadItem', JSON.stringify(downloadItem)) + + downloadItem.itemProgress = 0 + downloadItem.episodes = downloadItem.downloadItemParts.filter((dip) => dip.episode).map((dip) => dip.episode) + + this.$store.commit('globals/addUpdateItemDownload', downloadItem) + }, + onDownloadItemPartUpdate(itemPart) { + this.$store.commit('globals/updateDownloadItemPart', itemPart) } }, mounted() { - this.updateListener = AbsDownloader.addListener('onItemDownloadUpdate', (data) => this.onItemDownloadUpdate(data)) + this.downloadItemListener = AbsDownloader.addListener('onDownloadItem', (data) => this.onDownloadItem(data)) + this.itemPartUpdateListener = AbsDownloader.addListener('onDownloadItemPartUpdate', (data) => this.onDownloadItemPartUpdate(data)) this.completeListener = AbsDownloader.addListener('onItemDownloadComplete', (data) => this.onItemDownloadComplete(data)) }, beforeDestroy() { - if (this.updateListener) this.updateListener.remove() + if (this.downloadItemListener) this.downloadItemListener.remove() if (this.completeListener) this.completeListener.remove() + if (this.itemPartUpdateListener) this.itemPartUpdateListener.remove() } } \ No newline at end of file diff --git a/pages/downloading.vue b/pages/downloading.vue new file mode 100644 index 00000000..71dc8673 --- /dev/null +++ b/pages/downloading.vue @@ -0,0 +1,43 @@ + + + + diff --git a/pages/item/_id/index.vue b/pages/item/_id/index.vue index 57f91a7c..ae6b89f5 100644 --- a/pages/item/_id/index.vue +++ b/pages/item/_id/index.vue @@ -79,6 +79,10 @@ +
+

Downloading! ({{ Math.round(downloadItem.itemProgress * 100) }}%)

+
+
Narrators
@@ -119,10 +123,6 @@
-
-

Downloading! ({{ Math.round(downloadItem.itemProgress * 100) }}%)

-
-

{{ description }}

diff --git a/store/globals.js b/store/globals.js index d6168cc3..6064fbc5 100644 --- a/store/globals.js +++ b/store/globals.js @@ -108,6 +108,31 @@ export const mutations = { state.itemDownloads.push(downloadItem) } }, + updateDownloadItemPart(state, downloadItemPart) { + const downloadItem = state.itemDownloads.find(i => i.id == downloadItemPart.downloadItemId) + if (!downloadItem) { + console.error('updateDownloadItemPart: Download item not found for itemPart', JSON.stringify(downloadItemPart)) + return + } + + let totalBytes = 0 + let totalBytesDownloaded = 0 + downloadItem.downloadItemParts = downloadItem.downloadItemParts.map(dip => { + let newDip = dip.id == downloadItemPart.id ? downloadItemPart : dip + + totalBytes += newDip.fileSize + totalBytesDownloaded += newDip.bytesDownloaded + + return newDip + }) + + if (totalBytes > 0) { + downloadItem.itemProgress = Math.min(1, totalBytesDownloaded / totalBytes) + console.log(`updateDownloadItemPart: filename=${downloadItemPart.filename}, totalBytes=${totalBytes}, downloaded=${totalBytesDownloaded}, itemProgress=${downloadItem.itemProgress}`) + } else { + downloadItem.itemProgress = 0 + } + }, removeItemDownload(state, id) { state.itemDownloads = state.itemDownloads.filter(i => i.id != id) },