Update android scanner and add local media file page, scan library items

This commit is contained in:
advplyr 2022-04-06 20:36:17 -05:00
parent 12de187b7a
commit ee942c6704
9 changed files with 540 additions and 45 deletions

View file

@ -34,7 +34,11 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
@JsonIgnore
open fun getAudioTracks():List<AudioTrack> { return mutableListOf() }
@JsonIgnore
open fun setAudioTracks(audioTracks:List<AudioTrack>) { }
open fun setAudioTracks(audioTracks:MutableList<AudioTrack>) { }
@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<AudioTrack>) {
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
// 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<String>,
var audioFiles:List<AudioFile>,
var chapters:List<BookChapter>,
var tracks:List<AudioTrack>?,
var tracks:MutableList<AudioTrack>?,
var size:Long?,
var duration:Double?
) : MediaType(metadata, coverPath) {
@ -82,9 +95,30 @@ class Book(
return tracks ?: mutableListOf()
}
@JsonIgnore
override fun setAudioTracks(audioTracks:List<AudioTrack>) {
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
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

View file

@ -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<AbsDownloader.DownloadItem> {
var downloadItems:MutableList<AbsDownloader.DownloadItem> = 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)

View file

@ -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<AudioTrack>, _localFiles:MutableList<LocalFile>) {
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
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,

View file

@ -8,3 +8,8 @@ data class FolderScanResult(
val localFolder:LocalFolder,
val localLibraryItems:List<LocalLibraryItem>,
)
data class LocalLibraryItemScanResult(
val updated:Boolean,
val localLibraryItem:LocalLibraryItem,
)

View file

@ -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<LocalFile> = mutableListOf()
var audioTracks:MutableList<AudioTrack> = 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<AudioProbeResult>(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<AudioProbeResult>(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)
}
}

View file

@ -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<DownloadItemPart>
)
@ -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)
}
}

View file

@ -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()

View file

@ -13,7 +13,7 @@
</div>
<div v-else class="w-full media-item-container overflow-y-auto">
<template v-for="mediaItem in localLibraryItems">
<div :key="mediaItem.id" class="flex my-1">
<nuxt-link :to="`/localMedia/item/${mediaItem.id}`" :key="mediaItem.id" class="flex my-1">
<div class="w-12 h-12 bg-primary">
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
</div>
@ -23,11 +23,12 @@
<p v-else-if="mediaItem.type == 'podcast'">{{ mediaItem.media.episodes.length }} Tracks</p>
</div>
<div class="w-12 h-12 flex items-center justify-center">
<button v-if="!isMissing" class="shadow-sm text-accent flex items-center justify-center rounded-full" @click.stop="play(mediaItem)">
<span class="material-icons text-xl text-gray-300">arrow_right</span>
<!-- <button class="shadow-sm text-accent flex items-center justify-center rounded-full" @click.stop="play(mediaItem)">
<span class="material-icons" style="font-size: 2rem">play_arrow</span>
</button>
</button> -->
</div>
</div>
</nuxt-link>
</template>
</div>
</div>

View file

@ -0,0 +1,177 @@
<template>
<div class="w-full h-full py-6 px-2">
<div v-if="localLibraryItem" class="w-full h-full">
<div class="flex items-center mb-4">
<button v-if="audioTracks.length" class="shadow-sm text-accent flex items-center justify-center rounded-full" @click.stop="play">
<span class="material-icons" style="font-size: 2rem">play_arrow</span>
</button>
<div class="flex-grow" />
<ui-btn v-if="!removingItem" :loading="isScanning" small @click="clickScan">Scan</ui-btn>
<ui-btn v-if="!removingItem" :loading="isScanning" small class="ml-2" color="warning" @click="clickForceRescan">Force Re-Scan</ui-btn>
<ui-icon-btn class="ml-2" bg-color="error" outlined :loading="removingItem" icon="delete" @click="clickDeleteItem" />
</div>
<p class="text-lg mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
<p class="mb-4 text-xl">{{ mediaMetadata.title }}</p>
<div v-if="isScanning" class="w-full text-center p-4">
<p>Scanning...</p>
</div>
<div v-else class="w-full media-item-container overflow-y-auto">
<p class="text-lg mb-2">Audio Tracks</p>
<template v-for="track in audioTracks">
<div :key="track.localFileId" class="flex items-center my-1">
<div class="w-12 h-12 flex items-center justify-center">
<p class="font-mono font-bold text-xl">{{ track.index }}</p>
</div>
<div class="flex-grow px-2">
<p class="text-sm">{{ track.title }}</p>
</div>
<div class="w-20 text-center text-gray-300">
<p class="text-xs">{{ track.mimeType }}</p>
<p class="text-sm">{{ $elapsedPretty(track.duration) }}</p>
</div>
</div>
</template>
<p class="text-lg mb-2 pt-8">Local Files</p>
<template v-for="file in localFiles">
<div :key="file.id" class="flex items-center my-1">
<div class="w-12 h-12 flex items-center justify-center">
<img v-if="(file.mimeType || '').startsWith('image')" :src="getCapImageSrc(file.contentUrl)" class="w-full h-full object-contain" />
<span v-else class="material-icons">music_note</span>
</div>
<div class="flex-grow px-2">
<p class="text-sm">{{ file.filename }}</p>
</div>
<div class="w-20 text-center text-gray-300">
<p class="text-xs">{{ file.mimeType }}</p>
<p class="text-sm">{{ $bytesPretty(file.size) }}</p>
</div>
</div>
</template>
<p class="py-4">{{ audioTracks.length }} Audio Tracks</p>
</div>
</div>
<div v-else class="w-full h-full">
<p class="text-lg text-center px-8">{{ failed ? 'Failed to get local library item ' + localLibraryItemId : 'Loading..' }}</p>
</div>
</div>
</template>
<script>
import { Capacitor } from '@capacitor/core'
import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem } from '@/plugins/capacitor'
export default {
asyncData({ params }) {
return {
localLibraryItemId: params.id
}
},
data() {
return {
failed: false,
localLibraryItem: null,
removingItem: false,
folderId: null,
folder: null,
isScanning: false
}
},
computed: {
localFiles() {
return this.localLibraryItem ? this.localLibraryItem.localFiles : []
},
folderName() {
return this.folder ? this.folder.name : null
},
mediaType() {
return this.localLibraryItem ? this.localLibraryItem.mediaType : null
},
media() {
return this.localLibraryItem ? this.localLibraryItem.media : null
},
mediaMetadata() {
return this.media ? this.media.metadata || {} : {}
},
audioTracks() {
if (!this.media) return []
if (this.mediaType == 'book') {
return this.media.tracks || []
} else {
return this.media.episodes || []
}
}
},
methods: {
play() {
this.$eventBus.$emit('play-item', this.localLibraryItemId)
},
getCapImageSrc(contentUrl) {
return Capacitor.convertFileSrc(contentUrl)
},
clickScan() {
this.scanItem()
},
clickForceRescan() {
this.scanItem(true)
},
async clickDeleteItem() {
var deleteMessage = 'Are you sure you want to remove this local library item? (does not delete anything in your file system)'
const { value } = await Dialog.confirm({
title: 'Confirm',
message: deleteMessage
})
if (value) {
this.removingItem = true
await AbsFileSystem.removeLocalLibraryItem({ localLibraryItemId: this.localLibraryItemId })
this.removingItem = false
this.$router.replace(`/localMedia/folders/${this.folderId}`)
}
},
play(mediaItem) {
this.$eventBus.$emit('play-item', mediaItem.id)
},
async scanItem(forceAudioProbe = false) {
this.isScanning = true
var response = await AbsFileSystem.scanLocalLibraryItem({ localLibraryItemId: this.localLibraryItemId, forceAudioProbe })
if (response && response.localLibraryItem) {
if (response.updated) {
this.$toast.success('Local item was updated')
this.localLibraryItem = response.localLibraryItem
} else {
this.$toast.info('Local item was up to date')
}
} else {
console.log('Failed')
this.$toast.error('Something went wrong..')
}
this.isScanning = false
},
async init() {
this.localLibraryItem = await this.$db.getLocalLibraryItem(this.localLibraryItemId)
if (!this.localLibraryItem) {
console.error('Failed to get local library item', this.localLibraryItemId)
this.failed = true
return
}
console.log('Got local library item', JSON.stringify(this.localLibraryItem))
this.folderId = this.localLibraryItem.folderId
this.folder = await this.$db.getLocalFolder(this.folderId)
}
},
mounted() {
this.init()
}
}
</script>
<style scoped>
.media-item-container {
height: calc(100vh - 200px);
max-height: calc(100vh - 200px);
}
</style>