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 d8420940..a3239bcb 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 @@ -126,6 +126,7 @@ class Book( var audioFiles:List?, var chapters:List?, var tracks:MutableList?, + var ebookFile: EBookFile?, var size:Long?, var duration:Double?, var numTracks:Int? @@ -179,7 +180,7 @@ class Book( } @JsonIgnore override fun getLocalCopy(): Book { - return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0) + return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0) } } 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 a85a84a2..2fd133be 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 @@ -48,6 +48,20 @@ data class LocalFile( if (mimeType == "video/mp4") return true return mimeType?.startsWith("audio") == true } + @JsonIgnore + fun isEBookFile():Boolean { + return getEBookFormat() != null + } + @JsonIgnore + fun getEBookFormat():String? { + if (mimeType == "application/epub+zip") return "epub" + if (mimeType == "application/pdf") return "pdf" + if (mimeType == "application/x-mobipocket-ebook") return "mobi" + if (mimeType == "application/vnd.comicbook+zip") return "cbz" + if (mimeType == "application/vnd.comicbook-rar") return "cbr" + if (mimeType == "application/vnd.amazon.mobi8-ebook") return "azw3" + return null + } } @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/EBookFile.kt b/android/app/src/main/java/com/audiobookshelf/app/data/EBookFile.kt new file mode 100644 index 00000000..88bdd8b1 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/EBookFile.kt @@ -0,0 +1,13 @@ +package com.audiobookshelf.app.data + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class EBookFile( + var ino:String, + var metadata:FileMetadata?, + var ebookFormat:String, + var isLocal:Boolean, + var localFileId:String?, + var contentUrl:String? +) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt index fb9008f7..c502a1eb 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt @@ -18,6 +18,7 @@ data class LocalMediaItem( var basePath:String, var absolutePath:String, var audioTracks:MutableList, + var ebookFile:EBookFile?, var localFiles:MutableList, var coverContentUrl:String?, var coverAbsolutePath:String? @@ -61,7 +62,7 @@ data class LocalMediaItem( val mediaMetadata = getMediaMetadata() if (mediaType == "book") { val chapters = getAudiobookChapters() - val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size) + val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,ebookFile,getTotalSize(),getDuration(),audioTracks.size) return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null) } else { val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt index 7f177bd1..6f8ec75b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaProgress.kt @@ -13,6 +13,8 @@ class LocalMediaProgress( progress:Double, // 0 to 1 currentTime:Double, isFinished:Boolean, + var ebookLocation:String?, // cfi tag + var ebookProgress:Double?, // 0 to 1 var lastUpdate:Long, var startedAt:Long, var finishedAt:Long?, @@ -58,11 +60,20 @@ class LocalMediaProgress( finishedAt = if (isFinished) lastUpdate else null } + @JsonIgnore + fun updateEbookProgress(ebookLocation:String, ebookProgress:Double) { + lastUpdate = System.currentTimeMillis() + this.ebookProgress = ebookProgress + this.ebookLocation = ebookLocation + } + @JsonIgnore fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) { isFinished = serverMediaProgress.isFinished progress = serverMediaProgress.progress currentTime = serverMediaProgress.currentTime + ebookProgress = serverMediaProgress.ebookProgress + ebookLocation = serverMediaProgress.ebookLocation duration = serverMediaProgress.duration lastUpdate = serverMediaProgress.lastUpdate finishedAt = serverMediaProgress.finishedAt diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt index 415ae2b0..2f256b82 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/MediaProgress.kt @@ -12,6 +12,8 @@ class MediaProgress( progress:Double, // 0 to 1 currentTime:Double, isFinished:Boolean, + var ebookLocation:String?, // cfi tag + var ebookProgress:Double?, // 0 to 1 var lastUpdate:Long, var startedAt:Long, var finishedAt:Long? diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt index d7fccbb0..288af33b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -264,6 +264,6 @@ class PlaybackSession( @JsonIgnore fun getNewLocalMediaProgress():LocalMediaProgress { - return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId) + return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,null,null,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId) } } 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 fc6998fa..9fc5a94d 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 @@ -72,7 +72,7 @@ class FolderScanner(var ctx: Context) { Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}") val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) } - val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream")) + val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*")) // Do not scan folders that have no media items and not an existing item already if (existingItem != null || filesInFolder.isNotEmpty()) { @@ -110,6 +110,8 @@ class FolderScanner(var ctx: Context) { var startOffset = 0.0 var coverContentUrl:String? = null var coverAbsolutePath:String? = null + var hasEBookFile = false + var newEBookFile:EBookFile? = null val existingLocalFilesRemoved = existingLocalFiles.filter { elf -> filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder @@ -122,7 +124,6 @@ class FolderScanner(var ctx: Context) { filesInFolder.forEach { file -> val mimeType = file.mimeType ?: "" val filename = file.name ?: "" - val isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4" Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName") val localFileId = DeviceManager.getBase64Id(file.id) @@ -132,7 +133,7 @@ class FolderScanner(var ctx: Context) { Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}") - if (isAudio) { + if (localFile.isAudioFile()) { val audioTrackToAdd:AudioTrack? val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId } @@ -174,6 +175,15 @@ class FolderScanner(var ctx: Context) { startOffset += audioTrackToAdd.duration index++ audioTracks.add(audioTrackToAdd) + } else if (localFile.isEBookFile()) { + val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId } + + if (localFolder.mediaType == "book") { + hasEBookFile = true + if (existingLocalFile == null) { + newEBookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl) + } + } } else { val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId } @@ -198,7 +208,7 @@ class FolderScanner(var ctx: Context) { } } - if (existingItem != null && audioTracks.isEmpty()) { + if (existingItem != null && audioTracks.isEmpty() && !hasEBookFile) { Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item") DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id) return ItemScanResult.REMOVED @@ -210,9 +220,9 @@ class FolderScanner(var ctx: Context) { existingItem.updateFromScan(audioTracks,localFiles) DeviceManager.dbManager.saveLocalLibraryItem(existingItem) return ItemScanResult.UPDATED - } else if (audioTracks.isNotEmpty()) { + } else if (audioTracks.isNotEmpty() || newEBookFile != null) { Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files") - val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath) + val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,newEBookFile,localFiles,coverContentUrl,coverAbsolutePath) val localLibraryItem = localMediaItem.getLocalLibraryItem() DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem) return ItemScanResult.ADDED @@ -257,7 +267,7 @@ class FolderScanner(var ctx: Context) { // Search for files in media item folder // m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154 - val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream")) + val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*")) Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}") var localEpisodeId:String? = null @@ -274,6 +284,7 @@ class FolderScanner(var ctx: Context) { } val audioTracks:MutableList = mutableListOf() + var foundEBookFile = false filesFound.forEach { docFile -> val itemPart = downloadItem.downloadItemParts.find { itemPart -> @@ -304,6 +315,16 @@ class FolderScanner(var ctx: Context) { localEpisodeId = newEpisode.id Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}") } + } else if (itemPart.ebookFile != null) { // Ebook + foundEBookFile = true + Log.d(tag, "scanDownloadItem: Ebook file found with mimetype=${docFile.mimeType}") + 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()) + localLibraryItem.localFiles.add(localFile) + + val ebookFile = EBookFile(itemPart.ebookFile.ino, itemPart.ebookFile.metadata, itemPart.ebookFile.ebookFormat, true, localFileId, localFile.contentUrl) + (localLibraryItem.media as Book).ebookFile = ebookFile + Log.d(tag, "scanDownloadItem: Ebook file added to lli ${localFile.contentUrl}") } else { // Cover image 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,8 +335,8 @@ class FolderScanner(var ctx: Context) { } } - if (audioTracks.isEmpty()) { - Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}") + if (audioTracks.isEmpty() && !foundEBookFile) { + Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}") return cb(null) } @@ -350,6 +371,8 @@ class FolderScanner(var ctx: Context) { progress = mediaProgress.progress, currentTime = mediaProgress.currentTime, isFinished = false, + ebookLocation = mediaProgress.ebookLocation, + ebookProgress = mediaProgress.ebookProgress, lastUpdate = mediaProgress.lastUpdate, startedAt = mediaProgress.startedAt, finishedAt = mediaProgress.finishedAt, @@ -381,7 +404,7 @@ class FolderScanner(var ctx: Context) { var wasUpdated = false // Search for files in media item folder - val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream")) + val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*")) Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}") filesFound.forEach { @@ -427,7 +450,7 @@ class FolderScanner(var ctx: Context) { val audioProbeResult = probeAudioFile(localFile.absolutePath) val existingTrack = existingAudioTracks.find { audioTrack -> - audioTrack.localFileId == localFile.id + audioTrack.localFileId == localFileId } if (existingTrack == null) { @@ -446,6 +469,16 @@ class FolderScanner(var ctx: Context) { wasUpdated = true } + } else if (localFile.isEBookFile()) { + if (localLibraryItem.mediaType == "book") { + val existingEbookFile = (localLibraryItem.media as Book).ebookFile + if (existingEbookFile == null || existingEbookFile.localFileId != localFileId) { + val ebookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl) + (localLibraryItem.media as Book).ebookFile = ebookFile + Log.d(tag, "scanLocalLibraryItem: Ebook file added to lli ${localFile.contentUrl}") + wasUpdated = true + } + } } else { // Check if cover is empty if (localLibraryItem.coverContentUrl == null) { Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}") 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 aceed2f4..ba5c7423 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 @@ -4,6 +4,7 @@ import android.app.DownloadManager import android.net.Uri import android.util.Log import com.audiobookshelf.app.data.AudioTrack +import com.audiobookshelf.app.data.EBookFile import com.audiobookshelf.app.data.LocalFolder import com.audiobookshelf.app.data.PodcastEpisode import com.audiobookshelf.app.device.DeviceManager @@ -20,6 +21,7 @@ data class DownloadItemPart( val localFolderName: String, val localFolderUrl: String, val localFolderId: String, + val ebookFile: EBookFile?, val audioTrack: AudioTrack?, val episode: PodcastEpisode?, var completed:Boolean, @@ -35,7 +37,7 @@ data class DownloadItemPart( var bytesDownloaded: Long ) { companion object { - fun make(downloadItemId:String, filename:String, fileSize: Long, 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, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart { val destinationUri = Uri.fromFile(destinationFile) val finalDestinationUri = Uri.fromFile(finalDestinationFile) @@ -53,6 +55,7 @@ data class DownloadItemPart( localFolderName = localFolder.name, localFolderUrl = localFolder.contentUrl, localFolderId = localFolder.id, + ebookFile = ebookFile, audioTrack = audioTrack, episode = episode, completed = false, diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index 05bd575f..74b222d7 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -265,6 +265,8 @@ class AbsDatabase : Plugin() { progress = mediaProgress.progress, currentTime = mediaProgress.currentTime, isFinished = mediaProgress.isFinished, + ebookLocation = mediaProgress.ebookLocation, + ebookProgress = mediaProgress.ebookProgress, lastUpdate = mediaProgress.lastUpdate, startedAt = mediaProgress.startedAt, finishedAt = mediaProgress.finishedAt, @@ -345,6 +347,8 @@ class AbsDatabase : Plugin() { progress = if (isFinished) 1.0 else 0.0, currentTime = 0.0, isFinished = isFinished, + ebookLocation = null, + ebookProgress = null, lastUpdate = currentTime, startedAt = if (isFinished) currentTime else 0L, finishedAt = if (isFinished) currentTime else null, @@ -389,6 +393,55 @@ class AbsDatabase : Plugin() { } } + @PluginMethod + fun updateLocalEbookProgress(call:PluginCall) { + val localLibraryItemId = call.getString("localLibraryItemId", "").toString() + val ebookLocation = call.getString("ebookLocation", "").toString() + val ebookProgress = call.getDouble("ebookProgress") ?: 0.0 + + val localMediaProgressId = localLibraryItemId + var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId) + + if (localMediaProgress == null) { + Log.d(tag, "updateLocalEbookProgress Local Media Progress not found $localMediaProgressId - Creating new") + val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId) + ?: return call.resolve(JSObject("{\"error\":\"Library Item not found\"}")) + + val book = localLibraryItem.media as Book + + localMediaProgress = LocalMediaProgress( + id = localMediaProgressId, + localLibraryItemId = localLibraryItemId, + localEpisodeId = null, + duration = book.duration ?: 0.0, + progress = 0.0, + currentTime = 0.0, + isFinished = false, + ebookLocation = ebookLocation, + ebookProgress = ebookProgress, + lastUpdate = System.currentTimeMillis(), + startedAt = 0L, + finishedAt = null, + serverConnectionConfigId = localLibraryItem.serverConnectionConfigId, + serverAddress = localLibraryItem.serverAddress, + serverUserId = localLibraryItem.serverUserId, + libraryItemId = localLibraryItem.libraryItemId, + episodeId = null) + } else { + localMediaProgress.updateEbookProgress(ebookLocation, ebookProgress) + } + + // Save local media progress locally + DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress) + + val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress) + Log.d(tag, "updateLocalEbookProgress: Local Media Progress String $lmpstring") + + val jsobj = JSObject() + jsobj.put("localMediaProgress", JSObject(lmpstring)) + call.resolve(jsobj) + } + @PluginMethod fun updateLocalTrackOrder(call:PluginCall) { val localLibraryItemId = call.getString("localLibraryItemId", "") ?: "" 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 3121af6f..51b0ef25 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 @@ -2,7 +2,6 @@ package com.audiobookshelf.app.plugins import android.app.DownloadManager import android.content.Context -import android.os.Build import android.os.Environment import android.util.Log import com.audiobookshelf.app.MainActivity @@ -142,6 +141,28 @@ class AbsDownloader : Plugin() { val itemFolderPath = "${localFolder.absolutePath}/$itemSubfolder" val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf()) + val book = libraryItem.media as Book + book.ebookFile?.let { ebookFile -> + val fileSize = ebookFile.metadata?.size ?: 0 + val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(ebookFile.metadata?.relPath ?: "")}" + val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "") + val finalDestinationFile = File("$itemFolderPath/$destinationFilename") + val destinationFile = File("$tempFolderPath/$destinationFilename") + + if (destinationFile.exists()) { + Log.d(tag, "TEMP ebook file already exists, removing it from ${destinationFile.absolutePath}") + destinationFile.delete() + } + + if (finalDestinationFile.exists()) { + Log.d(tag, "ebook file already exists, removing it from ${finalDestinationFile.absolutePath}") + finalDestinationFile.delete() + } + + val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null) + downloadItem.downloadItemParts.add(downloadItemPart) + } + // Create download item part for each audio track tracks.forEach { audioTrack -> val fileSize = audioTrack.metadata?.size ?: 0 @@ -162,7 +183,7 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,audioTrack,null) + val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null) downloadItem.downloadItemParts.add(downloadItemPart) } @@ -187,7 +208,7 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null) + val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null) downloadItem.downloadItemParts.add(downloadItemPart) } @@ -216,7 +237,7 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode) + var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode) downloadItem.downloadItemParts.add(downloadItemPart) if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) { @@ -232,7 +253,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,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null) + downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null) downloadItem.downloadItemParts.add(downloadItemPart) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index afd27a01..931fc094 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -356,6 +356,16 @@ class ApiHandler(var ctx:Context) { Log.d(tag, "Server progress for media item id=\"${mediaProgress.mediaItemId}\" is more recent then local. Updating local current time ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}") localMediaProgress.updateFromServerMediaProgress(mediaProgress) MediaEventManager.syncEvent(mediaProgress, "Sync on server connection") + } else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) { + // Patch ebook progress to server + val endpoint = "/api/me/progress/${localMediaProgress.libraryItemId}" + val updatePayload = JSObject() + updatePayload.put("ebookLocation", localMediaProgress.ebookLocation) + updatePayload.put("ebookProgress", localMediaProgress.ebookProgress) + updatePayload.put("lastUpdate", localMediaProgress.lastUpdate) + patchRequest(endpoint,updatePayload) { + Log.d(tag, "syncLocalMediaProgressForUser patched ebook progress") + } } } } diff --git a/components/readers/EpubReader.vue b/components/readers/EpubReader.vue index cd38be3e..72190eb9 100644 --- a/components/readers/EpubReader.vue +++ b/components/readers/EpubReader.vue @@ -20,7 +20,8 @@ export default { libraryItem: { type: Object, default: () => {} - } + }, + isLocal: Boolean }, data() { return { @@ -41,6 +42,22 @@ export default { libraryItemId() { return this.libraryItem?.id }, + localLibraryItem() { + if (this.isLocal) return this.libraryItem + return this.libraryItem.localLibraryItem || null + }, + localLibraryItemId() { + return this.localLibraryItem?.id + }, + serverLibraryItemId() { + if (!this.isLocal) return this.libraryItem.id + // Check if local library item is connected to the current server + if (!this.libraryItem.serverAddress || !this.libraryItem.libraryItemId) return null + if (this.$store.getters['user/getServerAddress'] === this.libraryItem.serverAddress) { + return this.libraryItem.libraryItemId + } + return null + }, playerLibraryItemId() { return this.$store.state.playerLibraryItemId }, @@ -51,9 +68,15 @@ export default { chapters() { return this.book ? this.book.navigation.toc : [] }, - userMediaProgress() { - if (!this.libraryItemId) return - return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) + userItemProgress() { + if (this.isLocal) return this.localItemProgress + return this.serverItemProgress + }, + localItemProgress() { + return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId) + }, + serverItemProgress() { + return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId) }, localStorageLocationsKey() { return `ebookLocations-${this.libraryItemId}` @@ -80,10 +103,25 @@ export default { * @param {string} payload.ebookLocation - CFI of the current location * @param {string} payload.ebookProgress - eBook Progress Percentage */ - updateProgress(payload) { - this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { - console.error('EpubReader.updateProgress failed:', error) - }) + async updateProgress(payload) { + // Update local item + if (this.localLibraryItemId) { + const localPayload = { + localLibraryItemId: this.localLibraryItemId, + ...payload + } + const localResponse = await this.$db.updateLocalEbookProgress(localPayload) + if (localResponse.localMediaProgress) { + this.$store.commit('globals/updateLocalMediaProgress', localResponse.localMediaProgress) + } + } + + // Update server item + if (this.serverLibraryItemId) { + this.$axios.$patch(`/api/me/progress/${this.serverLibraryItemId}`, payload).catch((error) => { + console.error('EpubReader.updateProgress failed:', error) + }) + } }, getAllEbookLocationData() { const locations = [] @@ -172,7 +210,7 @@ export default { }, /** @param {string} location - CFI of the new location */ relocated(location) { - if (this.userMediaProgress?.ebookLocation === location.start.cfi) { + if (this.userItemProgress?.ebookLocation === location.start.cfi) { return } @@ -189,7 +227,7 @@ export default { } }, initEpub() { - this.progress = Math.round((this.userMediaProgress?.ebookProgress || 0) * 100) + this.progress = Math.round((this.userItemProgress?.ebookProgress || 0) * 100) /** @type {EpubReader} */ const reader = this @@ -210,7 +248,7 @@ export default { }) // load saved progress - reader.rendition.display(this.userMediaProgress?.ebookLocation || reader.book.locations.start) + reader.rendition.display(this.userItemProgress?.ebookLocation || reader.book.locations.start) // load style reader.rendition.themes.default({ '*': { color: '#fff!important' } }) diff --git a/components/readers/Reader.vue b/components/readers/Reader.vue index 829aae89..ef4f34ad 100644 --- a/components/readers/Reader.vue +++ b/components/readers/Reader.vue @@ -5,11 +5,13 @@
close
- +