New downloader for downloading multiple tracks, android media player support for using multiple tracks

This commit is contained in:
advplyr 2022-04-02 19:43:43 -05:00
parent f70f707100
commit 7a091dd428
15 changed files with 767 additions and 457 deletions

View file

@ -7,6 +7,7 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View file

@ -7,15 +7,29 @@ import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.*
import com.anggrayudi.storage.media.FileDescription
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LocalFolder
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.*
@CapacitorPlugin(name = "AudioDownloader")
@ -24,21 +38,46 @@ class AudioDownloader : Plugin() {
lateinit var mainActivity: MainActivity
lateinit var downloadManager: DownloadManager
lateinit var apiHandler: ApiHandler
lateinit var folderScanner: FolderScanner
// data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
// fun toJSObject() : JSObject {
// var obj = JSObject()
// obj.put("uri", this.uri)
// obj.put("name", this.name)
// obj.put("size", this.size)
// obj.put("coverUrl", this.coverUrl)
// return obj
// }
// }
data class DownloadItemPart(
val id: String,
val name: String,
val itemTitle: String,
val serverPath: String,
val folderName: String,
val localFolderId: String,
@JsonIgnore val uri: Uri,
@JsonIgnore val destinationUri: Uri,
var downloadId: Long?,
var progress: Long
) {
@JsonIgnore
fun getDownloadRequest(): DownloadManager.Request {
var dlRequest = DownloadManager.Request(uri)
dlRequest.setTitle(name)
dlRequest.setDescription("Downloading to $folderName for book $itemTitle")
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
dlRequest.setDestinationUri(destinationUri)
return dlRequest
}
}
data class DownloadItem(
val id: String,
val localFolder: LocalFolder,
val itemTitle: String,
val downloadItemParts: MutableList<DownloadItemPart>
)
var downloadQueue: MutableList<DownloadItem> = mutableListOf()
override fun load() {
mainActivity = (activity as MainActivity)
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
folderScanner = FolderScanner(mainActivity)
apiHandler = ApiHandler(mainActivity)
var recieverEvent: (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
if (evt == "complete") {
@ -52,303 +91,355 @@ class AudioDownloader : Plugin() {
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
}
// @PluginMethod
// fun load(call: PluginCall) {
// var audiobookUrls = call.data.getJSONArray("audiobookUrls")
// var len = audiobookUrls?.length()
// if (len == null) {
// len = 0
// }
// Log.d(tag, "CALLED LOAD $len")
// var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
//
// (0 until len).forEach {
// var jsobj = audiobookUrls.get(it) as JSONObject
// var audiobookUrl = jsobj.get("contentUrl").toString()
// var coverUrl = jsobj.get("coverUrl").toString()
// var storageId = ""
// if(jsobj.has("storageId")) jsobj.get("storageId").toString()
//
// var basePath = ""
// if(jsobj.has("basePath")) jsobj.get("basePath").toString()
//
// var coverBasePath = ""
// if(jsobj.has("coverBasePath")) jsobj.get("coverBasePath").toString()
//
// Log.d(tag, "LOOKUP $storageId $basePath $audiobookUrl")
//
// var audiobookFile: DocumentFile? = null
// var coverFile: DocumentFile? = null
//
// // Android 9 OR Below use storage id and base path
// if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
// audiobookFile = DocumentFileCompat.fromSimplePath(context, storageId, basePath)
// if (coverUrl != null && coverUrl != "") {
// coverFile = DocumentFileCompat.fromSimplePath(context, storageId, coverBasePath)
// }
// } else {
// // Android 10 and up manually deleting will still load the file causing crash
// var exists = checkUriExists(Uri.parse(audiobookUrl))
// if (exists) {
// Log.d(tag, "Audiobook exists")
// audiobookFile = DocumentFileCompat.fromUri(context, Uri.parse(audiobookUrl))
// } else {
// Log.e(tag, "Audiobook does not exist")
// }
//
// var coverExists = checkUriExists(Uri.parse(coverUrl))
// if (coverExists) {
// Log.d(tag, "Cover Exists")
// coverFile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
// } else if (coverUrl != null && coverUrl != "") {
// Log.e(tag, "Cover does not exist")
// }
// }
//
// if (audiobookFile == null) {
// Log.e(tag, "Audiobook was not found $audiobookUrl")
// } else {
// Log.d(tag, "Audiobook File Found StorageId:${audiobookFile.getStorageId(context)} | AbsolutePath:${audiobookFile.getAbsolutePath(context)} | BasePath:${audiobookFile.getBasePath(context)}")
//
// var _name = audiobookFile.name
// if (_name == null) _name = ""
//
// var size = audiobookFile.length()
//
// if (audiobookFile.uri.toString() !== audiobookUrl) {
// Log.d(tag, "Audiobook URI ${audiobookFile.uri} is different from $audiobookUrl => using the latter")
// }
//
// // Use existing URI's - bug happening where new uri is different from initial
// var abItem = AudiobookItem(Uri.parse(audiobookUrl), _name, size, coverUrl)
//
// Log.d(tag, "Setting AB ITEM ${abItem.name} | ${abItem.size} | ${abItem.uri} | ${abItem.coverUrl}")
//
// audiobookItems.add(abItem)
// }
// }
//
// Log.d(tag, "Load Finished ${audiobookItems.size} found")
//
// var audiobookObjs:List<JSObject> = audiobookItems.map{ it.toJSObject() }
// var mediaItemNoticePayload = JSObject()
// mediaItemNoticePayload.put("items", audiobookObjs)
// notifyListeners("onMediaLoaded", mediaItemNoticePayload)
// }
@PluginMethod
fun download(call: PluginCall) {
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
var url = call.data.getString("downloadUrl", "unknown").toString()
var coverDownloadUrl = call.data.getString("coverDownloadUrl", "").toString()
var title = call.data.getString("title", "Audiobook").toString()
var filename = call.data.getString("filename", "audiobook.mp3").toString()
var coverFilename = call.data.getString("coverFilename", "cover.png").toString()
var downloadFolderUrl = call.data.getString("downloadFolderUrl", "").toString()
var folder = DocumentFileCompat.fromUri(context, Uri.parse(downloadFolderUrl))!!
Log.d(tag, "Called download: $url | Folder: ${folder.name} | $downloadFolderUrl")
fun downloadLibraryItem(call: PluginCall) {
var libraryItemId = call.data.getString("libraryItemId").toString()
var localFolderId = call.data.getString("localFolderId").toString()
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId")
var dlfilename = audiobookId + "." + File(filename).extension
var coverdlfilename = audiobookId + "." + File(coverFilename).extension
Log.d(tag, "DL Filename $dlfilename | Cover DL Filename $coverdlfilename")
var canWriteToFolder = folder.canWrite()
if (!canWriteToFolder) {
Log.e(tag, "Error Cannot Write to Folder ${folder.baseName}")
val ret = JSObject()
ret.put("error", "Cannot write to ${folder.baseName}")
call.resolve(ret)
return
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()
}
}
var dlRequest = DownloadManager.Request(Uri.parse(url))
dlRequest.setTitle("Ab: $title")
dlRequest.setDescription("Downloading to ${folder.name}")
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, dlfilename)
var audiobookDownloadId = downloadManager.enqueue(dlRequest)
var coverDownloadId:Long? = null
if (coverDownloadUrl != "") {
var coverDlRequest = DownloadManager.Request(Uri.parse(coverDownloadUrl))
coverDlRequest.setTitle("Cover: $title")
coverDlRequest.setDescription("Downloading to ${folder.name}")
coverDlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
coverDlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, coverdlfilename)
coverDownloadId = downloadManager.enqueue(coverDlRequest)
call.resolve(JSObject("{\"error\":\"Library Item not found\"}"))
}
var progressReceiver : (id:Long, prog: Long) -> Unit = { id:Long, prog: Long ->
if (id == audiobookDownloadId) {
// Clean folder path so it can be used in URL
fun cleanRelPath(relPath: String): String {
var cleanedRelPath = relPath.replace("\\", "/").replace("%", "%25").replace("#", "%23")
return if (cleanedRelPath.startsWith("/")) cleanedRelPath.substring(1) else cleanedRelPath
}
// Item filenames could be the same if they are in subfolders, this will make them unique
fun getFilenameFromRelPath(relPath: String): String {
var cleanedRelPath = relPath.replace("\\", "_").replace("/", "_")
return if (cleanedRelPath.startsWith("_")) cleanedRelPath.substring(1) else cleanedRelPath
}
fun getAbMetadataText(libraryItem:LibraryItem):String {
var bookMedia = libraryItem.media as com.audiobookshelf.app.data.Book
var fileString = ";ABMETADATA1\n"
fileString += "#libraryItemId=${libraryItem.id}\n"
fileString += "title=${bookMedia.metadata.title}\n"
fileString += "author=${bookMedia.metadata.authorName}\n"
fileString += "narrator=${bookMedia.metadata.narratorName}\n"
fileString += "series=${bookMedia.metadata.seriesName}\n"
return fileString
}
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()
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.metadata.relPath)}"
var destinationFilename = getFilenameFromRelPath(audioFile.metadata.relPath)
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioFile.metadata.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
var destinationFile = File("$itemFolderPath/$destinationFilename")
var destinationUri = Uri.fromFile(destinationFile)
var downloadUri = Uri.parse("${apiHandler.serverUrl}${serverPath}?token=${apiHandler.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)
downloadItem.downloadItemParts.add(downloadItemPart)
var dlRequest = downloadItemPart.getDownloadRequest()
var downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
}
Log.d(tag, "Done queueing downloads ${downloadQueue.size}")
if (downloadItem.downloadItemParts.isNotEmpty()) {
// TODO: Cannot create new text file here but can download here... ??
// var abmetadataFile = File(itemFolderPath, "abmetadata.abs")
// abmetadataFile.createNewFileIfPossible()
// abmetadataFile.writeText(getAbMetadataText(libraryItem))
downloadQueue.add(downloadItem)
startWatchingDownloads(downloadItem)
}
} else {
// TODO: Download podcast episode(s)
}
}
fun startWatchingDownloads(downloadItem: DownloadItem) {
GlobalScope.launch(Dispatchers.IO) {
while (downloadItem.downloadItemParts.isNotEmpty()) {
checkDownloads(downloadItem)
notifyListeners("onItemDownloadUpdate", JSObject(jacksonObjectMapper().writeValueAsString(downloadItem)))
delay(500)
}
var folderScanResult = folderScanner.scanForMediaItems(downloadItem.localFolder, false)
Log.d(tag, "Item download complete ${downloadItem.itemTitle}")
var jsobj = JSObject()
jsobj.put("audiobookId", audiobookId)
jsobj.put("progress", prog)
notifyListeners("onDownloadProgress", jsobj)
jsobj.put("libraryItemId", downloadItem.id)
jsobj.put("localFolderId", downloadItem.localFolder.id)
jsobj.put("folderScanResult", JSObject(jacksonObjectMapper().writeValueAsString(folderScanResult)))
notifyListeners("onItemDownloadComplete", jsobj)
}
}
var coverDocFile:DocumentFile? = null
var doneReceiver : (id:Long, success: Boolean) -> Unit = { id:Long, success: Boolean ->
Log.d(tag, "RECEIVER DONE $id, SUCCES? $success")
var docfile:DocumentFile? = null
// Download was complete, now find downloaded file
if (id == coverDownloadId) {
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, coverdlfilename)
Log.d(tag, "Move Cover File ${docfile?.name}")
// For unknown reason, Android 10 test was using the title set in "setTitle" for the dl manager as the filename
// check if this was the case
if (docfile?.name == null) {
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, "Cover: $title")
Log.d(tag, "Cover File name attempt 2 ${docfile?.name}")
}
} else if (id == audiobookDownloadId) {
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, dlfilename)
Log.d(tag, "Move Audiobook File ${docfile?.name}")
if (docfile?.name == null) {
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, "Ab: $title")
Log.d(tag, "File name attempt 2 ${docfile?.name}")
}
}
// Callback for moving the downloaded file
var callback = object : FileCallback() {
override fun onPrepare() {
Log.d(tag, "PREPARING MOVE FILE")
}
override fun onFailed(errorCode:ErrorCode) {
Log.e(tag, "FAILED MOVE FILE $errorCode")
docfile?.delete()
coverDocFile?.delete()
if (id == audiobookDownloadId) {
var jsobj = JSObject()
jsobj.put("audiobookId", audiobookId)
jsobj.put("error", "Move failed")
notifyListeners("onDownloadFailed", jsobj)
}
}
override fun onCompleted(result:Any) {
var resultDocFile = result as DocumentFile
var simplePath = resultDocFile.getSimplePath(context)
var storageId = resultDocFile.getStorageId(context)
var size = resultDocFile.length()
Log.d(tag, "Finished Moving File, NAME: ${resultDocFile.name} | URI:${resultDocFile.uri} | AbsolutePath:${resultDocFile.getAbsolutePath(context)} | $storageId | SimplePath: $simplePath")
var abFolder = folder.findFolder(title)
var jsobj = JSObject()
jsobj.put("audiobookId", audiobookId)
jsobj.put("downloadId", id)
jsobj.put("storageId", storageId)
jsobj.put("storageType", resultDocFile.getStorageType(context))
jsobj.put("folderUrl", abFolder?.uri)
jsobj.put("folderName", abFolder?.name)
jsobj.put("downloadFolderUrl", downloadFolderUrl)
jsobj.put("contentUrl", resultDocFile.uri)
jsobj.put("basePath", resultDocFile.getBasePath(context))
jsobj.put("filename", filename)
jsobj.put("simplePath", simplePath)
jsobj.put("size", size)
if (resultDocFile.name == filename) {
Log.d(tag, "Audiobook Finishing Moving")
} else if (resultDocFile.name == coverFilename) {
coverDocFile = docfile
Log.d(tag, "Audiobook Cover Finished Moving")
jsobj.put("isCover", true)
}
notifyListeners("onDownloadComplete", jsobj)
}
}
// After file is downloaded, move the files into an audiobook directory inside the user selected folder
if (id == coverDownloadId) {
docfile?.moveFileTo(context, folder, FileDescription(coverFilename, title, MimeType.IMAGE), callback)
} else if (id == audiobookDownloadId) {
docfile?.moveFileTo(context, folder, FileDescription(filename, title, MimeType.AUDIO), callback)
}
}
var progressUpdater = DownloadProgressUpdater(downloadManager, audiobookDownloadId, progressReceiver, doneReceiver)
progressUpdater.run()
if (coverDownloadId != null) {
var coverProgressUpdater = DownloadProgressUpdater(downloadManager, coverDownloadId, progressReceiver, doneReceiver)
coverProgressUpdater.run()
}
val ret = JSObject()
ret.put("audiobookDownloadId", audiobookDownloadId)
ret.put("coverDownloadId", coverDownloadId)
call.resolve(ret)
}
internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long, Long) -> Unit, private var doneReceiver: (Long, Boolean) -> Unit) : Thread() {
private val query: DownloadManager.Query = DownloadManager.Query()
private var totalBytes: Int = 0
private var TAG = "DownloadProgressUpdater"
init {
query.setFilterById(this.downloadId)
}
override fun run() {
Log.d(TAG, "RUN FOR ID $downloadId")
var keepRunning = true
var increment = 0
while (keepRunning) {
Thread.sleep(500)
increment++
if (increment % 4 == 0) {
Log.d(TAG, "Loop $increment : $downloadId")
}
manager.query(query).use {
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!!
val query = DownloadManager.Query().setFilterById(dlid)
downloadManager.query(query).use {
if (it.moveToFirst()) {
//get total bytes of the file
if (totalBytes <= 0) {
totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
if (totalBytes <= 0) {
Log.e(TAG, "Download Is 0 Bytes $downloadId")
doneReceiver(downloadId, false)
keepRunning = false
this.interrupt()
return
}
}
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))
if (increment % 4 == 0) {
Log.d(TAG, "BYTES $increment : $downloadId : $bytesDownloadedSoFar : TOTAL: $totalBytes")
}
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
doneReceiver(downloadId, true)
} else {
doneReceiver(downloadId, false)
}
keepRunning = false
this.interrupt()
Log.d(tag, "Download ${downloadItemPart.name} Done")
downloadItem.downloadItemParts.remove(downloadItemPart)
} else if (downloadStatus == DownloadManager.STATUS_FAILED) {
Log.d(tag, "Download ${downloadItemPart.name} Failed")
downloadItem.downloadItemParts.remove(downloadItemPart)
} else {
//update progress
val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
receiver(downloadId, percentProgress)
val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
Log.d(tag, "${downloadItemPart.name} Progress = $percentProgress%")
downloadItemPart.progress = percentProgress
}
} else {
Log.e(TAG, "NOT FOUND IN QUERY")
keepRunning = false
Log.d(tag, "Download ${downloadItemPart.name} not found in dlmanager")
downloadItem.downloadItemParts.remove(downloadItemPart)
}
}
}
}
}
}
//
// @PluginMethod
// fun download(call: PluginCall) {
// var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
// var url = call.data.getString("downloadUrl", "unknown").toString()
// var coverDownloadUrl = call.data.getString("coverDownloadUrl", "").toString()
// var title = call.data.getString("title", "Audiobook").toString()
// var filename = call.data.getString("filename", "audiobook.mp3").toString()
// var coverFilename = call.data.getString("coverFilename", "cover.png").toString()
// var downloadFolderUrl = call.data.getString("downloadFolderUrl", "").toString()
// var folder = DocumentFileCompat.fromUri(context, Uri.parse(downloadFolderUrl))!!
// Log.d(tag, "Called download: $url | Folder: ${folder.name} | $downloadFolderUrl")
//
// var dlfilename = audiobookId + "." + File(filename).extension
// var coverdlfilename = audiobookId + "." + File(coverFilename).extension
// Log.d(tag, "DL Filename $dlfilename | Cover DL Filename $coverdlfilename")
//
// var canWriteToFolder = folder.canWrite()
// if (!canWriteToFolder) {
// Log.e(tag, "Error Cannot Write to Folder ${folder.baseName}")
// val ret = JSObject()
// ret.put("error", "Cannot write to ${folder.baseName}")
// call.resolve(ret)
// return
// }
//
// var dlRequest = DownloadManager.Request(Uri.parse(url))
// dlRequest.setTitle("Ab: $title")
// dlRequest.setDescription("Downloading to ${folder.name}")
// dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
// dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, dlfilename)
//
// var audiobookDownloadId = downloadManager.enqueue(dlRequest)
// var coverDownloadId:Long? = null
//
// if (coverDownloadUrl != "") {
// var coverDlRequest = DownloadManager.Request(Uri.parse(coverDownloadUrl))
// coverDlRequest.setTitle("Cover: $title")
// coverDlRequest.setDescription("Downloading to ${folder.name}")
// coverDlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
// coverDlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, coverdlfilename)
// coverDownloadId = downloadManager.enqueue(coverDlRequest)
// }
//
// var progressReceiver : (id:Long, prog: Long) -> Unit = { id:Long, prog: Long ->
// if (id == audiobookDownloadId) {
// var jsobj = JSObject()
// jsobj.put("audiobookId", audiobookId)
// jsobj.put("progress", prog)
// notifyListeners("onDownloadProgress", jsobj)
// }
// }
//
// var coverDocFile:DocumentFile? = null
//
// var doneReceiver : (id:Long, success: Boolean) -> Unit = { id:Long, success: Boolean ->
// Log.d(tag, "RECEIVER DONE $id, SUCCES? $success")
// var docfile:DocumentFile? = null
//
// // Download was complete, now find downloaded file
// if (id == coverDownloadId) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, coverdlfilename)
// Log.d(tag, "Move Cover File ${docfile?.name}")
//
// // For unknown reason, Android 10 test was using the title set in "setTitle" for the dl manager as the filename
// // check if this was the case
// if (docfile?.name == null) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, "Cover: $title")
// Log.d(tag, "Cover File name attempt 2 ${docfile?.name}")
// }
// } else if (id == audiobookDownloadId) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, dlfilename)
// Log.d(tag, "Move Audiobook File ${docfile?.name}")
//
// if (docfile?.name == null) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, "Ab: $title")
// Log.d(tag, "File name attempt 2 ${docfile?.name}")
// }
// }
//
// // Callback for moving the downloaded file
// var callback = object : FileCallback() {
// override fun onPrepare() {
// Log.d(tag, "PREPARING MOVE FILE")
// }
// override fun onFailed(errorCode:ErrorCode) {
// Log.e(tag, "FAILED MOVE FILE $errorCode")
//
// docfile?.delete()
// coverDocFile?.delete()
//
// if (id == audiobookDownloadId) {
// var jsobj = JSObject()
// jsobj.put("audiobookId", audiobookId)
// jsobj.put("error", "Move failed")
// notifyListeners("onDownloadFailed", jsobj)
// }
// }
// override fun onCompleted(result:Any) {
// var resultDocFile = result as DocumentFile
// var simplePath = resultDocFile.getSimplePath(context)
// var storageId = resultDocFile.getStorageId(context)
// var size = resultDocFile.length()
// Log.d(tag, "Finished Moving File, NAME: ${resultDocFile.name} | URI:${resultDocFile.uri} | AbsolutePath:${resultDocFile.getAbsolutePath(context)} | $storageId | SimplePath: $simplePath")
//
// var abFolder = folder.findFolder(title)
// var jsobj = JSObject()
// jsobj.put("audiobookId", audiobookId)
// jsobj.put("downloadId", id)
// jsobj.put("storageId", storageId)
// jsobj.put("storageType", resultDocFile.getStorageType(context))
// jsobj.put("folderUrl", abFolder?.uri)
// jsobj.put("folderName", abFolder?.name)
// jsobj.put("downloadFolderUrl", downloadFolderUrl)
// jsobj.put("contentUrl", resultDocFile.uri)
// jsobj.put("basePath", resultDocFile.getBasePath(context))
// jsobj.put("filename", filename)
// jsobj.put("simplePath", simplePath)
// jsobj.put("size", size)
//
// if (resultDocFile.name == filename) {
// Log.d(tag, "Audiobook Finishing Moving")
// } else if (resultDocFile.name == coverFilename) {
// coverDocFile = docfile
// Log.d(tag, "Audiobook Cover Finished Moving")
// jsobj.put("isCover", true)
// }
// notifyListeners("onDownloadComplete", jsobj)
// }
// }
//
// // After file is downloaded, move the files into an audiobook directory inside the user selected folder
// if (id == coverDownloadId) {
// docfile?.moveFileTo(context, folder, FileDescription(coverFilename, title, MimeType.IMAGE), callback)
// } else if (id == audiobookDownloadId) {
// docfile?.moveFileTo(context, folder, FileDescription(filename, title, MimeType.AUDIO), callback)
// }
// }
//
// var progressUpdater = DownloadProgressUpdater(downloadManager, audiobookDownloadId, progressReceiver, doneReceiver)
// progressUpdater.run()
// if (coverDownloadId != null) {
// var coverProgressUpdater = DownloadProgressUpdater(downloadManager, coverDownloadId, progressReceiver, doneReceiver)
// coverProgressUpdater.run()
// }
//
// val ret = JSObject()
// ret.put("audiobookDownloadId", audiobookDownloadId)
// ret.put("coverDownloadId", coverDownloadId)
// call.resolve(ret)
// }
//
//
//internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long, Long) -> Unit, private var doneReceiver: (Long, Boolean) -> Unit) : Thread() {
// private val query: DownloadManager.Query = DownloadManager.Query()
// private var totalBytes: Int = 0
// private var TAG = "DownloadProgressUpdater"
//
// init {
// query.setFilterById(this.downloadId)
// }
//
// override fun run() {
// Log.d(TAG, "RUN FOR ID $downloadId")
// var keepRunning = true
// var increment = 0
// while (keepRunning) {
// Thread.sleep(500)
// increment++
//
// if (increment % 4 == 0) {
// Log.d(TAG, "Loop $increment : $downloadId")
// }
//
// manager.query(query).use {
// if (it.moveToFirst()) {
// //get total bytes of the file
// if (totalBytes <= 0) {
// totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
// if (totalBytes <= 0) {
// Log.e(TAG, "Download Is 0 Bytes $downloadId")
// doneReceiver(downloadId, false)
// keepRunning = false
// this.interrupt()
// return
// }
// }
//
// val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
// val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
//
// if (increment % 4 == 0) {
// Log.d(TAG, "BYTES $increment : $downloadId : $bytesDownloadedSoFar : TOTAL: $totalBytes")
// }
//
// if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
// if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
// doneReceiver(downloadId, true)
// } else {
// doneReceiver(downloadId, false)
// }
// keepRunning = false
// this.interrupt()
// } else {
// //update progress
// val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
// receiver(downloadId, percentProgress)
// }
// } else {
// Log.e(TAG, "NOT FOUND IN QUERY")
// keepRunning = false
// }
// }
// }
// }
// }
//}

View file

@ -52,6 +52,9 @@ class MainActivity : BridgeActivity() {
Log.d(tag, "onCreate")
// var ss = SimpleStorage(this)
// ss.requestFullStorageAccess()
var permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,

View file

@ -381,10 +381,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
.setMediaId(currentPlaybackSession!!.id)
.setTitle(currentPlaybackSession!!.displayTitle)
.setSubtitle(currentPlaybackSession!!.displayAuthor)
.setMediaUri(currentPlaybackSession!!.getContentUri())
.setIconUri(currentPlaybackSession!!.getCoverUri())
return builder.build()
}
// .setMediaUri(currentPlaybackSession!!.getContentUri())
}
val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
@ -661,7 +661,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
}
/*
User callable methods
*/
@ -672,28 +671,44 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var metadata = playbackSession.getMediaMetadataCompat()
mediaSession.setMetadata(metadata)
var mediaMetadata = playbackSession.getExoMediaMetadata()
var mediaUri = playbackSession.getContentUri()
var mimeType = playbackSession.getMimeType()
var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
var mediaItems = playbackSession.getMediaItems()
if (mPlayer == currentPlayer) {
var mediaSource:MediaSource
if (!playbackSession.isHLS) {
Log.d(tag, "Playing Local File")
if (playbackSession.isLocal) {
Log.d(tag, "Playing Local Item")
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0])
} else if (!playbackSession.isHLS) {
Log.d(tag, "Direct Playing Item")
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0])
} else {
Log.d(tag, "Playing HLS File")
Log.d(tag, "Playing HLS Item")
var dataSourceFactory = DefaultHttpDataSource.Factory()
dataSourceFactory.setUserAgent(channelId)
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${playbackSession.token}"))
mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0])
}
Log.d(tag, "Playback Session CURRENT TIME ${playbackSession.currentTime} | ${playbackSession.currentTimeMs}")
mPlayer.setMediaSource(mediaSource, playbackSession.currentTimeMs)
mPlayer.setMediaSource(mediaSource)
// Add remaining media items if multi-track
if (mediaItems.size > 1) {
mPlayer.addMediaItems(mediaItems.subList(1, mediaItems.size))
Log.d(tag, "mPlayer total media items ${mPlayer.mediaItemCount}")
var currentTrackIndex = playbackSession.getCurrentTrackIndex()
var currentTrackTime = playbackSession.getCurrentTrackTimeMs()
Log.d(tag, "mPlayer current track index $currentTrackIndex & current track time $currentTrackTime")
mPlayer.seekTo(currentTrackIndex, currentTrackTime)
} else {
mPlayer.seekTo(playbackSession.currentTimeMs)
}
} else if (castPlayer != null) {
//// var mediaQueue = currentAudiobookStreamData!!.getCastQueue()
// // TODO: Start position will need to be adjusted if using multi-track queue
@ -784,12 +799,24 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
fun getCurrentTime() : Long {
if (currentPlayer.mediaItemCount > 1) {
var windowIndex = currentPlayer.currentWindowIndex
var currentTrackStartOffset = currentPlaybackSession?.getTrackStartOffsetMs(windowIndex) ?: 0L
return currentPlayer.currentPosition + currentTrackStartOffset
} else {
return currentPlayer.currentPosition
}
}
fun getBufferedTime() : Long {
if (currentPlayer.mediaItemCount > 1) {
var windowIndex = currentPlayer.currentWindowIndex
var currentTrackStartOffset = currentPlaybackSession?.getTrackStartOffsetMs(windowIndex) ?: 0L
return currentPlayer.bufferedPosition + currentTrackStartOffset
} else {
return currentPlayer.bufferedPosition
}
}
fun getTheLastPauseTime() : Long {
return lastPauseTime
@ -867,8 +894,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
fun seekPlayer(time: Long) {
if (currentPlayer.mediaItemCount > 1) {
currentPlaybackSession?.currentTime = time / 1000.0
var newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
var newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
} else {
currentPlayer.seekTo(time)
}
}
fun seekForward(amount: Long) {
currentPlayer.seekTo(mPlayer.currentPosition + amount)
@ -883,9 +917,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
fun terminateStream() {
// if (currentPlayer.playbackState == Player.STATE_READY) {
// currentPlayer.clearMediaItems()
// }
currentPlayer.clearMediaItems()
currentPlaybackSession = null
lastPauseTime = 0
@ -894,15 +925,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun sendClientMetadata(stateName: String) {
var metadata = JSObject()
var duration = mPlayer.duration
if (duration < 0) duration = 0
var duration = currentPlaybackSession?.getTotalDuration() ?: 0
metadata.put("duration", duration)
metadata.put("currentTime", mPlayer.currentPosition)
metadata.put("currentTime", getCurrentTime())
metadata.put("stateName", stateName)
clientEventEmitter?.onMetadata(metadata)
}
//
// MEDIA BROWSER STUFF (ANDROID AUTO)
//

View file

@ -47,7 +47,10 @@ data class Book(
var coverPath:String?,
var tags:MutableList<String>,
var audioFiles:MutableList<AudioFile>,
var chapters:MutableList<BookChapter>
var chapters:MutableList<BookChapter>,
var tracks:MutableList<AudioFile>?,
var size:Long?,
var duration:Double?
) : MediaType()
// This auto-detects whether it is a Book or Podcast
@ -154,7 +157,15 @@ data class AudioTrack(
var isLocal:Boolean,
var localFileId:String?,
var audioProbeResult:AudioProbeResult?
)
) {
@get:JsonIgnore
val startOffsetMs get() = (startOffset * 1000L).toLong()
@get:JsonIgnore
val durationMs get() = (duration * 1000L).toLong()
@get:JsonIgnore
val endOffsetMs get() = startOffsetMs + durationMs
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class BookChapter(

View file

@ -1,6 +1,7 @@
package com.audiobookshelf.app.data
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
@ -12,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.io.File
@CapacitorPlugin(name = "DbManager")
class DbManager : Plugin() {
@ -25,14 +27,28 @@ class DbManager : Plugin() {
Paper.book("device").write("data", deviceData)
}
fun loadLocalMediaItems():List<LocalMediaItem> {
fun loadLocalMediaItems():MutableList<LocalMediaItem> {
var localMediaItems:MutableList<LocalMediaItem> = mutableListOf()
Paper.book("localMediaItems").allKeys.forEach {
var localMediaItem:LocalMediaItem? = Paper.book("localMediaItems").read(it)
if (localMediaItem != null) {
// if (localMediaItem.coverContentUrl != null) {
// var file = DocumentFile.fromSingleUri(ctx)
// if (!file.exists()) {
// Log.e(tag, "Local media item cover url does not exist ${localMediaItem.coverContentUrl}")
// removeLocalMediaItem(localMediaItem.id)
// } else {
// localMediaItems.add(localMediaItem)
// }
// } else {
localMediaItems.add(localMediaItem)
// }
}
}
// localMediaItems = localMediaItems.filter {
//
// file.exists()
// }
return localMediaItems
}

View file

@ -29,7 +29,8 @@ data class LocalMediaItem(
var absolutePath:String,
var audioTracks:MutableList<AudioTrack>,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?
var coverContentUrl:String?,
var coverAbsolutePath:String?
) {
@JsonIgnore

View file

@ -2,9 +2,11 @@ package com.audiobookshelf.app.data
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import com.audiobookshelf.app.R
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaMetadata
// TODO: enum or something in kotlin?
@ -39,6 +41,37 @@ class PlaybackSession(
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
val currentTimeMs get() = (currentTime * 1000L).toLong()
@JsonIgnore
fun getCurrentTrackIndex():Int {
for (i in 0..(audioTracks.size - 1)) {
var track = audioTracks[i]
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs) > currentTimeMs) {
return i
}
}
return audioTracks.size - 1
}
@JsonIgnore
fun getCurrentTrackTimeMs():Long {
var currentTrack = audioTracks[this.getCurrentTrackIndex()]
var time = currentTime - currentTrack.startOffset
return (time * 1000L).toLong()
}
@JsonIgnore
fun getTrackStartOffsetMs(index:Int):Long {
var currentTrack = audioTracks[index]
return (currentTrack.startOffset * 1000L).toLong()
}
@JsonIgnore
fun getTotalDuration():Double {
var total = 0.0
audioTracks.forEach { total += it.duration }
return total
}
@JsonIgnore
fun getCoverUri(): Uri {
if (localMediaItem?.coverContentUrl != null) return Uri.parse(localMediaItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
@ -48,18 +81,11 @@ class PlaybackSession(
}
@JsonIgnore
fun getContentUri(): Uri {
var audioTrack = audioTracks[0]
fun getContentUri(audioTrack:AudioTrack): Uri {
if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url
return Uri.parse("$serverUrl${audioTrack.contentUrl}?token=$token")
}
@JsonIgnore
fun getMimeType():String {
var audioTrack = audioTracks[0]
return audioTrack.mimeType
}
@JsonIgnore
fun getMediaMetadataCompat(): MediaMetadataCompat {
var metadataBuilder = MediaMetadataCompat.Builder()
@ -74,7 +100,7 @@ class PlaybackSession(
}
@JsonIgnore
fun getExoMediaMetadata(): MediaMetadata {
fun getExoMediaMetadata(audioTrack:AudioTrack): MediaMetadata {
var metadataBuilder = MediaMetadata.Builder()
.setTitle(displayTitle)
.setDisplayTitle(displayTitle)
@ -82,9 +108,23 @@ class PlaybackSession(
.setAlbumArtist(displayAuthor)
.setSubtitle(displayAuthor)
var contentUri = this.getContentUri()
var contentUri = this.getContentUri(audioTrack)
metadataBuilder.setMediaUri(contentUri)
return metadataBuilder.build()
}
@JsonIgnore
fun getMediaItems():List<MediaItem> {
var mediaItems:MutableList<MediaItem> = mutableListOf()
for (audioTrack in audioTracks) {
var mediaMetadata = this.getExoMediaMetadata(audioTrack)
var mediaUri = this.getContentUri(audioTrack)
var mimeType = audioTrack.mimeType
var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
mediaItems.add(mediaItem)
}
return mediaItems
}
}

View file

@ -73,6 +73,7 @@ class FolderScanner(var ctx: Context) {
var index = 1
var startOffset = 0.0
var coverContentUrl:String? = null
var coverAbsolutePath:String? = null
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
@ -157,6 +158,7 @@ class FolderScanner(var ctx: Context) {
// First image file use as cover path
if (coverContentUrl == null) {
coverContentUrl = localFile.contentUrl
coverAbsolutePath = localFile.absolutePath
}
}
}
@ -173,7 +175,7 @@ class FolderScanner(var ctx: Context) {
else mediaItemsAdded++
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl)
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
mediaItems.add(localMediaItem)
}
}

View file

@ -94,14 +94,20 @@ class ApiHandler {
}
}
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem) -> Unit) {
getRequest("/api/items/$libraryItemId?expanded=1") {
val libraryItem = jacksonObjectMapper().readValue<LibraryItem>(it.toString())
cb(libraryItem)
}
}
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
val mapper = jacksonObjectMapper()
getRequest("/api/libraries/$libraryId/items") {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
var array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = mapper.readValue<LibraryItem>(array.get(i).toString())
val item = jacksonObjectMapper().readValue<LibraryItem>(array.get(i).toString())
items.add(item)
}
}

View file

@ -291,8 +291,8 @@ export default {
return this.restart()
}
// If 1 second or less into current chapter, then go to previous
if (this.currentTime - this.currentChapter.start <= 1) {
// If 4 seconds or less into current chapter, then go to previous
if (this.currentTime - this.currentChapter.start <= 4) {
var currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
if (currChapterIndex > 0) {
var prevChapter = this.chapters[currChapterIndex - 1]
@ -509,8 +509,8 @@ export default {
console.log('onMetadata', JSON.stringify(data))
this.isLoading = false
this.totalDuration = Number((data.duration / 1000).toFixed(2))
this.$emit('setTotalDuration', this.totalDuration)
// this.totalDuration = Number((data.duration / 1000).toFixed(2))
this.totalDuration = Number(data.duration.toFixed(2))
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
this.stateName = data.stateName

View file

@ -1,19 +1,7 @@
<template>
<div>
<div id="streamContainer">
<app-audio-player
ref="audioPlayer"
:playing.sync="isPlaying"
:bookmarks="bookmarks"
:sleep-timer-running="isSleepTimerRunning"
:sleep-time-remaining="sleepTimeRemaining"
@setTotalDuration="setTotalDuration"
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
@updateTime="(t) => (currentTime = t)"
@showSleepTimer="showSleepTimer"
@showBookmarks="showBookmarks"
@hook:mounted="audioPlayerMounted"
/>
<app-audio-player ref="audioPlayer" :playing.sync="isPlaying" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" @hook:mounted="audioPlayerMounted" />
</div>
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
@ -43,8 +31,7 @@ export default {
onSleepTimerEndedListener: null,
onSleepTimerSetListener: null,
sleepInterval: null,
currentEndOfChapterTime: 0,
totalDuration: 0
currentEndOfChapterTime: 0
}
},
watch: {
@ -121,9 +108,6 @@ export default {
console.log('Canceling sleep timer')
await MyNativeAudio.cancelSleepTimer()
},
setTotalDuration(duration) {
this.totalDuration = duration
},
streamClosed() {
console.log('Stream Closed')
},

View file

@ -0,0 +1,62 @@
<template>
<modals-modal v-model="show" :width="300" height="100%">
<template #outer>
<div class="absolute top-7 left-4 z-40" style="max-width: 80%">
<p class="text-white text-lg truncate">Select Local Folder</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="folder in localFolders">
<li :key="folder.id" :id="`folder-${folder.id}`" class="text-gray-50 select-none relative py-4" role="option" @click="clickedOption(folder)">
<div class="relative flex items-center pl-3" style="padding-right: 4.5rem">
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ folder.name }}</p>
</div>
</li>
</template>
</ul>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
mediaType: String
},
data() {
return {
localFolders: []
}
},
watch: {
value(newVal) {
this.$nextTick(this.init)
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
clickedOption(folder) {
this.$emit('select', folder)
},
async init() {
var localFolders = (await this.$db.loadFolders()) || []
this.localFolders = localFolders.filter((lf) => lf.mediaType == this.mediaType)
}
},
mounted() {}
}
</script>

View file

@ -253,17 +253,26 @@ export default {
await this.$store.dispatch('downloads/linkOrphanDownloads')
}
},
onItemDownloadUpdate(data) {
console.log('ON ITEM DOWNLOAD UPDATE', JSON.stringify(data))
},
onItemDownloadComplete(data) {
console.log('ON ITEM DOWNLOAD COMPLETE', JSON.stringify(data))
},
async initMediaStore() {
// Request and setup listeners for media files on native
AudioDownloader.addListener('onDownloadComplete', (data) => {
this.onDownloadComplete(data)
AudioDownloader.addListener('onItemDownloadUpdate', (data) => {
this.onItemDownloadUpdate(data)
})
AudioDownloader.addListener('onDownloadFailed', (data) => {
this.onDownloadFailed(data)
})
AudioDownloader.addListener('onDownloadProgress', (data) => {
this.onDownloadProgress(data)
AudioDownloader.addListener('onItemDownloadComplete', (data) => {
this.onItemDownloadComplete(data)
})
// AudioDownloader.addListener('onDownloadFailed', (data) => {
// this.onDownloadFailed(data)
// })
// AudioDownloader.addListener('onDownloadProgress', (data) => {
// this.onDownloadProgress(data)
// })
var downloads = await this.$store.dispatch('downloads/loadFromStorage')
var downloadFolder = await this.$localStore.getDownloadFolder()

View file

@ -45,6 +45,8 @@
<div class="w-full py-4">
<p>{{ description }}</p>
</div>
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
</div>
</template>
@ -81,7 +83,8 @@ export default {
},
data() {
return {
resettingProgress: false
resettingProgress: false,
showSelectLocalFolder: false
}
},
computed: {
@ -97,6 +100,9 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
mediaType() {
return this.libraryItem.mediaType
},
media() {
return this.libraryItem.media || {}
},
@ -235,17 +241,6 @@ export default {
this.resettingProgress = false
})
}
// if (value) {
// this.resettingProgress = true
// this.$store.dispatch('user/updateUserAudiobookData', {
// libraryItemId: this.libraryItemId,
// currentTime: 0,
// totalDuration: this.duration,
// progress: 0,
// lastUpdate: Date.now(),
// isRead: false
// })
// }
},
itemUpdated(libraryItem) {
if (libraryItem.id === this.libraryItemId) {
@ -253,14 +248,74 @@ export default {
this.libraryItem = libraryItem
}
},
async selectFolder() {
// Select and save the local folder for media type
var folderObj = await StorageManager.selectFolder({ mediaType: this.mediaType })
if (folderObj.error) {
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
}
return folderObj
},
selectedLocalFolder(localFolder) {
this.showSelectLocalFolder = false
this.download(localFolder)
},
downloadClick() {
this.download()
},
async download(selectedLocalFolder = null) {
console.log('downloadClick ' + this.$server.connected + ' | ' + !!this.downloadObj)
if (!this.$server.connected) return
if (this.downloadObj) {
console.log('Already downloaded', this.downloadObj)
if (!this.numTracks || this.downloadObj) {
return
}
// Get the local folder to download to
var localFolder = selectedLocalFolder
if (!localFolder) {
var localFolders = (await this.$db.loadFolders()) || []
console.log('Local folders loaded', localFolders.length)
var foldersWithMediaType = localFolders.filter((lf) => {
console.log('Checking local folder', lf.mediaType)
return lf.mediaType == this.mediaType
})
console.log('Folders with media type', this.mediaType, foldersWithMediaType.length)
if (!foldersWithMediaType.length) {
// No local folders or no local folders with this media type
localFolder = await this.selectFolder()
} else if (foldersWithMediaType.length == 1) {
console.log('Only 1 local folder with this media type - auto select it')
localFolder = foldersWithMediaType[0]
} else {
this.prepareDownload()
console.log('Multiple folders with media type')
this.showSelectLocalFolder = true
return
}
if (!localFolder) {
return this.$toast.error('Invalid download folder')
}
}
console.log('Local folder', JSON.stringify(localFolder))
var startDownloadMessage = `Start download for "${this.title}" with ${this.numTracks} audio track${this.numTracks == 1 ? '' : 's'} to folder ${localFolder.name}?`
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
})
if (value) {
this.startDownload(localFolder)
}
},
async startDownload(localFolder) {
console.log('Starting download to local folder', localFolder.name)
var downloadRes = await AudioDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id })
if (downloadRes.error) {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download error', errorMsg)
this.$toast.update(download.toastId, { content: `Error: ${errorMsg}`, options: { timeout: 5000, type: 'error' } })
this.$store.commit('downloads/removeDownload', download)
}
},
async changeDownloadFolderClick() {
@ -286,103 +341,103 @@ export default {
await this.$localStore.setDownloadFolder(folderObj)
}
},
async prepareDownload() {
var audiobook = this.libraryItem
if (!audiobook) {
return
}
// async prepareDownload() {
// var audiobook = this.libraryItem
// if (!audiobook) {
// return
// }
// Download Path
var dlFolder = this.$localStore.downloadFolder
console.log('Prepare download: ' + this.hasStoragePermission + ' | ' + dlFolder)
// // Download Path
// var dlFolder = this.$localStore.downloadFolder
// console.log('Prepare download: ' + this.hasStoragePermission + ' | ' + dlFolder)
if (!this.hasStoragePermission || !dlFolder) {
console.log('No download folder, request from user')
// User to select download folder from download modal to ensure permissions
// this.$store.commit('downloads/setShowModal', true)
this.changeDownloadFolderClick()
return
} else {
console.log('Has Download folder: ' + JSON.stringify(dlFolder))
}
// if (!this.hasStoragePermission || !dlFolder) {
// console.log('No download folder, request from user')
// // User to select download folder from download modal to ensure permissions
// // this.$store.commit('downloads/setShowModal', true)
// this.changeDownloadFolderClick()
// return
// } else {
// console.log('Has Download folder: ' + JSON.stringify(dlFolder))
// }
var downloadObject = {
id: this.libraryItemId,
downloadFolderUrl: dlFolder.uri,
audiobook: {
...audiobook
},
isPreparing: true,
isDownloading: false,
toastId: this.$toast(`Preparing download for "${this.title}"`, { timeout: false })
}
if (audiobook.tracks.length === 1) {
// Single track should not need preparation
console.log('Single track, start download no prep needed')
var track = audiobook.tracks[0]
var fileext = track.ext
// var downloadObject = {
// id: this.libraryItemId,
// downloadFolderUrl: dlFolder.uri,
// audiobook: {
// ...audiobook
// },
// isPreparing: true,
// isDownloading: false,
// toastId: this.$toast(`Preparing download for "${this.title}"`, { timeout: false })
// }
// if (audiobook.tracks.length === 1) {
// // Single track should not need preparation
// console.log('Single track, start download no prep needed')
// var track = audiobook.tracks[0]
// var fileext = track.ext
console.log('Download Single Track Path: ' + track.path)
// console.log('Download Single Track Path: ' + track.path)
var relTrackPath = track.path.replace('\\', '/').replace(this.libraryItem.path.replace('\\', '/'), '')
// var relTrackPath = track.path.replace('\\', '/').replace(this.libraryItem.path.replace('\\', '/'), '')
var url = `${this.$store.state.serverUrl}/s/book/${this.libraryItemId}${relTrackPath}?token=${this.userToken}`
this.startDownload(url, fileext, downloadObject)
} else {
// Multi-track merge
this.$store.commit('downloads/addUpdateDownload', downloadObject)
// var url = `${this.$store.state.serverUrl}/s/book/${this.libraryItemId}${relTrackPath}?token=${this.userToken}`
// this.startDownload(url, fileext, downloadObject)
// } else {
// // Multi-track merge
// this.$store.commit('downloads/addUpdateDownload', downloadObject)
var prepareDownloadPayload = {
audiobookId: this.libraryItemId,
audioFileType: 'same',
type: 'singleAudio'
}
this.$server.socket.emit('download', prepareDownloadPayload)
}
},
getCoverUrlForDownload() {
if (!this.book || !this.book.cover) return null
// var prepareDownloadPayload = {
// audiobookId: this.libraryItemId,
// audioFileType: 'same',
// type: 'singleAudio'
// }
// this.$server.socket.emit('download', prepareDownloadPayload)
// }
// },
// getCoverUrlForDownload() {
// if (!this.book || !this.book.cover) return null
var cover = this.book.cover
if (cover.startsWith('http')) return cover
var coverSrc = this.$store.getters['global/getLibraryItemCoverSrc'](this.libraryItem)
return coverSrc
},
async startDownload(url, fileext, download) {
this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
// var cover = this.book.cover
// if (cover.startsWith('http')) return cover
// var coverSrc = this.$store.getters['global/getLibraryItemCoverSrc'](this.libraryItem)
// return coverSrc
// },
// async startDownload(url, fileext, download) {
// this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
var coverDownloadUrl = this.getCoverUrlForDownload()
var coverFilename = null
if (coverDownloadUrl) {
var coverNoQueryString = coverDownloadUrl.split('?')[0]
// var coverDownloadUrl = this.getCoverUrlForDownload()
// var coverFilename = null
// if (coverDownloadUrl) {
// var coverNoQueryString = coverDownloadUrl.split('?')[0]
var coverExt = Path.extname(coverNoQueryString) || '.jpg'
coverFilename = `cover-${download.id}${coverExt}`
}
// var coverExt = Path.extname(coverNoQueryString) || '.jpg'
// coverFilename = `cover-${download.id}${coverExt}`
// }
download.isDownloading = true
download.isPreparing = false
download.filename = `${download.audiobook.book.title}${fileext}`
this.$store.commit('downloads/addUpdateDownload', download)
// download.isDownloading = true
// download.isPreparing = false
// download.filename = `${download.audiobook.book.title}${fileext}`
// this.$store.commit('downloads/addUpdateDownload', download)
console.log('Starting Download URL', url)
var downloadRequestPayload = {
audiobookId: download.id,
filename: download.filename,
coverFilename,
coverDownloadUrl,
downloadUrl: url,
title: download.audiobook.book.title,
downloadFolderUrl: download.downloadFolderUrl
}
var downloadRes = await AudioDownloader.download(downloadRequestPayload)
if (downloadRes.error) {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download error', errorMsg)
this.$toast.update(download.toastId, { content: `Error: ${errorMsg}`, options: { timeout: 5000, type: 'error' } })
this.$store.commit('downloads/removeDownload', download)
}
},
// console.log('Starting Download URL', url)
// var downloadRequestPayload = {
// audiobookId: download.id,
// filename: download.filename,
// coverFilename,
// coverDownloadUrl,
// downloadUrl: url,
// title: download.audiobook.book.title,
// downloadFolderUrl: download.downloadFolderUrl
// }
// var downloadRes = await AudioDownloader.download(downloadRequestPayload)
// if (downloadRes.error) {
// var errorMsg = downloadRes.error || 'Unknown error'
// console.error('Download error', errorMsg)
// this.$toast.update(download.toastId, { content: `Error: ${errorMsg}`, options: { timeout: 5000, type: 'error' } })
// this.$store.commit('downloads/removeDownload', download)
// }
// },
downloadReady(prepareDownload) {
var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId)
if (download) {