diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 859001e7..f6a2e959 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -34,7 +34,11 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) { @JsonIgnore open fun getAudioTracks():List { return mutableListOf() } @JsonIgnore - open fun setAudioTracks(audioTracks:List) { } + open fun setAudioTracks(audioTracks:MutableList) { } + @JsonIgnore + open fun addAudioTrack(audioTrack:AudioTrack) { } + @JsonIgnore + open fun removeAudioTrack(localFileId:String) { } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -51,7 +55,7 @@ class Podcast( return tracks.filterNotNull() } @JsonIgnore - override fun setAudioTracks(audioTracks:List) { + override fun setAudioTracks(audioTracks:MutableList) { // Remove episodes no longer there in tracks episodes = episodes.filter { ep -> audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null @@ -64,6 +68,15 @@ class Podcast( } } } + @JsonIgnore + override fun addAudioTrack(audioTrack:AudioTrack) { + var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes.size + 1,null,null,audioTrack.title,null,null,null,audioTrack) + episodes.add(newEpisode) + } + @JsonIgnore + override fun removeAudioTrack(localFileId:String) { + episodes.removeIf { it.audioTrack?.localFileId == localFileId } + } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -73,7 +86,7 @@ class Book( var tags:List, var audioFiles:List, var chapters:List, - var tracks:List?, + var tracks:MutableList?, var size:Long?, var duration:Double? ) : MediaType(metadata, coverPath) { @@ -82,9 +95,30 @@ class Book( return tracks ?: mutableListOf() } @JsonIgnore - override fun setAudioTracks(audioTracks:List) { + override fun setAudioTracks(audioTracks:MutableList) { tracks = audioTracks + // TODO: Is it necessary to calculate this each time? check if can remove safely + var totalDuration = 0.0 + tracks?.forEach { + totalDuration += it.duration + } + duration = totalDuration + } + @JsonIgnore + override fun addAudioTrack(audioTrack:AudioTrack) { + tracks?.add(audioTrack) + + var totalDuration = 0.0 + tracks?.forEach { + totalDuration += it.duration + } + duration = totalDuration + } + @JsonIgnore + override fun removeAudioTrack(localFileId:String) { + tracks?.removeIf { it.localFileId == localFileId } + var totalDuration = 0.0 tracks?.forEach { totalDuration += it.duration diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt index b6356d4a..07ce32ac 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt @@ -1,6 +1,7 @@ package com.audiobookshelf.app.data import android.util.Log +import com.audiobookshelf.app.plugins.AbsDownloader import io.paperdb.Paper import org.json.JSONObject @@ -57,6 +58,10 @@ class DbManager { } } + fun saveLocalLibraryItem(localLibraryItem:LocalLibraryItem) { + Paper.book("localLibraryItems").write(localLibraryItem.id, localLibraryItem) + } + fun saveLocalFolder(localFolder:LocalFolder) { Paper.book("localFolders").write(localFolder.id,localFolder) } @@ -84,6 +89,25 @@ class DbManager { Paper.book("localFolders").delete(folderId) } + fun saveDownloadItem(downloadItem: AbsDownloader.DownloadItem) { + Paper.book("downloadItems").write(downloadItem.id, downloadItem) + } + + fun removeDownloadItem(downloadItemId:String) { + Paper.book("downloadItems").delete(downloadItemId) + } + + fun getDownloadItems():List { + var downloadItems:MutableList = mutableListOf() + Paper.book("downloadItems").allKeys.forEach { + var downloadItem:AbsDownloader.DownloadItem? = Paper.book("downloadItems").read(it) + if (downloadItem != null) { + downloadItems.add(downloadItem) + } + } + return downloadItems + } + fun saveObject(db:String, key:String, value:JSONObject) { Log.d(tag, "Saving Object $key ${value.toString()}") Paper.book(db).write(key, value) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index a03d924c..cd3bb51b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -25,6 +25,7 @@ data class LocalLibraryItem( var libraryItemId:String?, var folderId:String, var absolutePath:String, + var contentUrl:String, var isInvalid:Boolean, var mediaType:String, var media:MediaType, @@ -42,7 +43,7 @@ data class LocalLibraryItem( } @JsonIgnore - fun updateFromScan(audioTracks:List, _localFiles:MutableList) { + fun updateFromScan(audioTracks:MutableList, _localFiles:MutableList) { media.setAudioTracks(audioTracks) localFiles = _localFiles @@ -125,10 +126,10 @@ data class LocalMediaItem( if (mediaType == "book") { var chapters = getAudiobookChapters() var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration()) - return LocalLibraryItem(id, null, folderId, absolutePath, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true) + return LocalLibraryItem(id, null, folderId, absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true) } else { var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false) - return LocalLibraryItem(id, null, folderId, absolutePath, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true) + return LocalLibraryItem(id, null, folderId, absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true) } } } @@ -142,12 +143,17 @@ data class LocalFile( var simplePath:String, var mimeType:String?, var size:Long -) +) { + @JsonIgnore + fun isAudioFile():Boolean { + return mimeType?.startsWith("audio") == true + } +} @JsonIgnoreProperties(ignoreUnknown = true) data class LocalFolder( var id:String, - var name:String?, + var name:String, var contentUrl:String, var absolutePath:String, var simplePath:String, diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/FolderScanResult.kt b/android/app/src/main/java/com/audiobookshelf/app/data/FolderScanResult.kt index 11449234..f4f706bf 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/FolderScanResult.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/FolderScanResult.kt @@ -8,3 +8,8 @@ data class FolderScanResult( val localFolder:LocalFolder, val localLibraryItems:List, ) + +data class LocalLibraryItemScanResult( + val updated:Boolean, + val localLibraryItem:LocalLibraryItem, +) 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 5579f3e4..b6acf887 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 @@ -9,6 +9,7 @@ import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.FFprobeKit import com.arthenica.ffmpegkit.Level import com.audiobookshelf.app.data.* +import com.audiobookshelf.app.plugins.AbsDownloader import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue @@ -63,6 +64,7 @@ class FolderScanner(var ctx: Context) { var itemFolderName = itemFolder.name ?: "" var itemId = getLocalLibraryItemId(itemFolder.id) + var itemContentUrl = itemFolder.uri.toString() var existingItem = existingLocalLibraryItems.find { emi -> emi.id == itemId } var existingLocalFiles = existingItem?.localFiles ?: mutableListOf() @@ -137,7 +139,6 @@ class FolderScanner(var ctx: Context) { } startOffset += audioProbeResult.duration - index++ isNewOrUpdated = true } else { audioTrackToAdd = existingAudioTrack @@ -203,4 +204,177 @@ class FolderScanner(var ctx: Context) { FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf()) } } + + // Scan item after download and create local library item + fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):LocalLibraryItem? { + var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl)) + var foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf() + var itemFolderUrl:String = "" + foldersFound.forEach { + if (it.name == downloadItem.itemTitle) { + itemFolderUrl = it.uri.toString() + } + } + + if (itemFolderUrl == "") { + Log.d(tag, "scanDownloadItem failed to find media folder") + return null + } + var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl)) + + if (df == null) { + Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}") + return null + } + Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri}") + + // Search for files in media item folder + var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) + Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}") + + var localLibraryItem = LocalLibraryItem("local_${downloadItem.id}", downloadItem.id, downloadItem.localFolder.id, downloadItem.itemFolderPath,itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true) + + var localFiles:MutableList = mutableListOf() + var audioTracks:MutableList = mutableListOf() + + filesFound.forEach { docFile -> + var itemPart = downloadItem.downloadItemParts.find { itemPart -> + itemPart.filename == docFile.name + } + if (itemPart == null) { + Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}") + } else if (itemPart.audioTrack != null) { // Is audio track + var audioTrackFromServer = itemPart.audioTrack + + var localFileId = DeviceManager.getBase64Id(docFile.id) + var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + localFiles.add(localFile) + + // TODO: Make asynchronous + var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") + + val audioProbeResult = jacksonObjectMapper().readValue(session.output) + Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}") + + // Create new audio track + var track = AudioTrack(audioTrackFromServer?.index ?: 0, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult) + audioTracks.add(track) + } else { // Cover image + var localFileId = DeviceManager.getBase64Id(docFile.id) + var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + localFiles.add(localFile) + + localLibraryItem.coverAbsolutePath = localFile.absolutePath + localLibraryItem.coverContentUrl = localFile.contentUrl + } + } + + if (audioTracks.isEmpty()) { + Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}") + return null + } + + localLibraryItem.media.setAudioTracks(audioTracks) + localLibraryItem.localFiles = localFiles + + DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem) + + return localLibraryItem + } + + fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? { + var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl)) + + if (df == null) { + Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}") + return null + } + Log.d(tag, "scanLocalLibraryItem starting for ${localLibraryItem.absolutePath} | ${df.uri}") + + var wasUpdated = false + + // Search for files in media item folder + var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) + Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}") + + filesFound.forEach { + try { + Log.d(tag, "Checking file found ${it.name} | ${it.id}") + }catch(e:Exception) { + Log.d(tag, "Check file found exception", e) + } + } + + var existingAudioTracks = localLibraryItem.media.getAudioTracks() + + // Remove any files no longer found in library item folder + var existingLocalFileIds = localLibraryItem.localFiles.map { it.id } + existingLocalFileIds.forEach { localFileId -> + Log.d(tag, "Checking local file id is there $localFileId") + if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) { + Log.d(tag, "scanLocalLibraryItem file $localFileId was removed from ${localLibraryItem.absolutePath}") + localLibraryItem.localFiles.removeIf { it.id == localFileId } + + if (existingAudioTracks.find { it.localFileId == localFileId } != null) { + Log.d(tag, "scanLocalLibraryItem audio track file ${localFileId} was removed from ${localLibraryItem.absolutePath}") + localLibraryItem.media.removeAudioTrack(localFileId) + } + wasUpdated = true + } + } + + filesFound.forEach { docFile -> + var localFileId = DeviceManager.getBase64Id(docFile.id) + var existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId } + + if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) { + + var localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + if (existingLocalFile == null) { + localLibraryItem.localFiles.add(localFile) + Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}") + } + + if (localFile.isAudioFile()) { + // TODO: Make asynchronous + var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") + + val audioProbeResult = jacksonObjectMapper().readValue(session.output) + Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}") + + var existingTrack = existingAudioTracks.find { audioTrack -> + audioTrack.localFileId == localFile.id + } + + if (existingTrack == null) { + // Create new audio track + var lastTrack = existingAudioTracks.lastOrNull() + var startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0) + var track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult) + localLibraryItem.media.addAudioTrack(track) + wasUpdated = true + } else { + existingTrack.audioProbeResult = audioProbeResult + // TODO: Update data found from probe + wasUpdated = true + } + } else { // Check if cover is empty + if (localLibraryItem.coverContentUrl == null) { + Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}") + localLibraryItem.coverContentUrl = localFile.contentUrl + localLibraryItem.coverAbsolutePath = localFile.absolutePath + wasUpdated = true + } + } + } + } + + if (wasUpdated) { + Log.d(tag, "Local library item was updated - saving it") + DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem) + } else { + Log.d(tag, "Local library item was up-to-date") + } + return LocalLibraryItemScanResult(wasUpdated, localLibraryItem) + } } 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 8d45f57e..70f9521e 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 @@ -6,8 +6,7 @@ import android.net.Uri import android.os.Build import android.util.Log import com.audiobookshelf.app.MainActivity -import com.audiobookshelf.app.data.LibraryItem -import com.audiobookshelf.app.data.LocalFolder +import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.FolderScanner import com.audiobookshelf.app.server.ApiHandler @@ -37,11 +36,14 @@ class AbsDownloader : Plugin() { data class DownloadItemPart( val id: String, - val name: String, + val filename: String, + val destinationPath:String, val itemTitle: String, val serverPath: String, - val folderName: String, + val localFolderName: String, val localFolderId: String, + val audioTrack: AudioTrack?, + var completed:Boolean, @JsonIgnore val uri: Uri, @JsonIgnore val destinationUri: Uri, var downloadId: Long?, @@ -50,8 +52,8 @@ class AbsDownloader : Plugin() { @JsonIgnore fun getDownloadRequest(): DownloadManager.Request { var dlRequest = DownloadManager.Request(uri) - dlRequest.setTitle(name) - dlRequest.setDescription("Downloading to $folderName for book $itemTitle") + dlRequest.setTitle(filename) + dlRequest.setDescription("Downloading to $localFolderName for book $itemTitle") dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) dlRequest.setDestinationUri(destinationUri) return dlRequest @@ -60,8 +62,11 @@ class AbsDownloader : Plugin() { data class DownloadItem( val id: String, + val mediaType: String, + val itemFolderPath:String, val localFolder: LocalFolder, val itemTitle: String, + val media:MediaType, val downloadItemParts: MutableList ) @@ -91,16 +96,21 @@ class AbsDownloader : Plugin() { var localFolderId = call.data.getString("localFolderId").toString() Log.d(tag, "Download library item $libraryItemId to folder $localFolderId") + if (downloadQueue.find { it.id == libraryItemId } != null) { + Log.d(tag, "Download already started for this library item $libraryItemId") + return call.resolve(JSObject("{\"error\":\"Download already started for this library item\"}")) + } + apiHandler.getLibraryItem(libraryItemId) { libraryItem -> Log.d(tag, "Got library item from server ${libraryItem.id}") var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId) if (localFolder != null) { startLibraryItemDownload(libraryItem, localFolder) call.resolve() + } else { + call.resolve(JSObject("{\"error\":\"Local Folder Not Found\"}")) } } - - call.resolve(JSObject("{\"error\":\"Library Item not found\"}")) } // Clean folder path so it can be used in URL @@ -128,22 +138,23 @@ class AbsDownloader : Plugin() { fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder) { if (libraryItem.mediaType == "book") { - var bookMedia = libraryItem.media as com.audiobookshelf.app.data.Book - var bookTitle = bookMedia.metadata.title - var tracks = bookMedia.tracks ?: mutableListOf() + var bookTitle = libraryItem.media.metadata.title + var tracks = libraryItem.media.getAudioTracks() Log.d(tag, "Starting library item download with ${tracks.size} tracks") - var downloadItem = DownloadItem(libraryItem.id, localFolder, bookTitle, mutableListOf()) var itemFolderPath = localFolder.absolutePath + "/" + bookTitle - tracks.forEach { audioFile -> - var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioFile.relPath)}" - var destinationFilename = getFilenameFromRelPath(audioFile.relPath) - Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioFile.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}") + var downloadItem = DownloadItem(libraryItem.id, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf()) + + + // Create download item part for each audio track + tracks.forEach { audioTrack -> + var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}" + var destinationFilename = getFilenameFromRelPath(audioTrack.relPath) + Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}") var destinationFile = File("$itemFolderPath/$destinationFilename") var destinationUri = Uri.fromFile(destinationFile) var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}") Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri") - var downloadItemPart = DownloadItemPart(UUID.randomUUID().toString(), destinationFilename, bookTitle, serverPath, localFolder.name - ?: "", localFolder.id, downloadUri, destinationUri, null, 0) + var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, audioTrack, false, downloadUri, destinationUri, null, 0) downloadItem.downloadItemParts.add(downloadItemPart) @@ -151,8 +162,24 @@ class AbsDownloader : Plugin() { var downloadId = downloadManager.enqueue(dlRequest) downloadItemPart.downloadId = downloadId } - Log.d(tag, "Done queueing downloads ${downloadQueue.size}") + if (downloadItem.downloadItemParts.isNotEmpty()) { + // Add cover download item + if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) { + var serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg" + var destinationFilename = "cover.jpg" + var destinationFile = File("$itemFolderPath/$destinationFilename") + var destinationUri = Uri.fromFile(destinationFile) + var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}") + var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, null, false, downloadUri, destinationUri, null, 0) + + downloadItem.downloadItemParts.add(downloadItemPart) + + var dlRequest = downloadItemPart.getDownloadRequest() + var downloadId = downloadManager.enqueue(dlRequest) + downloadItemPart.downloadId = downloadId + } + // TODO: Cannot create new text file here but can download here... ?? // var abmetadataFile = File(itemFolderPath, "abmetadata.abs") // abmetadataFile.createNewFileIfPossible() @@ -160,6 +187,7 @@ class AbsDownloader : Plugin() { downloadQueue.add(downloadItem) startWatchingDownloads(downloadItem) + DeviceManager.dbManager.saveDownloadItem(downloadItem) } } else { // TODO: Download podcast episode(s) @@ -168,26 +196,38 @@ class AbsDownloader : Plugin() { fun startWatchingDownloads(downloadItem: DownloadItem) { GlobalScope.launch(Dispatchers.IO) { - while (downloadItem.downloadItemParts.isNotEmpty()) { + while (downloadItem.downloadItemParts.find { !it.completed } != null) { // While some item is not completed + var numPartsBefore = downloadItem.downloadItemParts.size checkDownloads(downloadItem) + + // Keep database updated as item parts finish downloading + if (downloadItem.downloadItemParts.size > 0 && downloadItem.downloadItemParts.size != numPartsBefore) { + Log.d(tag, "Save download item on num parts changed from $numPartsBefore to ${downloadItem.downloadItemParts.size}") + DeviceManager.dbManager.saveDownloadItem(downloadItem) + } + notifyListeners("onItemDownloadUpdate", JSObject(jacksonObjectMapper().writeValueAsString(downloadItem))) delay(500) } - var folderScanResult = folderScanner.scanForMediaItems(downloadItem.localFolder, false) + var localLibraryItem = folderScanner.scanDownloadItem(downloadItem) + DeviceManager.dbManager.removeDownloadItem(downloadItem.id) + downloadQueue.remove(downloadItem) + + Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${localLibraryItem?.id} | Items remaining in Queue ${downloadQueue.size}") - Log.d(tag, "Item download complete ${downloadItem.itemTitle}") var jsobj = JSObject() jsobj.put("libraryItemId", downloadItem.id) jsobj.put("localFolderId", downloadItem.localFolder.id) - jsobj.put("folderScanResult", JSObject(jacksonObjectMapper().writeValueAsString(folderScanResult))) + if (localLibraryItem != null) { + jsobj.put("localLibraryItem", JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem))) + } notifyListeners("onItemDownloadComplete", jsobj) } } fun checkDownloads(downloadItem: DownloadItem) { var itemParts = downloadItem.downloadItemParts.map { it } - Log.d(tag, "Check Downloads ${itemParts.size}") for (downloadItemPart in itemParts) { if (downloadItemPart.downloadId != null) { var dlid = downloadItemPart.downloadId!! @@ -195,24 +235,26 @@ class AbsDownloader : Plugin() { downloadManager.query(query).use { if (it.moveToFirst()) { val totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) - Log.d(tag, "Download ${downloadItemPart.name} bytes $totalBytes") val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS)) val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + Log.d(tag, "Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus") if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) { - Log.d(tag, "Download ${downloadItemPart.name} Done") - downloadItem.downloadItemParts.remove(downloadItemPart) + Log.d(tag, "Download ${downloadItemPart.filename} Done") +// downloadItem.downloadItemParts.remove(downloadItemPart) + downloadItemPart.completed = true } else if (downloadStatus == DownloadManager.STATUS_FAILED) { - Log.d(tag, "Download ${downloadItemPart.name} Failed") + Log.d(tag, "Download ${downloadItemPart.filename} Failed") downloadItem.downloadItemParts.remove(downloadItemPart) +// downloadItemPart.completed = true } else { //update progress val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0 - Log.d(tag, "${downloadItemPart.name} Progress = $percentProgress%") + Log.d(tag, "${downloadItemPart.filename} Progress = $percentProgress%") downloadItemPart.progress = percentProgress } } else { - Log.d(tag, "Download ${downloadItemPart.name} not found in dlmanager") + Log.d(tag, "Download ${downloadItemPart.filename} not found in dlmanager") downloadItem.downloadItemParts.remove(downloadItemPart) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt index 74151615..23d1a14e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt @@ -12,11 +12,15 @@ import com.anggrayudi.storage.callback.StorageAccessCallback import com.anggrayudi.storage.file.* import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.data.LocalFolder +import com.audiobookshelf.app.data.LocalLibraryItem import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.FolderScanner import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.getcapacitor.* import com.getcapacitor.annotation.CapacitorPlugin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch @CapacitorPlugin(name = "AbsFileSystem") class AbsFileSystem : Plugin() { @@ -68,7 +72,7 @@ class AbsFileSystem : Plugin() { var simplePath = folder.getSimplePath(activity) var folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT) - var localFolder = LocalFolder(folderId, folder.name, folder.uri.toString(),absolutePath, simplePath, storageType.toString(), mediaType) + var localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),absolutePath, simplePath, storageType.toString(), mediaType) DeviceManager.dbManager.saveLocalFolder(localFolder) call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localFolder))) @@ -155,6 +159,34 @@ class AbsFileSystem : Plugin() { call.resolve() } + @PluginMethod + fun removeLocalLibraryItem(call: PluginCall) { + var localLibraryItemId = call.data.getString("localLibraryItemId", "").toString() + DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId) + call.resolve() + } + + @PluginMethod + fun scanLocalLibraryItem(call: PluginCall) { + var localLibraryItemId = call.data.getString("localLibraryItemId", "").toString() + var forceAudioProbe = call.data.getBoolean("forceAudioProbe") + Log.d(TAG, "Scan Local library item $localLibraryItemId | Force Audio Probe $forceAudioProbe") + GlobalScope.launch(Dispatchers.IO) { + var localLibraryItem: LocalLibraryItem? = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId) + localLibraryItem?.let { + var folderScanner = FolderScanner(context) + var scanResult = folderScanner.scanLocalLibraryItem(it, forceAudioProbe) + if (scanResult == null) { + Log.d(TAG, "NO Scan DATA") + call.resolve(JSObject()) + } else { + Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(scanResult)}") + call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(scanResult))) + } + } ?: call.resolve(JSObject()) + } + } + @PluginMethod fun delete(call: PluginCall) { var url = call.data.getString("url", "").toString() diff --git a/pages/localMedia/folders/_id.vue b/pages/localMedia/folders/_id.vue index f2c17f08..6a098d47 100644 --- a/pages/localMedia/folders/_id.vue +++ b/pages/localMedia/folders/_id.vue @@ -13,7 +13,7 @@
diff --git a/pages/localMedia/item/_id.vue b/pages/localMedia/item/_id.vue new file mode 100644 index 00000000..f7cb919a --- /dev/null +++ b/pages/localMedia/item/_id.vue @@ -0,0 +1,177 @@ + + + + + \ No newline at end of file