mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-06 03:54:50 +02:00
Support for android 8+, selectable download location, permissions check, recursive delete
This commit is contained in:
parent
b4ffe0fc83
commit
e124d3858f
23 changed files with 798 additions and 387 deletions
|
@ -36,7 +36,7 @@ class Server extends EventEmitter {
|
||||||
this.user = user
|
this.user = user
|
||||||
this.store.commit('user/setUser', user)
|
this.store.commit('user/setUser', user)
|
||||||
if (user) {
|
if (user) {
|
||||||
this.store.commit('user/setSettings', user.settings)
|
// this.store.commit('user/setSettings', user.settings)
|
||||||
Storage.set({ key: 'token', value: user.token })
|
Storage.set({ key: 'token', value: user.token })
|
||||||
} else {
|
} else {
|
||||||
Storage.remove({ key: 'token' })
|
Storage.remove({ key: 'token' })
|
||||||
|
|
|
@ -5,13 +5,16 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = ['-Xjvm-default=all']
|
||||||
|
}
|
||||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 13
|
versionCode 15
|
||||||
versionName "0.8.4-beta"
|
versionName "0.9.0-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
@ -35,6 +38,7 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation "com.anggrayudi:storage:0.13.0"
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />-->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -14,7 +17,8 @@
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
|
import com.anggrayudi.storage.callback.FileCallback
|
||||||
|
import com.anggrayudi.storage.callback.FolderPickerCallback
|
||||||
|
import com.anggrayudi.storage.callback.StorageAccessCallback
|
||||||
|
import com.anggrayudi.storage.file.*
|
||||||
|
import com.anggrayudi.storage.media.FileDescription
|
||||||
|
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
import com.getcapacitor.PluginMethod
|
import com.getcapacitor.PluginMethod
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AudioDownloader")
|
@CapacitorPlugin(name = "AudioDownloader")
|
||||||
|
@ -22,281 +36,415 @@ class AudioDownloader : Plugin() {
|
||||||
lateinit var mainActivity:MainActivity
|
lateinit var mainActivity:MainActivity
|
||||||
lateinit var downloadManager:DownloadManager
|
lateinit var downloadManager:DownloadManager
|
||||||
|
|
||||||
data class AudiobookDownload(val url: String, val filename: String, val downloadId: Long)
|
data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
|
||||||
var downloads:MutableList<AudiobookDownload> = mutableListOf()
|
|
||||||
|
|
||||||
data class CoverItem(val name: String, val coverUrl: String)
|
|
||||||
data class AudiobookItem(val id: Long, val uri: Uri, val name: String, val size: Int, val duration: Int, val coverUrl: String) {
|
|
||||||
fun toJSObject() : JSObject {
|
fun toJSObject() : JSObject {
|
||||||
var obj = JSObject()
|
var obj = JSObject()
|
||||||
obj.put("id", this.id)
|
|
||||||
obj.put("uri", this.uri)
|
obj.put("uri", this.uri)
|
||||||
obj.put("name", this.name)
|
obj.put("name", this.name)
|
||||||
obj.put("size", this.size)
|
obj.put("size", this.size)
|
||||||
obj.put("duration", this.duration)
|
|
||||||
obj.put("coverUrl", this.coverUrl)
|
obj.put("coverUrl", this.coverUrl)
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
|
||||||
|
|
||||||
override fun load() {
|
override fun load() {
|
||||||
mainActivity = (activity as MainActivity)
|
mainActivity = (activity as MainActivity)
|
||||||
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
// storage = SimpleStorage(mainActivity)
|
||||||
|
|
||||||
var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
||||||
Log.d(tag, "RECEIVE EVT $evt $id")
|
if (evt == "complete") {}
|
||||||
if (evt == "complete") {
|
|
||||||
var path = downloadManager.getUriForDownloadedFile(id)
|
|
||||||
|
|
||||||
var download = downloads.find { it.downloadId == id }
|
|
||||||
var filename = download?.filename
|
|
||||||
|
|
||||||
var jsobj = JSObject()
|
|
||||||
jsobj.put("downloadId", id)
|
|
||||||
jsobj.put("contentUrl", path)
|
|
||||||
jsobj.put("filename", filename)
|
|
||||||
notifyListeners("onDownloadComplete", jsobj)
|
|
||||||
downloads = downloads.filter { it.downloadId != id } as MutableList<AudiobookDownload>
|
|
||||||
}
|
|
||||||
if (evt == "clicked") {
|
if (evt == "clicked") {
|
||||||
Log.d(tag, "Clicked $id back in the audiodownloader")
|
Log.d(tag, "Clicked $id back in the audiodownloader")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainActivity.registerBroadcastReceiver(recieverEvent)
|
mainActivity.registerBroadcastReceiver(recieverEvent)
|
||||||
|
|
||||||
|
|
||||||
|
setupSimpleStorage()
|
||||||
|
|
||||||
|
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
||||||
|
// Android 9 OR Below Request Permissions
|
||||||
|
// if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||||
|
// Log.d(tag, "Requires Permission")
|
||||||
|
//// storage.requestStorageAccess(9)
|
||||||
|
// var jsobj = JSObject()
|
||||||
|
// jsobj.put("value", "required")
|
||||||
|
// notifyListeners("permission", jsobj)
|
||||||
|
// } else {
|
||||||
|
// Log.d(tag, "Does not request permission")
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAudiobooks() {
|
private fun setupSimpleStorage() {
|
||||||
var covers = loadCovers()
|
mainActivity.storageHelper.onFolderSelected = { requestCode, folder ->
|
||||||
|
Log.d(tag, "FOLDER SELECTED $requestCode ${folder.name} ${folder.uri}")
|
||||||
val projection = arrayOf(
|
var jsobj = JSObject()
|
||||||
MediaStore.Audio.Media._ID,
|
jsobj.put("value", "granted")
|
||||||
MediaStore.Audio.Media.DISPLAY_NAME,
|
jsobj.put("uri", folder.uri)
|
||||||
MediaStore.Audio.Media.DURATION,
|
jsobj.put("absolutePath", folder.getAbsolutePath(context))
|
||||||
MediaStore.Audio.Media.SIZE,
|
jsobj.put("storageId", folder.getStorageId(context))
|
||||||
MediaStore.Audio.Media.IS_AUDIOBOOK,
|
jsobj.put("storageType", folder.getStorageType(context))
|
||||||
MediaStore.Audio.Media.RELATIVE_PATH
|
jsobj.put("simplePath", folder.getSimplePath(context))
|
||||||
)
|
jsobj.put("basePath", folder.getBasePath(context))
|
||||||
|
notifyListeners("permission", jsobj)
|
||||||
var _audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
|
||||||
val selection = "${MediaStore.Audio.Media.IS_AUDIOBOOK} == ?"
|
|
||||||
val selectionArgs = arrayOf("1")
|
|
||||||
val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
|
|
||||||
|
|
||||||
activity.applicationContext.contentResolver.query(
|
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
projection,
|
|
||||||
selection,
|
|
||||||
selectionArgs,
|
|
||||||
sortOrder
|
|
||||||
)?.use { cursor ->
|
|
||||||
|
|
||||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
|
||||||
val nameColumn =
|
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
|
||||||
val durationColumn =
|
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
|
||||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
|
|
||||||
val isAudiobookColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_AUDIOBOOK)
|
|
||||||
var relativePathColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.RELATIVE_PATH)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val id = cursor.getLong(idColumn)
|
|
||||||
val name = cursor.getString(nameColumn)
|
|
||||||
val duration = cursor.getInt(durationColumn)
|
|
||||||
val size = cursor.getInt(sizeColumn)
|
|
||||||
var isAudiobook = cursor.getInt(isAudiobookColumn)
|
|
||||||
var relativePath = cursor.getString(relativePathColumn)
|
|
||||||
|
|
||||||
if (isAudiobook == 1) {
|
|
||||||
val contentUri: Uri = ContentUris.withAppendedId(
|
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.d(tag, "Got Content FRom MEdia STORE $id $contentUri, Name: $name, Dur: $duration, Size: $size, relativePath: $relativePath")
|
|
||||||
var audiobookId = File(name).nameWithoutExtension
|
|
||||||
var coverItem:CoverItem? = covers.find{it.name == audiobookId}
|
|
||||||
var coverUrl = coverItem?.coverUrl ?: ""
|
|
||||||
|
|
||||||
_audiobookItems.add(AudiobookItem(id, contentUri, name, duration, size, coverUrl))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback {
|
||||||
|
override fun onRootPathNotSelected(
|
||||||
|
requestCode: Int,
|
||||||
|
rootPath: String,
|
||||||
|
uri: Uri,
|
||||||
|
selectedStorageType: StorageType,
|
||||||
|
expectedStorageType: StorageType
|
||||||
|
) {
|
||||||
|
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||||
}
|
}
|
||||||
audiobookItems = _audiobookItems
|
|
||||||
|
|
||||||
var audiobookObjs:List<JSObject> = _audiobookItems.map{ it.toJSObject() }
|
override fun onCanceledByUser(requestCode: Int) {
|
||||||
|
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
var mediaItemNoticePayload = JSObject()
|
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) {
|
||||||
mediaItemNoticePayload.put("items", audiobookObjs)
|
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||||
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
}
|
||||||
|
|
||||||
|
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||||
|
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
|
||||||
|
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadCovers() : MutableList<CoverItem> {
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
val projection = arrayOf(
|
|
||||||
MediaStore.Images.Media._ID,
|
|
||||||
MediaStore.Images.Media.DISPLAY_NAME
|
|
||||||
)
|
|
||||||
val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC"
|
|
||||||
|
|
||||||
var coverItems:MutableList<CoverItem> = mutableListOf()
|
|
||||||
|
|
||||||
activity.applicationContext.contentResolver.query(
|
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
projection,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
sortOrder
|
|
||||||
)?.use { cursor ->
|
|
||||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
|
|
||||||
val nameColumn =
|
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val id = cursor.getLong(idColumn)
|
|
||||||
val filename = cursor.getString(nameColumn)
|
|
||||||
val contentUri: Uri = ContentUris.withAppendedId(
|
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
|
|
||||||
var name = File(filename).nameWithoutExtension
|
|
||||||
Log.d(tag, "Got IMAGE FRom Media STORE $id $contentUri, Name: $name")
|
|
||||||
|
|
||||||
var coverItem = CoverItem(name, contentUri.toString())
|
|
||||||
coverItems.add(coverItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return coverItems
|
|
||||||
}
|
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun load(call: PluginCall) {
|
fun requestStoragePermission(call: PluginCall) {
|
||||||
loadAudiobooks()
|
Log.d(tag, "Request Storage Permissions")
|
||||||
|
mainActivity.storageHelper.requestStorageAccess()
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun downloadCover(call: PluginCall) {
|
fun checkStoragePermission(call: PluginCall) {
|
||||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
var res = false
|
||||||
var url = call.data.getString("downloadUrl", "unknown").toString()
|
|
||||||
var title = call.data.getString("title", "Cover").toString()
|
|
||||||
var filename = call.data.getString("filename", "audiobook.jpg").toString()
|
|
||||||
|
|
||||||
Log.d(tag, "Called download cover: $url")
|
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||||
|
res = SimpleStorage.hasStoragePermission(context)
|
||||||
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
Log.d(tag, "Check Storage Access $res")
|
||||||
dlRequest.setTitle("Cover Art: $title")
|
|
||||||
dlRequest.setDescription("Cover art for audiobook")
|
|
||||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
|
||||||
|
|
||||||
var file:File = File(audiobookId, filename)
|
|
||||||
Log.d(tag, "FILE ${file.path} | ${file.canonicalPath}")
|
|
||||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, file.path)
|
|
||||||
var downloadId = downloadManager.enqueue(dlRequest)
|
|
||||||
|
|
||||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
|
||||||
var jsobj = JSObject()
|
|
||||||
if (success) {
|
|
||||||
var path = downloadManager.getUriForDownloadedFile(downloadId)
|
|
||||||
jsobj.put("url", path)
|
|
||||||
call.resolve(jsobj)
|
|
||||||
} else {
|
} else {
|
||||||
jsobj.put("failed", true)
|
Log.d(tag, "Has permission on Android 10 or up")
|
||||||
|
res = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsobj = JSObject()
|
||||||
|
jsobj.put("value", res)
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkUriExists(uri: Uri?): Boolean {
|
||||||
|
if (uri == null) return false
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
//1. Check Uri
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
val isUriExist: Boolean = try {
|
||||||
|
cursor = resolver.query(uri, null, null, null, null)
|
||||||
|
//cursor null: content Uri was invalid or some other error occurred
|
||||||
|
//cursor.moveToFirst() false: Uri was ok but no entry found.
|
||||||
|
(cursor != null && cursor.moveToFirst())
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
false
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
cursor?.close()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
return isUriExist
|
||||||
|
}
|
||||||
|
|
||||||
|
@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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
if (audiobookFile == null) {
|
||||||
progressUpdater.run()
|
Log.e(tag, "Audiobook was not found $audiobookUrl")
|
||||||
|
} else {
|
||||||
|
var _name = audiobookFile.name
|
||||||
|
if (_name == null) _name = ""
|
||||||
|
var _coverUrl = ""
|
||||||
|
if (coverFile != null) _coverUrl = coverFile.uri.toString()
|
||||||
|
|
||||||
|
var size = audiobookFile.length()
|
||||||
|
var abItem = AudiobookItem(audiobookFile.uri, _name, size, _coverUrl)
|
||||||
|
audiobookItems.add(abItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var audiobookObjs:List<JSObject> = audiobookItems.map{ it.toJSObject() }
|
||||||
|
var mediaItemNoticePayload = JSObject()
|
||||||
|
mediaItemNoticePayload.put("items", audiobookObjs)
|
||||||
|
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun download(call: PluginCall) {
|
fun download(call: PluginCall) {
|
||||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
||||||
var url = call.data.getString("downloadUrl", "unknown").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 title = call.data.getString("title", "Audiobook").toString()
|
||||||
var filename = call.data.getString("filename", "audiobook.mp3").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")
|
||||||
|
|
||||||
Log.d(tag, "Called download: $url")
|
var dlfilename = audiobookId + "." + File(filename).extension
|
||||||
|
var coverdlfilename = audiobookId + "." + File(coverFilename).extension
|
||||||
|
|
||||||
|
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))
|
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
||||||
dlRequest.setTitle(title)
|
dlRequest.setTitle(title)
|
||||||
dlRequest.setDescription("Downloading to Audiobooks directory")
|
dlRequest.setDescription("Downloading to ${folder.name}")
|
||||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, dlfilename)
|
||||||
|
|
||||||
var file:File = File(audiobookId, filename)
|
var audiobookDownloadId = downloadManager.enqueue(dlRequest)
|
||||||
Log.d(tag, "FILE ${file.path} | ${file.canonicalPath}")
|
var coverDownloadId:Long? = null
|
||||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, file.path)
|
|
||||||
|
|
||||||
var downloadId = downloadManager.enqueue(dlRequest)
|
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 download = AudiobookDownload(url, filename, downloadId)
|
var progressReceiver : (id:Long, prog: Long) -> Unit = { id:Long, prog: Long ->
|
||||||
downloads.add(download)
|
if (id == audiobookDownloadId) {
|
||||||
|
|
||||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
|
||||||
var jsobj = JSObject()
|
var jsobj = JSObject()
|
||||||
jsobj.put("filename", filename)
|
jsobj.put("audiobookId", audiobookId)
|
||||||
jsobj.put("downloadId", downloadId)
|
|
||||||
jsobj.put("progress", prog)
|
jsobj.put("progress", prog)
|
||||||
notifyListeners("onDownloadProgress", jsobj)
|
notifyListeners("onDownloadProgress", jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
|
||||||
Log.d(tag, "RECIEVER DONE, SUCCES? $success")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
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
|
||||||
|
if (id == coverDownloadId) {
|
||||||
|
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, coverdlfilename)
|
||||||
|
|
||||||
|
Log.d(tag, "Move Cover File ${docfile?.name}")
|
||||||
|
} else if (id == audiobookDownloadId) {
|
||||||
|
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, dlfilename)
|
||||||
|
Log.d(tag, "Move Audiobook File ${docfile?.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
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} | $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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val executorService: ExecutorService = Executors.newFixedThreadPool(4)
|
||||||
|
executorService.execute {
|
||||||
|
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()
|
progressUpdater.run()
|
||||||
|
if (coverDownloadId != null) {
|
||||||
|
var coverProgressUpdater = DownloadProgressUpdater(downloadManager, coverDownloadId, progressReceiver, doneReceiver)
|
||||||
|
coverProgressUpdater.run()
|
||||||
|
}
|
||||||
|
|
||||||
val ret = JSObject()
|
val ret = JSObject()
|
||||||
ret.put("value", downloadId)
|
ret.put("audiobookDownloadId", audiobookDownloadId)
|
||||||
|
ret.put("coverDownloadId", coverDownloadId)
|
||||||
call.resolve(ret)
|
call.resolve(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun selectFolder(call: PluginCall) {
|
||||||
|
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
|
||||||
|
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
|
||||||
|
Log.d(tag, "ONF OLDER SELECRTED ${folder.uri} ${folder.name}")
|
||||||
|
|
||||||
|
var absolutePath = folder.getAbsolutePath(activity)
|
||||||
|
var storageId = folder.getStorageId(activity)
|
||||||
|
var storageType = folder.getStorageType(activity)
|
||||||
|
var simplePath = folder.getSimplePath(activity)
|
||||||
|
var basePath = folder.getBasePath(activity)
|
||||||
|
|
||||||
|
var jsobj = JSObject()
|
||||||
|
jsobj.put("uri", folder.uri)
|
||||||
|
jsobj.put("absolutePath", absolutePath)
|
||||||
|
jsobj.put("storageId", storageId)
|
||||||
|
jsobj.put("storageType", storageType)
|
||||||
|
jsobj.put("simplePath", simplePath)
|
||||||
|
jsobj.put("basePath", basePath)
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
|
||||||
|
Log.e(tag, "STORAGE ACCESS DENIED")
|
||||||
|
var jsobj = JSObject()
|
||||||
|
jsobj.put("error", "Access Denied")
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||||
|
Log.d(tag, "STORAGE PERMISSION DENIED $requestCode")
|
||||||
|
var jsobj = JSObject()
|
||||||
|
jsobj.put("error", "Permission Denied")
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainActivity.storage.openFolderPicker(6)
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun delete(call: PluginCall) {
|
fun delete(call: PluginCall) {
|
||||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
|
||||||
var filename = call.data.getString("filename", "audiobook.mp3").toString()
|
|
||||||
var url = call.data.getString("url", "").toString()
|
var url = call.data.getString("url", "").toString()
|
||||||
var coverUrl = call.data.getString("coverUrl", "").toString()
|
var coverUrl = call.data.getString("coverUrl", "").toString()
|
||||||
|
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||||
|
|
||||||
// Does Not Work
|
if (folderUrl != "") {
|
||||||
// var audiobookDirRoot = activity.applicationContext.getExternalFilesDir(Environment.DIRECTORY_AUDIOBOOKS)
|
Log.d(tag, "CALLED DELETE FIOLDER: $folderUrl")
|
||||||
// Log.d(tag, "AUDIOBOOK DIR ROOT $audiobookDirRoot")
|
var folder = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
||||||
// var result = audiobookDirRoot?.deleteRecursively()
|
var success = folder?.deleteRecursively(context)
|
||||||
// Log.d(tag, "DONE DELETING FOLDER $result")
|
var jsobj = JSObject()
|
||||||
|
jsobj.put("success", success)
|
||||||
// Does Not Work
|
|
||||||
// var audiobookDir = File(audiobookDirRoot, audiobookId + "/")
|
|
||||||
// Log.d(tag, "Delete Audiobook DIR ${audiobookDir.path} is dir ${audiobookDir.isDirectory}")
|
|
||||||
// var result = audiobookDir.deleteRecursively()
|
|
||||||
//
|
|
||||||
|
|
||||||
// Does Not Work
|
|
||||||
// var audiobookDir = activity.applicationContext.getExternalFilesDir(Environment.DIRECTORY_AUDIOBOOKS)
|
|
||||||
// Log.d(tag, "AUDIOBOOK DIR ${audiobookDir?.path}")
|
|
||||||
// var dir = File(audiobookDir, "$audiobookId/")
|
|
||||||
// Log.d(tag, "DIR DIR ${dir.path}")
|
|
||||||
// var res = dir.delete()
|
|
||||||
// Log.d(tag, "DELETED $res")
|
|
||||||
|
|
||||||
var contentResolver = activity.applicationContext.contentResolver
|
|
||||||
contentResolver.delete(Uri.parse(url), null, null)
|
|
||||||
|
|
||||||
if (coverUrl != "") {
|
|
||||||
contentResolver.delete(Uri.parse(coverUrl), null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
call.resolve()
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
// Older audiobooks did not store a folder url, use cover and audiobook url
|
||||||
|
var abExists = checkUriExists(Uri.parse(url))
|
||||||
|
if (abExists) {
|
||||||
|
var abfile = DocumentFileCompat.fromUri(context, Uri.parse(url))
|
||||||
|
abfile?.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long) -> Unit, private var doneReceiver: (Boolean) -> Unit) : Thread() {
|
var coverExists = checkUriExists(Uri.parse(coverUrl))
|
||||||
|
if (coverExists) {
|
||||||
|
var coverfile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
|
||||||
|
coverfile?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 val query: DownloadManager.Query = DownloadManager.Query()
|
||||||
private var totalBytes: Int = 0
|
private var totalBytes: Int = 0
|
||||||
private var TAG = "DownloadProgressUpdater"
|
private var TAG = "DownloadProgressUpdater"
|
||||||
|
@ -308,34 +456,49 @@ class AudioDownloader : Plugin() {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
Log.d(TAG, "RUN FOR ID $downloadId")
|
Log.d(TAG, "RUN FOR ID $downloadId")
|
||||||
var keepRunning = true
|
var keepRunning = true
|
||||||
|
var increment = 0
|
||||||
while (keepRunning) {
|
while (keepRunning) {
|
||||||
Thread.sleep(500)
|
Thread.sleep(500)
|
||||||
|
increment++
|
||||||
|
|
||||||
|
if (increment % 4 == 0) {
|
||||||
|
Log.d(TAG, "Loop $increment : $downloadId")
|
||||||
|
}
|
||||||
|
|
||||||
manager.query(query).use {
|
manager.query(query).use {
|
||||||
if (it.moveToFirst()) {
|
if (it.moveToFirst()) {
|
||||||
|
|
||||||
//get total bytes of the file
|
//get total bytes of the file
|
||||||
if (totalBytes <= 0) {
|
if (totalBytes <= 0) {
|
||||||
totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
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 downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
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 || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
doneReceiver(true)
|
doneReceiver(downloadId, true)
|
||||||
} else {
|
} else {
|
||||||
doneReceiver(false)
|
doneReceiver(downloadId, false)
|
||||||
}
|
}
|
||||||
keepRunning = false
|
keepRunning = false
|
||||||
this.interrupt()
|
this.interrupt()
|
||||||
} else {
|
} else {
|
||||||
//update progress
|
//update progress
|
||||||
val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
|
val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
|
||||||
receiver(percentProgress)
|
receiver(downloadId, percentProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "NOT FOUND IN QUERY")
|
Log.e(TAG, "NOT FOUND IN QUERY")
|
||||||
keepRunning = false
|
keepRunning = false
|
||||||
|
@ -343,6 +506,5 @@ class AudioDownloader : Plugin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,10 @@ package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.os.Bundle
|
import android.os.*
|
||||||
import android.os.Handler
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,23 +19,19 @@ class MainActivity : BridgeActivity() {
|
||||||
lateinit var pluginCallback : () -> Unit
|
lateinit var pluginCallback : () -> Unit
|
||||||
lateinit var downloaderCallback : (String, Long) -> Unit
|
lateinit var downloaderCallback : (String, Long) -> Unit
|
||||||
|
|
||||||
|
val storageHelper = SimpleStorageHelper(this)
|
||||||
|
val storage = SimpleStorage(this)
|
||||||
|
|
||||||
val broadcastReceiver = object: BroadcastReceiver() {
|
val broadcastReceiver = object: BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||||
|
|
||||||
downloaderCallback("complete", thisdlid)
|
downloaderCallback("complete", thisdlid)
|
||||||
|
|
||||||
Log.d(tag, "DOWNNLAOD COMPELTE $thisdlid")
|
|
||||||
Toast.makeText(this@MainActivity, "Download Completed $thisdlid", Toast.LENGTH_SHORT)
|
|
||||||
}
|
}
|
||||||
DownloadManager.ACTION_NOTIFICATION_CLICKED -> {
|
DownloadManager.ACTION_NOTIFICATION_CLICKED -> {
|
||||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||||
downloaderCallback("clicked", thisdlid)
|
downloaderCallback("clicked", thisdlid)
|
||||||
|
|
||||||
Log.d(tag, "CLICKED NOTFIFICAIONT $thisdlid")
|
|
||||||
Toast.makeText(this@MainActivity, "Download CLICKED $thisdlid", Toast.LENGTH_SHORT)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +39,7 @@ class MainActivity : BridgeActivity() {
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
registerPlugin(MyNativeAudio::class.java)
|
registerPlugin(MyNativeAudio::class.java)
|
||||||
registerPlugin(AudioDownloader::class.java)
|
registerPlugin(AudioDownloader::class.java)
|
||||||
|
@ -57,7 +52,7 @@ class MainActivity : BridgeActivity() {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
// unregisterReceiver(broadcastReceiver)
|
unregisterReceiver(broadcastReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -101,4 +96,29 @@ class MainActivity : BridgeActivity() {
|
||||||
fun registerBroadcastReceiver(cb: (String, Long) -> Unit) {
|
fun registerBroadcastReceiver(cb: (String, Long) -> Unit) {
|
||||||
downloaderCallback = cb
|
downloaderCallback = cb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
storageHelper.onSaveInstanceState(outState)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
storageHelper.onRestoreInstanceState(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||||
|
storageHelper.storage.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
Log.d(tag, "onRequestPermissionResult $requestCode")
|
||||||
|
permissions.forEach { Log.d(tag, "PERMISSION $it") }
|
||||||
|
grantResults.forEach { Log.d(tag, "GRANTREUSLTS $it") }
|
||||||
|
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||||
|
storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
ext {
|
ext {
|
||||||
minSdkVersion = 29
|
minSdkVersion = 23
|
||||||
compileSdkVersion = 30
|
compileSdkVersion = 30
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 30
|
||||||
androidxActivityVersion = '1.2.0'
|
androidxActivityVersion = '1.2.0'
|
||||||
|
|
|
@ -3,12 +3,7 @@
|
||||||
<template v-for="(shelf, index) in groupedBooks">
|
<template v-for="(shelf, index) in groupedBooks">
|
||||||
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
||||||
<template v-for="audiobook in shelf">
|
<template v-for="audiobook in shelf">
|
||||||
<!-- <div :key="audiobook.id" class="relative px-4"> -->
|
|
||||||
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" :local-user-progress="localUserAudiobooks[audiobook.id]" />
|
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" :local-user-progress="localUserAudiobooks[audiobook.id]" />
|
||||||
<!-- <nuxt-link :to="`/audiobook/${audiobook.id}`">
|
|
||||||
<cards-book-cover :audiobook="audiobook" :width="cardWidth" class="mx-auto -mb-px" style="box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166" />
|
|
||||||
</nuxt-link> -->
|
|
||||||
<!-- </div> -->
|
|
||||||
</template>
|
</template>
|
||||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,16 +88,22 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
console.log('Disconnected - Reset to local storage')
|
console.log('Disconnected - Reset to local storage')
|
||||||
this.$store.commit('audiobooks/reset')
|
this.$store.commit('audiobooks/reset')
|
||||||
this.$store.dispatch('downloads/loadFromStorage')
|
this.$store.dispatch('audiobooks/useDownloaded')
|
||||||
|
// this.calcShelves()
|
||||||
|
// this.$store.dispatch('downloads/loadFromStorage')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||||
|
|
||||||
window.addEventListener('resize', this.resize)
|
window.addEventListener('resize', this.resize)
|
||||||
|
|
||||||
|
if (!this.$server) {
|
||||||
|
console.error('Bookshelf mounted no server')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.$server.on('connected', this.socketConnected)
|
this.$server.on('connected', this.socketConnected)
|
||||||
if (this.$server.connected) {
|
if (this.$server.connected) {
|
||||||
this.$store.dispatch('audiobooks/load')
|
this.$store.dispatch('audiobooks/load')
|
||||||
|
@ -115,6 +116,12 @@ export default {
|
||||||
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||||
window.removeEventListener('resize', this.resize)
|
window.removeEventListener('resize', this.resize)
|
||||||
|
|
||||||
|
if (!this.$server) {
|
||||||
|
console.error('Bookshelf beforeDestroy no server')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$server.off('connected', this.socketConnected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -128,6 +128,7 @@ export default {
|
||||||
if (this.$refs.audioPlayerMini) {
|
if (this.$refs.audioPlayerMini) {
|
||||||
this.$refs.audioPlayerMini.terminateStream()
|
this.$refs.audioPlayerMini.terminateStream()
|
||||||
}
|
}
|
||||||
|
this.download = null
|
||||||
this.$store.commit('setPlayingDownload', null)
|
this.$store.commit('setPlayingDownload', null)
|
||||||
|
|
||||||
this.$localStore.setCurrent(null)
|
this.$localStore.setCurrent(null)
|
||||||
|
@ -167,12 +168,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$server.connected) {
|
if (this.$server.connected) {
|
||||||
// var updateObj = {
|
|
||||||
// audiobookId: this.download.id,
|
|
||||||
// totalDuration: this.download.audiobook.duration,
|
|
||||||
// clientCurrentTime: currentTime,
|
|
||||||
// clientProgress: Number((currentTime / this.download.audiobook.duration).toFixed(3))
|
|
||||||
// }
|
|
||||||
this.$server.socket.emit('progress_update', progressUpdate)
|
this.$server.socket.emit('progress_update', progressUpdate)
|
||||||
}
|
}
|
||||||
this.$localStore.updateUserAudiobookProgress(progressUpdate).then(() => {
|
this.$localStore.updateUserAudiobookProgress(progressUpdate).then(() => {
|
||||||
|
@ -184,7 +179,6 @@ export default {
|
||||||
closeStream() {},
|
closeStream() {},
|
||||||
streamClosed(audiobookId) {
|
streamClosed(audiobookId) {
|
||||||
console.log('Stream Closed')
|
console.log('Stream Closed')
|
||||||
|
|
||||||
if (this.stream.audiobook.id === audiobookId || audiobookId === 'n/a') {
|
if (this.stream.audiobook.id === audiobookId || audiobookId === 'n/a') {
|
||||||
this.$store.commit('setStreamAudiobook', null)
|
this.$store.commit('setStreamAudiobook', null)
|
||||||
}
|
}
|
||||||
|
@ -305,23 +299,15 @@ export default {
|
||||||
|
|
||||||
if (this.playingDownload) {
|
if (this.playingDownload) {
|
||||||
console.log('[StreamContainer] Play download on audio mount')
|
console.log('[StreamContainer] Play download on audio mount')
|
||||||
|
if (!this.download) {
|
||||||
|
this.download = { ...this.playingDownload }
|
||||||
|
}
|
||||||
this.playDownload()
|
this.playDownload()
|
||||||
} else if (this.$server.stream) {
|
} else if (this.$server.stream) {
|
||||||
console.log('[StreamContainer] Open stream on audio mount')
|
console.log('[StreamContainer] Open stream on audio mount')
|
||||||
this.streamOpen(this.$server.stream)
|
this.streamOpen(this.$server.stream)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setListeners() {
|
|
||||||
if (!this.$server.socket) {
|
|
||||||
console.error('Invalid server socket not set')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$server.socket.on('stream_open', this.streamOpen)
|
|
||||||
this.$server.socket.on('stream_closed', this.streamClosed)
|
|
||||||
this.$server.socket.on('stream_progress', this.streamProgress)
|
|
||||||
this.$server.socket.on('stream_ready', this.streamReady)
|
|
||||||
this.$server.socket.on('stream_reset', this.streamReset)
|
|
||||||
},
|
|
||||||
changePlaybackSpeed(speed) {
|
changePlaybackSpeed(speed) {
|
||||||
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
||||||
},
|
},
|
||||||
|
@ -342,6 +328,17 @@ export default {
|
||||||
this.cancelStream()
|
this.cancelStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setListeners() {
|
||||||
|
if (!this.$server.socket) {
|
||||||
|
console.error('Invalid server socket not set')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$server.socket.on('stream_open', this.streamOpen)
|
||||||
|
this.$server.socket.on('stream_closed', this.streamClosed)
|
||||||
|
this.$server.socket.on('stream_progress', this.streamProgress)
|
||||||
|
this.$server.socket.on('stream_ready', this.streamReady)
|
||||||
|
this.$server.socket.on('stream_reset', this.streamReset)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -110,6 +110,7 @@ export default {
|
||||||
return this.download ? this.download.cover : null
|
return this.download ? this.download.cover : null
|
||||||
},
|
},
|
||||||
download() {
|
download() {
|
||||||
|
return null
|
||||||
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
|
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
|
||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative">
|
||||||
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
|
<div class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||||
<div class="w-full h-full z-0" ref="coverBg" />
|
<!-- Blurred background for covers that dont fill -->
|
||||||
|
<div v-if="showCoverBg" class="w-full h-full z-0" ref="coverBg" />
|
||||||
|
|
||||||
|
<!-- Image Loading indicator -->
|
||||||
|
<div v-if="!isImageLoaded" class="w-full h-full flex items-center justify-center text-white">
|
||||||
|
<svg class="animate-spin w-12 h-12" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +50,8 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
imageFailed: false,
|
imageFailed: false,
|
||||||
showCoverBg: false
|
showCoverBg: false,
|
||||||
|
isImageLoaded: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -142,10 +151,11 @@ export default {
|
||||||
this.showCoverBg = false
|
this.showCoverBg = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.isImageLoaded = true
|
||||||
},
|
},
|
||||||
imageError(err) {
|
imageError(err) {
|
||||||
console.error('ImgError', err)
|
|
||||||
this.imageFailed = true
|
this.imageFailed = true
|
||||||
|
console.error('ImgError', err, `SET IMAGE FAILED ${this.imageFailed}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,17 @@
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
<p class="absolute top-6 left-2 text-2xl">Downloads</p>
|
<p class="absolute top-6 left-2 text-2xl">Downloads</p>
|
||||||
|
|
||||||
<div 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>
|
<div class="absolute top-16 left-0 right-0 w-full flex items-center px-2 py-1" :class="hasStoragePermission ? '' : 'text-error'">
|
||||||
|
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
|
||||||
|
<p v-if="hasStoragePermission" class="text-sm px-4" @click="changeDownloadFolderClick">{{ downloadFolderSimplePath || 'No Download Folder Selected' }}</p>
|
||||||
|
<p v-else class="text-sm px-4" @click="changeDownloadFolderClick">No Storage Permissions. Click here</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="totalSize" class="absolute bottom-0 left-0 right-0 w-full py-3 text-center">
|
||||||
|
<p class="text-sm text-center text-gray-300">Total: {{ $bytesPretty(totalSize) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 mt-10" style="max-height: 75%" @click.stop>
|
||||||
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||||
<p>No Downloads</p>
|
<p>No Downloads</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,9 +38,10 @@
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<img v-if="download.cover" :src="download.cover" class="w-10 h-16 object-contain" />
|
<img v-if="download.cover" :src="download.cover" class="w-10 h-16 object-contain" />
|
||||||
<img v-else src="/book_placeholder.jpg" class="w-10 h-16 object-contain" />
|
<img v-else src="/book_placeholder.jpg" class="w-10 h-16 object-contain" />
|
||||||
<div class="pl-2 w-1/2">
|
<div class="pl-2 w-2/3">
|
||||||
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
|
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
|
||||||
<p class="font-normal truncate text-xs text-gray-400">{{ download.audiobook.book.author }}</p>
|
<p class="font-normal truncate text-xs text-gray-400">{{ download.audiobook.book.author }}</p>
|
||||||
|
<p class="font-normal truncate text-xs text-gray-400">{{ $bytesPretty(download.size) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div v-if="download.isIncomplete" class="shadow-sm text-warning flex items-center justify-center rounded-full mr-4">
|
<div v-if="download.isIncomplete" class="shadow-sm text-warning flex items-center justify-center rounded-full mr-4">
|
||||||
|
@ -71,11 +82,22 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
downloadingProgress: {}
|
downloadFolder: null,
|
||||||
|
downloadingProgress: {},
|
||||||
|
totalSize: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async show(newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.downloadFolder = await this.$localStore.getDownloadFolder()
|
||||||
|
this.setTotalSize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -87,6 +109,12 @@ export default {
|
||||||
this.$store.commit('downloads/setShowModal', val)
|
this.$store.commit('downloads/setShowModal', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
hasStoragePermission() {
|
||||||
|
return this.$store.state.hasStoragePermission
|
||||||
|
},
|
||||||
|
downloadFolderSimplePath() {
|
||||||
|
return this.downloadFolder ? this.downloadFolder.simplePath : null
|
||||||
|
},
|
||||||
totalDownloads() {
|
totalDownloads() {
|
||||||
return this.downloadsReady.length + this.orphanDownloads.length + this.downloadsDownloading.length
|
return this.downloadsReady.length + this.orphanDownloads.length + this.downloadsDownloading.length
|
||||||
},
|
},
|
||||||
|
@ -132,6 +160,25 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setTotalSize() {
|
||||||
|
var totalSize = 0
|
||||||
|
this.downloadsReady.forEach((dl) => {
|
||||||
|
totalSize += dl.size && !isNaN(dl.size) ? Number(dl.size) : 0
|
||||||
|
})
|
||||||
|
this.totalSize = totalSize
|
||||||
|
},
|
||||||
|
async changeDownloadFolderClick() {
|
||||||
|
if (!this.hasStoragePermission) {
|
||||||
|
console.log('Requesting Storage Permission')
|
||||||
|
AudioDownloader.requestStoragePermission()
|
||||||
|
} else {
|
||||||
|
var folderObj = await AudioDownloader.selectFolder()
|
||||||
|
if (folderObj.error) {
|
||||||
|
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||||
|
}
|
||||||
|
await this.$localStore.setDownloadFolder(folderObj)
|
||||||
|
}
|
||||||
|
},
|
||||||
updateDownloadProgress({ audiobookId, progress }) {
|
updateDownloadProgress({ audiobookId, progress }) {
|
||||||
this.$set(this.downloadingProgress, audiobookId, progress)
|
this.$set(this.downloadingProgress, audiobookId, progress)
|
||||||
},
|
},
|
||||||
|
|
|
@ -43,6 +43,10 @@ export default {
|
||||||
text: 'Added At',
|
text: 'Added At',
|
||||||
value: 'addedAt'
|
value: 'addedAt'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Volume #',
|
||||||
|
value: 'book.volumeNumber'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Duration',
|
text: 'Duration',
|
||||||
value: 'duration'
|
value: 'duration'
|
||||||
|
|
|
@ -55,14 +55,16 @@ export default {
|
||||||
if (localAudiobooks[audiobookId].lastUpdate > userAudiobooks[audiobookId].lastUpdate) {
|
if (localAudiobooks[audiobookId].lastUpdate > userAudiobooks[audiobookId].lastUpdate) {
|
||||||
// Local progress is more recent than user progress
|
// Local progress is more recent than user progress
|
||||||
this.updateAudiobookProgressOnServer(localAudiobooks[audiobookId])
|
this.updateAudiobookProgressOnServer(localAudiobooks[audiobookId])
|
||||||
} else {
|
} else if (localAudiobooks[audiobookId].lastUpdate < userAudiobooks[audiobookId].lastUpdate) {
|
||||||
// Server is more recent than local
|
// Server is more recent than local
|
||||||
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
||||||
|
console.log('SYNCUSERPROGRESS Server IS MORE RECENT for', audiobookId)
|
||||||
localHasUpdates = true
|
localHasUpdates = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not on local yet - store on local
|
// Not on local yet - store on local
|
||||||
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
||||||
|
console.log('SYNCUSERPROGRESS LOCAL Is NOT Stored YET for', audiobookId, JSON.stringify(newestLocal[audiobookId]))
|
||||||
localHasUpdates = true
|
localHasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,22 +113,19 @@ export default {
|
||||||
this.$store.commit('setAppUpdateInfo', result)
|
this.$store.commit('setAppUpdateInfo', result)
|
||||||
if (result.updateAvailability === 2) {
|
if (result.updateAvailability === 2) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$toast.info(`Update is available!`, {
|
this.$toast.info(`Update is available! Click to update.`, {
|
||||||
draggable: false,
|
draggable: false,
|
||||||
hideProgressBar: false,
|
hideProgressBar: false,
|
||||||
timeout: 4000,
|
timeout: 20000,
|
||||||
closeButton: false,
|
closeButton: true,
|
||||||
onClick: this.clickUpdateToast()
|
onClick: this.clickUpdateToast
|
||||||
})
|
})
|
||||||
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
|
||||||
}, 5000)
|
}, 5000)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDownloadProgress(data) {
|
onDownloadProgress(data) {
|
||||||
// var downloadId = data.downloadId
|
|
||||||
var progress = data.progress
|
var progress = data.progress
|
||||||
var filename = data.filename
|
var audiobookId = data.audiobookId
|
||||||
var audiobookId = filename ? Path.basename(filename, Path.extname(filename)) : ''
|
|
||||||
|
|
||||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||||
if (downloadObj) {
|
if (downloadObj) {
|
||||||
|
@ -136,31 +135,70 @@ export default {
|
||||||
this.$toast.update(downloadObj.toastId, { content: `${progress}% Downloading ${downloadObj.audiobook.book.title}` })
|
this.$toast.update(downloadObj.toastId, { content: `${progress}% Downloading ${downloadObj.audiobook.book.title}` })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDownloadFailed(data) {
|
||||||
|
if (!data.audiobookId) {
|
||||||
|
console.error('Download failed invalid audiobook id', data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var downloadObj = this.$store.getters['downloads/getDownload'](data.audiobookId)
|
||||||
|
if (!downloadObj) {
|
||||||
|
console.error('Failed to find download for audiobook', data.audiobookId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var message = data.error || 'Unknown Error'
|
||||||
|
this.$toast.update(downloadObj.toastId, { content: `Failed. ${message}.`, options: { timeout: 5000, type: 'error' } }, true)
|
||||||
|
this.$store.commit('downloads/removeDownload', downloadObj)
|
||||||
|
},
|
||||||
onDownloadComplete(data) {
|
onDownloadComplete(data) {
|
||||||
|
if (!data.audiobookId) {
|
||||||
|
console.error('Download compelte invalid audiobook id', data)
|
||||||
|
return
|
||||||
|
}
|
||||||
var downloadId = data.downloadId
|
var downloadId = data.downloadId
|
||||||
var contentUrl = data.contentUrl
|
var contentUrl = data.contentUrl
|
||||||
|
var folderUrl = data.folderUrl
|
||||||
|
var folderName = data.folderName
|
||||||
|
var storageId = data.storageId
|
||||||
|
var storageType = data.storageType
|
||||||
|
var simplePath = data.simplePath
|
||||||
var filename = data.filename
|
var filename = data.filename
|
||||||
var audiobookId = filename ? Path.basename(filename, Path.extname(filename)) : ''
|
var audiobookId = data.audiobookId
|
||||||
|
var size = data.size || 0
|
||||||
|
var isCover = !!data.isCover
|
||||||
|
|
||||||
if (audiobookId) {
|
console.log('Download complete', filename, downloadId, contentUrl, 'AudiobookId:', audiobookId, 'Is Cover:', isCover)
|
||||||
|
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||||
|
if (!downloadObj) {
|
||||||
|
console.error('Failed to find download for audiobook', audiobookId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCover) {
|
||||||
// Notify server to remove prepared download
|
// Notify server to remove prepared download
|
||||||
if (this.$server.socket) {
|
if (this.$server.socket) {
|
||||||
this.$server.socket.emit('remove_download', audiobookId)
|
this.$server.socket.emit('remove_download', audiobookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Download complete', filename, downloadId, contentUrl, 'AudiobookId:', audiobookId)
|
|
||||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
|
||||||
if (!downloadObj) {
|
|
||||||
console.error('Could not find download...')
|
|
||||||
} else {
|
|
||||||
this.$toast.update(downloadObj.toastId, { content: `Success! ${downloadObj.audiobook.book.title} downloaded.`, options: { timeout: 5000, type: 'success' } }, true)
|
this.$toast.update(downloadObj.toastId, { content: `Success! ${downloadObj.audiobook.book.title} downloaded.`, options: { timeout: 5000, type: 'success' } }, true)
|
||||||
|
|
||||||
console.log('Found download, update with content url')
|
|
||||||
delete downloadObj.isDownloading
|
delete downloadObj.isDownloading
|
||||||
delete downloadObj.isPreparing
|
delete downloadObj.isPreparing
|
||||||
downloadObj.contentUrl = contentUrl
|
downloadObj.contentUrl = contentUrl
|
||||||
|
downloadObj.simplePath = simplePath
|
||||||
|
downloadObj.folderUrl = folderUrl
|
||||||
|
downloadObj.folderName = folderName
|
||||||
|
downloadObj.storageType = storageType
|
||||||
|
downloadObj.storageId = storageId
|
||||||
|
downloadObj.basePath = data.basePath || null
|
||||||
|
downloadObj.size = size
|
||||||
|
this.$store.commit('downloads/addUpdateDownload', downloadObj)
|
||||||
|
} else {
|
||||||
|
downloadObj.coverUrl = contentUrl
|
||||||
|
downloadObj.cover = Capacitor.convertFileSrc(contentUrl)
|
||||||
|
downloadObj.coverSize = size
|
||||||
|
downloadObj.coverBasePath = data.basePath || null
|
||||||
|
console.log('Updating download with cover', downloadObj.cover)
|
||||||
this.$store.commit('downloads/addUpdateDownload', downloadObj)
|
this.$store.commit('downloads/addUpdateDownload', downloadObj)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async checkLoadCurrent() {
|
async checkLoadCurrent() {
|
||||||
|
@ -176,59 +214,38 @@ export default {
|
||||||
this.$localStore.setCurrent(null)
|
this.$localStore.setCurrent(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMediaLoaded(items) {
|
async onMediaLoaded(items) {
|
||||||
var jsitems = JSON.parse(items)
|
var jsitems = JSON.parse(items)
|
||||||
jsitems = jsitems.map((item) => {
|
jsitems = jsitems.map((item) => {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
|
||||||
size: item.size,
|
|
||||||
duration: item.duration,
|
|
||||||
filename: item.name,
|
filename: item.name,
|
||||||
audiobookId: item.name ? Path.basename(item.name, Path.extname(item.name)) : '',
|
size: item.size,
|
||||||
contentUrl: item.uri.replace(/\\\//g, '/'),
|
contentUrl: item.uri,
|
||||||
coverUrl: item.coverUrl || null
|
coverUrl: item.coverUrl || null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
jsitems.forEach((item) => {
|
console.log('onMediaLoaded Items', JSON.stringify(jsitems))
|
||||||
var download = this.$store.getters['downloads/getDownload'](item.audiobookId)
|
|
||||||
if (!download) {
|
|
||||||
console.error(`Unknown media item found for filename ${item.filename}`)
|
|
||||||
|
|
||||||
var orphanDownload = {
|
var downloads = await this.$sqlStore.getAllDownloads()
|
||||||
id: `orphan-${item.id}`,
|
for (let i = 0; i < downloads.length; i++) {
|
||||||
contentUrl: item.contentUrl,
|
var download = downloads[i]
|
||||||
coverUrl: item.coverUrl,
|
var jsitem = jsitems.find((item) => item.contentUrl === download.contentUrl)
|
||||||
cover: item.coverUrl ? Capacitor.convertFileSrc(item.coverUrl) : null,
|
if (!jsitem) {
|
||||||
mediaId: item.id,
|
console.error('Removing download was not found', JSON.stringify(download))
|
||||||
filename: item.filename,
|
await this.$sqlStore.removeDownload(download.id)
|
||||||
size: item.size,
|
} else if (download.coverUrl && !jsitem.coverUrl) {
|
||||||
duration: item.duration,
|
console.error('Removing cover for download was not found')
|
||||||
isOrphan: true
|
download.cover = null
|
||||||
}
|
download.coverUrl = null
|
||||||
this.$store.commit('downloads/addUpdateDownload', orphanDownload)
|
download.size = jsitem.size || 0
|
||||||
} else {
|
this.$store.commit('downloads/addUpdateDownload', download)
|
||||||
console.log(`Found media item for audiobook ${download.audiobook.book.title} (${item.audiobookId})`)
|
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||||
download.contentUrl = item.contentUrl
|
} else {
|
||||||
download.coverUrl = item.coverUrl
|
download.size = jsitem.size || 0
|
||||||
download.cover = item.coverUrl ? Capacitor.convertFileSrc(item.coverUrl) : null
|
|
||||||
download.size = item.size
|
|
||||||
download.duration = item.duration
|
|
||||||
this.$store.commit('downloads/addUpdateDownload', download)
|
this.$store.commit('downloads/addUpdateDownload', download)
|
||||||
|
|
||||||
download.audiobook.isDownloaded = true
|
|
||||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
var downloads = this.$store.state.downloads.downloads
|
|
||||||
|
|
||||||
downloads.forEach((download) => {
|
|
||||||
var matchingItem = jsitems.find((item) => item.audiobookId === download.id)
|
|
||||||
if (!matchingItem) {
|
|
||||||
console.error(`Saved download not in media store ${download.audiobook.book.title} (${download.id})`)
|
|
||||||
this.$store.commit('downloads/removeDownload', download)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
this.checkLoadCurrent()
|
this.checkLoadCurrent()
|
||||||
},
|
},
|
||||||
|
@ -245,32 +262,95 @@ export default {
|
||||||
this.$refs.streamContainer.cancelStream()
|
this.$refs.streamContainer.cancelStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.contentUrl) {
|
if (download.contentUrl) {
|
||||||
await AudioDownloader.delete({ audiobookId: download.id, filename: download.filename, url: download.contentUrl, coverUrl: download.coverUrl })
|
await AudioDownloader.delete(download)
|
||||||
}
|
}
|
||||||
this.$store.commit('downloads/removeDownload', download)
|
this.$store.commit('downloads/removeDownload', download)
|
||||||
},
|
},
|
||||||
async initMediaStore() {
|
async onPermissionUpdate(data) {
|
||||||
// Load local database of downloads
|
var val = data.value
|
||||||
await this.$store.dispatch('downloads/loadFromStorage')
|
console.log('Permission Update', val)
|
||||||
await this.$localStore.loadUserAudiobooks()
|
if (val === 'required') {
|
||||||
|
console.log('Permission Required - Making Request')
|
||||||
|
this.$store.commit('setHasStoragePermission', false)
|
||||||
|
} else if (val === 'canceled' || val === 'denied') {
|
||||||
|
console.error('Permission denied by user')
|
||||||
|
this.$store.commit('setHasStoragePermission', false)
|
||||||
|
} else if (val === 'granted') {
|
||||||
|
console.log('User Granted permission')
|
||||||
|
this.$store.commit('setHasStoragePermission', true)
|
||||||
|
|
||||||
|
var folder = data
|
||||||
|
delete folder.value
|
||||||
|
console.log('Setting folder', JSON.stringify(folder))
|
||||||
|
await this.$localStore.setDownloadFolder(folder)
|
||||||
|
} else {
|
||||||
|
console.warn('Other permisiso update', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async initMediaStore() {
|
||||||
// Request and setup listeners for media files on native
|
// Request and setup listeners for media files on native
|
||||||
|
console.log('Permissino SET LISTENER')
|
||||||
|
AudioDownloader.addListener('permission', (data) => {
|
||||||
|
this.onPermissionUpdate(data)
|
||||||
|
})
|
||||||
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
||||||
this.onDownloadComplete(data)
|
this.onDownloadComplete(data)
|
||||||
})
|
})
|
||||||
|
AudioDownloader.addListener('onDownloadFailed', (data) => {
|
||||||
|
this.onDownloadFailed(data)
|
||||||
|
})
|
||||||
AudioDownloader.addListener('onMediaLoaded', (data) => {
|
AudioDownloader.addListener('onMediaLoaded', (data) => {
|
||||||
this.onMediaLoaded(data.items)
|
this.onMediaLoaded(data.items)
|
||||||
})
|
})
|
||||||
AudioDownloader.addListener('onDownloadProgress', (data) => {
|
AudioDownloader.addListener('onDownloadProgress', (data) => {
|
||||||
this.onDownloadProgress(data)
|
this.onDownloadProgress(data)
|
||||||
})
|
})
|
||||||
AudioDownloader.load()
|
|
||||||
|
await this.$localStore.loadUserAudiobooks()
|
||||||
|
await this.$localStore.getDownloadFolder()
|
||||||
|
|
||||||
|
var userSavedSettings = await this.$localStore.getUserSettings()
|
||||||
|
if (userSavedSettings) {
|
||||||
|
this.$store.commit('user/setSettings', userSavedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloads = await this.$sqlStore.getAllDownloads()
|
||||||
|
if (downloads.length) {
|
||||||
|
var urls = downloads
|
||||||
|
.map((d) => {
|
||||||
|
return {
|
||||||
|
contentUrl: d.contentUrl,
|
||||||
|
coverUrl: d.coverUrl || '',
|
||||||
|
storageId: d.storageId,
|
||||||
|
basePath: d.basePath,
|
||||||
|
coverBasePath: d.coverBasePath || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((d) => {
|
||||||
|
if (!d.contentUrl) {
|
||||||
|
console.error('Invalid Download no Content URL', JSON.stringify(d))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
AudioDownloader.load({
|
||||||
|
audiobookUrls: urls
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkPermission = await AudioDownloader.checkStoragePermission()
|
||||||
|
console.log('Storage Permission is', checkPermission.value)
|
||||||
|
if (!checkPermission.value) {
|
||||||
|
console.log('Will require permissions')
|
||||||
|
} else {
|
||||||
|
console.log('Has Storage Permission')
|
||||||
|
this.$store.commit('setHasStoragePermission', true)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async setupNetworkListener() {
|
async setupNetworkListener() {
|
||||||
var status = await Network.getStatus()
|
var status = await Network.getStatus()
|
||||||
console.log('Network status', status.connected, status.connectionType)
|
|
||||||
this.$store.commit('setNetworkStatus', status)
|
this.$store.commit('setNetworkStatus', status)
|
||||||
|
|
||||||
Network.addListener('networkStatusChange', (status) => {
|
Network.addListener('networkStatusChange', (status) => {
|
||||||
|
@ -282,13 +362,27 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
if (!this.$server) return console.error('No Server')
|
if (!this.$server) return console.error('No Server')
|
||||||
|
|
||||||
|
console.log('Default Mounted')
|
||||||
|
|
||||||
this.$server.on('connected', this.connected)
|
this.$server.on('connected', this.connected)
|
||||||
this.$server.on('initialStream', this.initialStream)
|
this.$server.on('initialStream', this.initialStream)
|
||||||
|
|
||||||
|
if (this.$store.state.isFirstLoad) {
|
||||||
|
this.$store.commit('setIsFirstLoad', false)
|
||||||
this.setupNetworkListener()
|
this.setupNetworkListener()
|
||||||
this.checkForUpdate()
|
this.checkForUpdate()
|
||||||
this.initMediaStore()
|
this.initMediaStore()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (!this.$server) {
|
||||||
|
console.error('No Server beforeDestroy')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('Default Before Destroy remove listeners')
|
||||||
|
this.$server.off('connected', this.connected)
|
||||||
|
this.$server.off('initialStream', this.initialStream)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -297,7 +391,6 @@ export default {
|
||||||
height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
#content.playerOpen {
|
#content.playerOpen {
|
||||||
/* height: calc(100vh - 204px); */
|
|
||||||
height: calc(100vh - 240px);
|
height: calc(100vh - 240px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.8.4-beta",
|
"version": "v0.9.0-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
|
|
@ -68,6 +68,9 @@ export default {
|
||||||
})
|
})
|
||||||
this.$server.logout()
|
this.$server.logout()
|
||||||
this.$router.push('/connect')
|
this.$router.push('/connect')
|
||||||
|
|
||||||
|
this.$store.commit('audiobooks/reset')
|
||||||
|
this.$store.dispatch('audiobooks/useDownloaded')
|
||||||
},
|
},
|
||||||
openAppStore() {
|
openAppStore() {
|
||||||
AppUpdate.openAppStore()
|
AppUpdate.openAppStore()
|
||||||
|
|
|
@ -162,6 +162,9 @@ export default {
|
||||||
},
|
},
|
||||||
downloadObj() {
|
downloadObj() {
|
||||||
return this.$store.getters['downloads/getDownload'](this.audiobookId)
|
return this.$store.getters['downloads/getDownload'](this.audiobookId)
|
||||||
|
},
|
||||||
|
hasStoragePermission() {
|
||||||
|
return this.$store.state.hasStoragePermission
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -229,17 +232,32 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async prepareDownload() {
|
async prepareDownload() {
|
||||||
// var audiobook = await this.$axios.$get(`/api/audiobook/${this.audiobookId}`).catch((error) => {
|
|
||||||
// console.error('Failed', error)
|
|
||||||
// return false
|
|
||||||
// })
|
|
||||||
var audiobook = this.audiobook
|
var audiobook = this.audiobook
|
||||||
if (!audiobook) {
|
if (!audiobook) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.hasStoragePermission) {
|
||||||
|
await AudioDownloader.requestStoragePermission()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download Path
|
||||||
|
var dlFolder = this.$localStore.downloadFolder
|
||||||
|
if (!dlFolder) {
|
||||||
|
console.log('No download folder, request from ujser')
|
||||||
|
var folderObj = await AudioDownloader.selectFolder()
|
||||||
|
|
||||||
|
if (folderObj.error) {
|
||||||
|
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||||
|
}
|
||||||
|
dlFolder = folderObj
|
||||||
|
await this.$localStore.setDownloadFolder(folderObj)
|
||||||
|
}
|
||||||
|
|
||||||
var downloadObject = {
|
var downloadObject = {
|
||||||
id: this.audiobookId,
|
id: this.audiobookId,
|
||||||
|
downloadFolderUrl: dlFolder.uri,
|
||||||
audiobook: {
|
audiobook: {
|
||||||
...audiobook
|
...audiobook
|
||||||
},
|
},
|
||||||
|
@ -278,59 +296,37 @@ export default {
|
||||||
}
|
}
|
||||||
return _clean
|
return _clean
|
||||||
},
|
},
|
||||||
async downloadCover(download) {
|
|
||||||
var coverUrl = this.getCoverUrlForDownload()
|
|
||||||
if (!coverUrl) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var extname = Path.extname(coverUrl) || '.jpg'
|
|
||||||
|
|
||||||
var downloadRequestPayload = {
|
|
||||||
audiobookId: download.id,
|
|
||||||
downloadUrl: coverUrl,
|
|
||||||
filename: `${download.id}${extname}`,
|
|
||||||
title: download.audiobook.book.title
|
|
||||||
}
|
|
||||||
console.log('Starting cover download', coverUrl, downloadRequestPayload.filename)
|
|
||||||
var downloadRes = await AudioDownloader.downloadCover(downloadRequestPayload)
|
|
||||||
if (downloadRes && downloadRes.url) {
|
|
||||||
console.log('Cover art downloaded', downloadRes.url)
|
|
||||||
return downloadRes.url
|
|
||||||
} else {
|
|
||||||
console.error('Cover art failed to download')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async startDownload(url, fileext, download) {
|
async startDownload(url, fileext, download) {
|
||||||
this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
|
this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
|
||||||
|
|
||||||
|
var coverDownloadUrl = this.getCoverUrlForDownload()
|
||||||
|
var coverFilename = null
|
||||||
|
if (coverDownloadUrl) {
|
||||||
|
var coverExt = Path.extname(coverDownloadUrl) || '.jpg'
|
||||||
|
coverFilename = `cover-${download.id}${coverExt}`
|
||||||
|
}
|
||||||
|
|
||||||
download.isDownloading = true
|
download.isDownloading = true
|
||||||
download.isPreparing = false
|
download.isPreparing = false
|
||||||
download.filename = `${download.id}${fileext}`
|
download.filename = `${download.audiobook.book.title}${fileext}`
|
||||||
this.$store.commit('downloads/addUpdateDownload', download)
|
this.$store.commit('downloads/addUpdateDownload', download)
|
||||||
|
|
||||||
console.log('Starting Download URL', url)
|
console.log('Starting Download URL', url)
|
||||||
var downloadRequestPayload = {
|
var downloadRequestPayload = {
|
||||||
audiobookId: download.id,
|
audiobookId: download.id,
|
||||||
filename: download.filename,
|
filename: download.filename,
|
||||||
|
coverFilename,
|
||||||
|
coverDownloadUrl,
|
||||||
downloadUrl: url,
|
downloadUrl: url,
|
||||||
title: download.audiobook.book.title
|
title: download.audiobook.book.title,
|
||||||
|
downloadFolderUrl: download.downloadFolderUrl
|
||||||
}
|
}
|
||||||
var downloadRes = await AudioDownloader.download(downloadRequestPayload)
|
var downloadRes = await AudioDownloader.download(downloadRequestPayload)
|
||||||
var downloadId = downloadRes.value
|
if (downloadRes.error) {
|
||||||
if (!downloadId) {
|
var errorMsg = downloadRes.error || 'Unknown error'
|
||||||
console.error('Invalid download, removing')
|
console.error('Download error', errorMsg)
|
||||||
this.$toast.update(download.toastId, { content: `Failed download "${download.audiobook.book.title}"`, options: { timeout: 5000, type: 'error' } })
|
this.$toast.update(download.toastId, { content: `Error: ${errorMsg}`, options: { timeout: 5000, type: 'error' } })
|
||||||
this.$store.commit('downloads/removeDownload', download)
|
this.$store.commit('downloads/removeDownload', download)
|
||||||
} else {
|
|
||||||
console.log('Download cover now')
|
|
||||||
var coverUrl = await this.downloadCover(download)
|
|
||||||
if (coverUrl) {
|
|
||||||
console.log('Got cover url', coverUrl)
|
|
||||||
download.coverUrl = coverUrl
|
|
||||||
download.cover = Capacitor.convertFileSrc(coverUrl)
|
|
||||||
this.$store.commit('downloads/addUpdateDownload', download)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
downloadReady(prepareDownload) {
|
downloadReady(prepareDownload) {
|
||||||
|
@ -363,16 +359,24 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
if (!this.$server.socket) {
|
||||||
|
console.warn('Audiobook Page mounted: Server socket not set')
|
||||||
|
} else {
|
||||||
this.$server.socket.on('download_ready', this.downloadReady)
|
this.$server.socket.on('download_ready', this.downloadReady)
|
||||||
this.$server.socket.on('download_killed', this.downloadKilled)
|
this.$server.socket.on('download_killed', this.downloadKilled)
|
||||||
this.$server.socket.on('download_failed', this.downloadFailed)
|
this.$server.socket.on('download_failed', this.downloadFailed)
|
||||||
|
}
|
||||||
|
|
||||||
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
if (!this.$server.socket) {
|
||||||
|
console.warn('Audiobook Page beforeDestroy: Server socket not set')
|
||||||
|
} else {
|
||||||
this.$server.socket.off('download_ready', this.downloadReady)
|
this.$server.socket.off('download_ready', this.downloadReady)
|
||||||
this.$server.socket.off('download_killed', this.downloadKilled)
|
this.$server.socket.off('download_killed', this.downloadKilled)
|
||||||
this.$server.socket.off('download_failed', this.downloadFailed)
|
this.$server.socket.off('download_failed', this.downloadFailed)
|
||||||
|
}
|
||||||
|
|
||||||
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,13 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (!this.$server) {
|
||||||
|
console.error('Connected beforeDestroy: No Server')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$server.off('connected', this.socketConnected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||||
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||||
|
if (isNaN(bytes) || bytes === null) return 'Invalid Bytes'
|
||||||
if (bytes === 0) {
|
if (bytes === 0) {
|
||||||
return '0 Bytes'
|
return '0 Bytes'
|
||||||
}
|
}
|
||||||
|
|
|
@ -408,6 +408,7 @@ class LocalStorage {
|
||||||
this.vuexStore = vuexStore
|
this.vuexStore = vuexStore
|
||||||
|
|
||||||
this.userAudiobooksLoaded = false
|
this.userAudiobooksLoaded = false
|
||||||
|
this.downloadFolder = null
|
||||||
this.userAudiobooks = {}
|
this.userAudiobooks = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,6 +503,32 @@ class LocalStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setDownloadFolder(folderObj) {
|
||||||
|
try {
|
||||||
|
if (folderObj) {
|
||||||
|
await Storage.set({ key: 'downloadFolder', value: JSON.stringify(folderObj) })
|
||||||
|
this.downloadFolder = folderObj
|
||||||
|
} else {
|
||||||
|
await Storage.remove({ key: 'downloadFolder' })
|
||||||
|
this.downloadFolder = null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LocalStorage] Failed to set download folder', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDownloadFolder() {
|
||||||
|
try {
|
||||||
|
var _value = (await Storage.get({ key: 'downloadFolder' }) || {}).value || null
|
||||||
|
if (!_value) return null
|
||||||
|
this.downloadFolder = JSON.parse(_value)
|
||||||
|
return this.downloadFolder
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LocalStorage] Failed to get download folder', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getServerUrl() {
|
async getServerUrl() {
|
||||||
try {
|
try {
|
||||||
return (await Storage.get({ key: 'serverUrl' }) || {}).value || null
|
return (await Storage.get({ key: 'serverUrl' }) || {}).value || null
|
||||||
|
|
|
@ -15,7 +15,11 @@ export const getters = {
|
||||||
getAudiobook: state => id => {
|
getAudiobook: state => id => {
|
||||||
return state.audiobooks.find(ab => ab.id === id)
|
return state.audiobooks.find(ab => ab.id === id)
|
||||||
},
|
},
|
||||||
getFiltered: (state, getters, rootState) => () => {
|
getFiltered: (state, getters, rootState, rootGetters) => () => {
|
||||||
|
// var isDisc = !rootState.networkConnected || !rootState.socketConnected
|
||||||
|
// console.log('GET FILETERED', isDisc)
|
||||||
|
// var filtered = isDisc ? rootGetters['downloads/getAudiobooks'] : state.audiobooks
|
||||||
|
// console.log('FILTERED LEN', filtered.length)
|
||||||
var filtered = state.audiobooks
|
var filtered = state.audiobooks
|
||||||
var settings = rootState.user.settings || {}
|
var settings = rootState.user.settings || {}
|
||||||
var filterBy = settings.filterBy || ''
|
var filterBy = settings.filterBy || ''
|
||||||
|
@ -36,9 +40,12 @@ export const getters = {
|
||||||
var direction = settings.orderDesc ? 'desc' : 'asc'
|
var direction = settings.orderDesc ? 'desc' : 'asc'
|
||||||
|
|
||||||
var filtered = getters.getFiltered()
|
var filtered = getters.getFiltered()
|
||||||
|
var orderByNumber = settings.orderBy === 'book.volumeNumber'
|
||||||
return sort(filtered)[direction]((ab) => {
|
return sort(filtered)[direction]((ab) => {
|
||||||
// Supports dot notation strings i.e. "book.title"
|
// Supports dot notation strings i.e. "book.title"
|
||||||
return settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
var value = settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
||||||
|
if (orderByNumber && !isNaN(value)) return Number(value)
|
||||||
|
return value
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getUniqueAuthors: (state) => {
|
getUniqueAuthors: (state) => {
|
||||||
|
@ -63,6 +70,9 @@ export const actions = {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
useDownloaded({ commit, rootGetters }) {
|
||||||
|
commit('set', rootGetters['downloads/getAudiobooks'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,9 @@ export const getters = {
|
||||||
getDownloadIfReady: (state) => id => {
|
getDownloadIfReady: (state) => id => {
|
||||||
var download = state.downloads.find(d => d.id === id)
|
var download = state.downloads.find(d => d.id === id)
|
||||||
return !!download && !download.isDownloading && !download.isPreparing ? download : null
|
return !!download && !download.isDownloading && !download.isPreparing ? download : null
|
||||||
|
},
|
||||||
|
getAudiobooks: (state) => {
|
||||||
|
return state.downloads.map(dl => dl.audiobook)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,10 @@ export const state = () => ({
|
||||||
appUpdateInfo: null,
|
appUpdateInfo: null,
|
||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
networkConnected: false,
|
networkConnected: false,
|
||||||
networkConnectionType: 'unknown',
|
networkConnectionType: null,
|
||||||
streamListener: null
|
streamListener: null,
|
||||||
|
isFirstLoad: true,
|
||||||
|
hasStoragePermission: false
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -27,6 +29,12 @@ export const getters = {
|
||||||
export const actions = {}
|
export const actions = {}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setHasStoragePermission(state, val) {
|
||||||
|
state.hasStoragePermission = val
|
||||||
|
},
|
||||||
|
setIsFirstLoad(state, val) {
|
||||||
|
state.isFirstLoad = val
|
||||||
|
},
|
||||||
setAppUpdateInfo(state, info) {
|
setAppUpdateInfo(state, info) {
|
||||||
state.appUpdateInfo = info
|
state.appUpdateInfo = info
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Storage } from '@capacitor/storage'
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue