mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-02 01:54:44 +02:00
Update:Android support storing downloads to internal app storage #635
This commit is contained in:
parent
87c74fe78b
commit
e6aaccfc74
14 changed files with 715 additions and 61 deletions
|
@ -15,6 +15,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class FolderScanner(var ctx: Context) {
|
class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
|
@ -231,8 +232,189 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun scanInternalDownloadItem(downloadItem:DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
|
||||||
|
val localLibraryItemId = "local_${downloadItem.libraryItemId}"
|
||||||
|
|
||||||
|
var localEpisodeId:String? = null
|
||||||
|
var localLibraryItem:LocalLibraryItem?
|
||||||
|
if (downloadItem.mediaType == "book") {
|
||||||
|
localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, downloadItem.itemFolderPath, downloadItem.itemFolderPath, "", false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
|
||||||
|
} else {
|
||||||
|
// Lookup or create podcast local library item
|
||||||
|
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
if (localLibraryItem == null) {
|
||||||
|
Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
|
||||||
|
localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, downloadItem.itemFolderPath, downloadItem.itemFolderPath, "", false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
||||||
|
var foundEBookFile = false
|
||||||
|
|
||||||
|
downloadItem.downloadItemParts.forEach { downloadItemPart ->
|
||||||
|
Log.d(tag, "Scan internal storage item with finalDestinationUri=${downloadItemPart.finalDestinationUri}")
|
||||||
|
|
||||||
|
val file = File(downloadItemPart.finalDestinationPath)
|
||||||
|
Log.d(tag, "Scan internal storage item created file ${file.name}")
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
Log.e(tag, "scanInternalDownloadItem: Null docFile for path ${downloadItemPart.finalDestinationPath}")
|
||||||
|
} else {
|
||||||
|
if (downloadItemPart.audioTrack != null) {
|
||||||
|
val audioTrackFromServer = downloadItemPart.audioTrack
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"scanInternalDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}"
|
||||||
|
)
|
||||||
|
|
||||||
|
val localFileId = DeviceManager.getBase64Id(file.name)
|
||||||
|
Log.d(tag, "Scan internal file localFileId=$localFileId")
|
||||||
|
val localFile = LocalFile(
|
||||||
|
localFileId,
|
||||||
|
file.name,
|
||||||
|
downloadItemPart.finalDestinationUri.toString(),
|
||||||
|
file.getBasePath(ctx),
|
||||||
|
file.absolutePath,
|
||||||
|
file.getSimplePath(ctx),
|
||||||
|
file.mimeType,
|
||||||
|
file.length()
|
||||||
|
)
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
|
||||||
|
// Create new audio track
|
||||||
|
val track = AudioTrack(
|
||||||
|
audioTrackFromServer.index,
|
||||||
|
audioTrackFromServer.startOffset,
|
||||||
|
audioTrackFromServer.duration,
|
||||||
|
localFile.filename ?: "",
|
||||||
|
localFile.contentUrl,
|
||||||
|
localFile.mimeType ?: "",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
localFileId,
|
||||||
|
null,
|
||||||
|
audioTrackFromServer.index
|
||||||
|
)
|
||||||
|
audioTracks.add(track)
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add podcast episodes to library
|
||||||
|
downloadItemPart.episode?.let { podcastEpisode ->
|
||||||
|
val podcast = localLibraryItem.media as Podcast
|
||||||
|
val newEpisode = podcast.addEpisode(track, podcastEpisode)
|
||||||
|
localEpisodeId = newEpisode.id
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"scanInternalDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (downloadItemPart.ebookFile != null) {
|
||||||
|
foundEBookFile = true
|
||||||
|
Log.d(tag, "scanInternalDownloadItem: Ebook file found with mimetype=${file.mimeType}")
|
||||||
|
val localFileId = DeviceManager.getBase64Id(file.name)
|
||||||
|
val localFile = LocalFile(
|
||||||
|
localFileId,
|
||||||
|
file.name,
|
||||||
|
Uri.fromFile(file).toString(),
|
||||||
|
file.getBasePath(ctx),
|
||||||
|
file.absolutePath,
|
||||||
|
file.getSimplePath(ctx),
|
||||||
|
file.mimeType,
|
||||||
|
file.length()
|
||||||
|
)
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
|
||||||
|
val ebookFile = EBookFile(
|
||||||
|
downloadItemPart.ebookFile.ino,
|
||||||
|
downloadItemPart.ebookFile.metadata,
|
||||||
|
downloadItemPart.ebookFile.ebookFormat,
|
||||||
|
true,
|
||||||
|
localFileId,
|
||||||
|
localFile.contentUrl
|
||||||
|
)
|
||||||
|
(localLibraryItem.media as Book).ebookFile = ebookFile
|
||||||
|
Log.d(tag, "scanInternalDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
|
||||||
|
} else {
|
||||||
|
val localFileId = DeviceManager.getBase64Id(file.name)
|
||||||
|
val localFile = LocalFile(localFileId,file.name,Uri.fromFile(file).toString(),file.getBasePath(ctx),file.absolutePath,file.getSimplePath(ctx),file.mimeType,file.length())
|
||||||
|
|
||||||
|
localLibraryItem.coverAbsolutePath = localFile.absolutePath
|
||||||
|
localLibraryItem.coverContentUrl = localFile.contentUrl
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioTracks.isEmpty() && !foundEBookFile) {
|
||||||
|
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}")
|
||||||
|
return cb(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For books sort audio tracks then set
|
||||||
|
if (downloadItem.mediaType == "book") {
|
||||||
|
audioTracks.sortBy { it.index }
|
||||||
|
|
||||||
|
var indexCheck = 1
|
||||||
|
var startOffset = 0.0
|
||||||
|
audioTracks.forEach { audioTrack ->
|
||||||
|
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
|
||||||
|
audioTrack.index = indexCheck
|
||||||
|
audioTrack.startOffset = startOffset
|
||||||
|
}
|
||||||
|
indexCheck++
|
||||||
|
startOffset += audioTrack.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
localLibraryItem.media.setAudioTracks(audioTracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null)
|
||||||
|
|
||||||
|
// If library item had media progress then make local media progress and save
|
||||||
|
downloadItem.userMediaProgress?.let { mediaProgress ->
|
||||||
|
val localMediaProgressId = if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
|
val newLocalMediaProgress = LocalMediaProgress(
|
||||||
|
id = localMediaProgressId,
|
||||||
|
localLibraryItemId = localLibraryItemId,
|
||||||
|
localEpisodeId = localEpisodeId,
|
||||||
|
duration = mediaProgress.duration,
|
||||||
|
progress = mediaProgress.progress,
|
||||||
|
currentTime = mediaProgress.currentTime,
|
||||||
|
isFinished = false,
|
||||||
|
ebookLocation = mediaProgress.ebookLocation,
|
||||||
|
ebookProgress = mediaProgress.ebookProgress,
|
||||||
|
lastUpdate = mediaProgress.lastUpdate,
|
||||||
|
startedAt = mediaProgress.startedAt,
|
||||||
|
finishedAt = mediaProgress.finishedAt,
|
||||||
|
serverConnectionConfigId = downloadItem.serverConnectionConfigId,
|
||||||
|
serverAddress = downloadItem.serverAddress,
|
||||||
|
serverUserId = downloadItem.serverUserId,
|
||||||
|
libraryItemId = downloadItem.libraryItemId,
|
||||||
|
episodeId = downloadItem.episodeId)
|
||||||
|
Log.d(tag, "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}")
|
||||||
|
DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress)
|
||||||
|
|
||||||
|
downloadItemScanResult.localMediaProgress = newLocalMediaProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
|
|
||||||
|
cb(downloadItemScanResult)
|
||||||
|
}
|
||||||
|
|
||||||
// Scan item after download and create local library item
|
// Scan item after download and create local library item
|
||||||
fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
|
fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
|
||||||
|
// If downloading to internal storage handle separately
|
||||||
|
if (downloadItem.isInternalStorage) {
|
||||||
|
scanInternalDownloadItem(downloadItem, cb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
||||||
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
|
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
||||||
|
@ -44,6 +46,11 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
||||||
fun onDownloadItemComplete(jsobj:JSObject)
|
fun onDownloadItemComplete(jsobj:JSObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InternalProgressCallback {
|
||||||
|
fun onProgress(totalBytesWritten:Long, progress: Long)
|
||||||
|
fun onComplete(failed: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var isDownloading:Boolean = false
|
var isDownloading:Boolean = false
|
||||||
}
|
}
|
||||||
|
@ -65,6 +72,26 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
||||||
|
|
||||||
if (nextDownloadItemParts.size > 0) {
|
if (nextDownloadItemParts.size > 0) {
|
||||||
nextDownloadItemParts.forEach {
|
nextDownloadItemParts.forEach {
|
||||||
|
if (it.isInternalStorage) {
|
||||||
|
val file = File(it.finalDestinationPath)
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
val fileOutputStream = FileOutputStream(it.finalDestinationPath)
|
||||||
|
val internalProgressCallback = (object : InternalProgressCallback {
|
||||||
|
override fun onProgress(totalBytesWritten:Long, progress: Long) {
|
||||||
|
it.bytesDownloaded = totalBytesWritten
|
||||||
|
it.progress = progress
|
||||||
|
}
|
||||||
|
override fun onComplete(failed:Boolean) {
|
||||||
|
it.failed = failed
|
||||||
|
it.completed = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Log.d(tag, "Start internal download to destination path ${it.finalDestinationPath} from ${it.serverUrl}")
|
||||||
|
InternalDownloadManager(fileOutputStream, internalProgressCallback).download(it.serverUrl)
|
||||||
|
currentDownloadItemParts.add(it)
|
||||||
|
} else {
|
||||||
val dlRequest = it.getDownloadRequest()
|
val dlRequest = it.getDownloadRequest()
|
||||||
val downloadId = downloadManager.enqueue(dlRequest)
|
val downloadId = downloadManager.enqueue(dlRequest)
|
||||||
it.downloadId = downloadId
|
it.downloadId = downloadId
|
||||||
|
@ -72,6 +99,7 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
||||||
currentDownloadItemParts.add(it)
|
currentDownloadItemParts.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentDownloadItemParts.size >= maxSimultaneousDownloads) {
|
if (currentDownloadItemParts.size >= maxSimultaneousDownloads) {
|
||||||
break
|
break
|
||||||
|
@ -89,15 +117,26 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
||||||
isDownloading = true
|
isDownloading = true
|
||||||
|
|
||||||
while (currentDownloadItemParts.size > 0) {
|
while (currentDownloadItemParts.size > 0) {
|
||||||
|
|
||||||
val itemParts = currentDownloadItemParts.filter { !it.isMoving }.map { it }
|
val itemParts = currentDownloadItemParts.filter { !it.isMoving }.map { it }
|
||||||
for (downloadItemPart in itemParts) {
|
for (downloadItemPart in itemParts) {
|
||||||
|
if (downloadItemPart.isInternalStorage) {
|
||||||
|
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
|
||||||
|
|
||||||
|
if (downloadItemPart.completed) {
|
||||||
|
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId }
|
||||||
|
downloadItem?.let {
|
||||||
|
checkDownloadItemFinished(it)
|
||||||
|
}
|
||||||
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val downloadCheckStatus = checkDownloadItemPart(downloadItemPart)
|
val downloadCheckStatus = checkDownloadItemPart(downloadItemPart)
|
||||||
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
|
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
|
||||||
|
|
||||||
// Will move to final destination, remove current item parts, and check if download item is finished
|
// Will move to final destination, remove current item parts, and check if download item is finished
|
||||||
handleDownloadItemPartCheck(downloadCheckStatus, downloadItemPart)
|
handleDownloadItemPartCheck(downloadCheckStatus, downloadItemPart)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delay(500)
|
delay(500)
|
||||||
|
|
||||||
|
@ -166,7 +205,6 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
||||||
Log.e(tag, "Download item part finished but download item not found ${downloadItemPart.filename}")
|
Log.e(tag, "Download item part finished but download item not found ${downloadItemPart.filename}")
|
||||||
currentDownloadItemParts.remove(downloadItemPart)
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
} else if (downloadCheckStatus == DownloadCheckStatus.Successful) {
|
} else if (downloadCheckStatus == DownloadCheckStatus.Successful) {
|
||||||
|
|
||||||
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
||||||
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}")
|
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}")
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.audiobookshelf.app.managers
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.common.net.HttpHeaders.CONTENT_LENGTH
|
||||||
|
import okhttp3.*
|
||||||
|
import java.io.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class InternalDownloadManager(outputStream:FileOutputStream, private val progressCallback: DownloadItemManager.InternalProgressCallback) : AutoCloseable {
|
||||||
|
private val tag = "InternalDownloadManager"
|
||||||
|
|
||||||
|
private val client: OkHttpClient = OkHttpClient()
|
||||||
|
private val writer = BinaryFileWriter(outputStream, progressCallback)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun download(url:String) {
|
||||||
|
val request: Request = Request.Builder().url(url).build()
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
Log.e(tag, "download URL $url FAILED")
|
||||||
|
progressCallback.onComplete(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
val responseBody: ResponseBody = response.body
|
||||||
|
?: throw IllegalStateException("Response doesn't contain a file")
|
||||||
|
|
||||||
|
val length: Long = (response.header(CONTENT_LENGTH, "1") ?: "0").toLong()
|
||||||
|
writer.write(responseBody.byteStream(), length)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun close() {
|
||||||
|
writer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BinaryFileWriter(outputStream: OutputStream, progressCallback: DownloadItemManager.InternalProgressCallback) :
|
||||||
|
AutoCloseable {
|
||||||
|
private val outputStream: OutputStream
|
||||||
|
private val progressCallback: DownloadItemManager.InternalProgressCallback
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.outputStream = outputStream
|
||||||
|
this.progressCallback = progressCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun write(inputStream: InputStream?, length: Long): Long {
|
||||||
|
BufferedInputStream(inputStream).use { input ->
|
||||||
|
val dataBuffer = ByteArray(CHUNK_SIZE)
|
||||||
|
var readBytes: Int
|
||||||
|
var totalBytes: Long = 0
|
||||||
|
while (input.read(dataBuffer).also { readBytes = it } != -1) {
|
||||||
|
totalBytes += readBytes.toLong()
|
||||||
|
outputStream.write(dataBuffer, 0, readBytes)
|
||||||
|
progressCallback.onProgress(totalBytes, (totalBytes * 100L) / length)
|
||||||
|
}
|
||||||
|
progressCallback.onComplete(false)
|
||||||
|
return totalBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun close() {
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHUNK_SIZE = 1024
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,9 @@ data class DownloadItem(
|
||||||
val media: MediaType,
|
val media: MediaType,
|
||||||
val downloadItemParts: MutableList<DownloadItemPart>
|
val downloadItemParts: MutableList<DownloadItemPart>
|
||||||
) {
|
) {
|
||||||
|
@get:JsonIgnore
|
||||||
|
val isInternalStorage get() = localFolder.id.startsWith("internal-")
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving }
|
val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving }
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,12 @@ data class DownloadItemPart(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val isInternalStorage get() = localFolderId.startsWith("internal-")
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val serverUrl get() = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getDownloadRequest(): DownloadManager.Request {
|
fun getDownloadRequest(): DownloadManager.Request {
|
||||||
val dlRequest = DownloadManager.Request(uri)
|
val dlRequest = DownloadManager.Request(uri)
|
||||||
|
|
|
@ -57,7 +57,7 @@ class AbsDownloader : Plugin() {
|
||||||
val libraryItemId = call.data.getString("libraryItemId").toString()
|
val libraryItemId = call.data.getString("libraryItemId").toString()
|
||||||
var episodeId = call.data.getString("episodeId").toString()
|
var episodeId = call.data.getString("episodeId").toString()
|
||||||
if (episodeId == "null") episodeId = ""
|
if (episodeId == "null") episodeId = ""
|
||||||
val localFolderId = call.data.getString("localFolderId").toString()
|
var localFolderId = call.data.getString("localFolderId", "").toString()
|
||||||
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId")
|
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId")
|
||||||
|
|
||||||
val downloadId = if (episodeId.isEmpty()) libraryItemId else "$libraryItemId-$episodeId"
|
val downloadId = if (episodeId.isEmpty()) libraryItemId else "$libraryItemId-$episodeId"
|
||||||
|
@ -72,9 +72,18 @@ class AbsDownloader : Plugin() {
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "Got library item from server ${libraryItem.id}")
|
Log.d(tag, "Got library item from server ${libraryItem.id}")
|
||||||
|
|
||||||
val localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
|
if (localFolderId == "") {
|
||||||
if (localFolder != null) {
|
localFolderId = "internal-${libraryItem.mediaType}"
|
||||||
|
}
|
||||||
|
var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
|
||||||
|
|
||||||
|
if (localFolder == null && localFolderId.startsWith("internal-")) {
|
||||||
|
Log.d(tag, "Creating new App Storage internal LocalFolder $localFolderId")
|
||||||
|
localFolder = LocalFolder(localFolderId, "Internal App Storage", "", "", "", "", "internal", libraryItem.mediaType)
|
||||||
|
DeviceManager.dbManager.saveLocalFolder(localFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localFolder != null) {
|
||||||
if (episodeId.isNotEmpty() && libraryItem.mediaType != "podcast") {
|
if (episodeId.isNotEmpty() && libraryItem.mediaType != "podcast") {
|
||||||
Log.e(tag, "Library item is not a podcast but episode was requested")
|
Log.e(tag, "Library item is not a podcast but episode was requested")
|
||||||
call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
|
call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
|
||||||
|
@ -127,7 +136,9 @@ class AbsDownloader : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
|
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
|
||||||
val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
val isInternal = localFolder.id.startsWith("internal-")
|
||||||
|
|
||||||
|
val tempFolderPath = if (isInternal) "${mainActivity.filesDir}/downloads/${libraryItem.id}" else mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
|
||||||
Log.d(tag, "downloadCacheDirectory=$tempFolderPath")
|
Log.d(tag, "downloadCacheDirectory=$tempFolderPath")
|
||||||
|
|
||||||
|
@ -138,7 +149,7 @@ class AbsDownloader : Plugin() {
|
||||||
val tracks = libraryItem.media.getAudioTracks()
|
val tracks = libraryItem.media.getAudioTracks()
|
||||||
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
||||||
val itemSubfolder = "$bookAuthor/$bookTitle"
|
val itemSubfolder = "$bookAuthor/$bookTitle"
|
||||||
val itemFolderPath = "${localFolder.absolutePath}/$itemSubfolder"
|
val itemFolderPath = if (isInternal) "$tempFolderPath" else "${localFolder.absolutePath}/$itemSubfolder"
|
||||||
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
|
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
|
||||||
|
|
||||||
val book = libraryItem.media as Book
|
val book = libraryItem.media as Book
|
||||||
|
|
|
@ -227,10 +227,18 @@ class AbsFileSystem : Plugin() {
|
||||||
val contentUrl = call.data.getString("contentUrl", "").toString()
|
val contentUrl = call.data.getString("contentUrl", "").toString()
|
||||||
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
|
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
|
||||||
|
|
||||||
var subfolderPathToDelete = ""
|
|
||||||
|
|
||||||
// Check if should delete subfolder
|
// Check if should delete subfolder
|
||||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
|
||||||
|
val success: Boolean
|
||||||
|
|
||||||
|
// If internal library item use File to delete
|
||||||
|
if (localLibraryItem?.folderId?.startsWith("internal-") == true) {
|
||||||
|
Log.d(tag, "Deleting internal library item at absolutePath $absolutePath")
|
||||||
|
val file = File(absolutePath)
|
||||||
|
success = file.deleteRecursively()
|
||||||
|
} else {
|
||||||
|
var subfolderPathToDelete = ""
|
||||||
localLibraryItem?.folderId?.let { folderId ->
|
localLibraryItem?.folderId?.let { folderId ->
|
||||||
val folder = DeviceManager.dbManager.getLocalFolder(folderId)
|
val folder = DeviceManager.dbManager.getLocalFolder(folderId)
|
||||||
folder?.absolutePath?.let { folderPath ->
|
folder?.absolutePath?.let { folderPath ->
|
||||||
|
@ -250,13 +258,14 @@ class AbsFileSystem : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||||
val success = docfile?.delete() == true
|
success = docfile?.delete() == true
|
||||||
|
|
||||||
if (subfolderPathToDelete != "") {
|
if (subfolderPathToDelete != "") {
|
||||||
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")
|
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")
|
||||||
val docfilesub = DocumentFileCompat.fromFullPath(mainActivity, subfolderPathToDelete)
|
val docfilesub = DocumentFileCompat.fromFullPath(mainActivity, subfolderPathToDelete)
|
||||||
docfilesub?.delete()
|
docfilesub?.delete()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
||||||
|
|
|
@ -0,0 +1,300 @@
|
||||||
|
package com.audiobookshelf.app.plugins
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
|
import com.anggrayudi.storage.callback.FolderPickerCallback
|
||||||
|
import com.anggrayudi.storage.callback.StorageAccessCallback
|
||||||
|
import com.anggrayudi.storage.file.*
|
||||||
|
import com.audiobookshelf.app.MainActivity
|
||||||
|
import com.audiobookshelf.app.data.LocalFolder
|
||||||
|
import com.audiobookshelf.app.data.LocalLibraryItem
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.device.FolderScanner
|
||||||
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.getcapacitor.*
|
||||||
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@CapacitorPlugin(name = "AbsFileSystem")
|
||||||
|
class AbsFileSystem : Plugin() {
|
||||||
|
private val TAG = "AbsFileSystem"
|
||||||
|
private val tag = "AbsFileSystem"
|
||||||
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
|
lateinit var mainActivity: MainActivity
|
||||||
|
|
||||||
|
override fun load() {
|
||||||
|
mainActivity = (activity as MainActivity)
|
||||||
|
|
||||||
|
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback {
|
||||||
|
override fun onRootPathNotSelected(
|
||||||
|
requestCode: Int,
|
||||||
|
rootPath: String,
|
||||||
|
uri: Uri,
|
||||||
|
selectedStorageType: StorageType,
|
||||||
|
expectedStorageType: StorageType
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCanceledByUser(requestCode: Int) {
|
||||||
|
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) {
|
||||||
|
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||||
|
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
|
||||||
|
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun selectFolder(call: PluginCall) {
|
||||||
|
val mediaType = call.data.getString("mediaType", "book").toString()
|
||||||
|
val REQUEST_CODE_SELECT_FOLDER = 6
|
||||||
|
val REQUEST_CODE_SDCARD_ACCESS = 7
|
||||||
|
|
||||||
|
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
|
||||||
|
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
|
||||||
|
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
|
||||||
|
val absolutePath = folder.getAbsolutePath(activity)
|
||||||
|
val storageType = folder.getStorageType(activity)
|
||||||
|
val simplePath = folder.getSimplePath(activity)
|
||||||
|
val basePath = folder.getBasePath(activity)
|
||||||
|
val folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
|
||||||
|
|
||||||
|
val localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType)
|
||||||
|
|
||||||
|
DeviceManager.dbManager.saveLocalFolder(localFolder)
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStorageAccessDenied(
|
||||||
|
requestCode: Int,
|
||||||
|
folder: DocumentFile?,
|
||||||
|
storageType: StorageType,
|
||||||
|
storageId: String
|
||||||
|
) {
|
||||||
|
Log.e(tag, "Storage Access Denied ${folder?.getAbsolutePath(mainActivity)}")
|
||||||
|
|
||||||
|
val jsobj = JSObject()
|
||||||
|
if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
|
||||||
|
|
||||||
|
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
|
||||||
|
builder.setMessage(
|
||||||
|
"You have no write access to this storage, thus selecting this folder is useless." +
|
||||||
|
"\nWould you like to grant access to this folder?")
|
||||||
|
builder.setNegativeButton("Dont Allow") { _, _ ->
|
||||||
|
run {
|
||||||
|
jsobj.put("error", "User Canceled, Access Denied")
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, initialPath = FileFullPath(mainActivity, storageId, "")) }
|
||||||
|
builder.show()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
|
||||||
|
jsobj.put("error", "Access Denied")
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||||
|
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("error", "Permission Denied")
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
@PluginMethod
|
||||||
|
fun requestStoragePermission(call: PluginCall) {
|
||||||
|
Log.d(TAG, "Request Storage Permissions")
|
||||||
|
mainActivity.storageHelper.requestStorageAccess()
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun checkStoragePermission(call: PluginCall) {
|
||||||
|
val res: Boolean
|
||||||
|
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||||
|
res = SimpleStorage.hasStoragePermission(context)
|
||||||
|
Log.d(TAG, "checkStoragePermission: Check Storage Access $res")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "checkStoragePermission: Has permission on Android 10 or up")
|
||||||
|
res = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("value", res)
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun checkFolderPermissions(call: PluginCall) {
|
||||||
|
val folderUrl = call.data.getString("folderUrl", "").toString()
|
||||||
|
Log.d(TAG, "Check Folder Permissions for $folderUrl")
|
||||||
|
|
||||||
|
val hasAccess = SimpleStorage.hasStorageAccess(context,folderUrl,true)
|
||||||
|
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("value", hasAccess)
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun scanFolder(call: PluginCall) {
|
||||||
|
val folderId = call.data.getString("folderId", "").toString()
|
||||||
|
val forceAudioProbe = call.data.getBoolean("forceAudioProbe")
|
||||||
|
Log.d(TAG, "Scan Folder $folderId | Force Audio Probe $forceAudioProbe")
|
||||||
|
|
||||||
|
val folder: LocalFolder? = DeviceManager.dbManager.getLocalFolder(folderId)
|
||||||
|
folder?.let {
|
||||||
|
val folderScanner = FolderScanner(context)
|
||||||
|
val folderScanResult = folderScanner.scanForMediaItems(it, forceAudioProbe)
|
||||||
|
if (folderScanResult == null) {
|
||||||
|
Log.d(TAG, "NO Scan DATA")
|
||||||
|
return call.resolve(JSObject())
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(folderScanResult)}")
|
||||||
|
return call.resolve(JSObject(jacksonMapper.writeValueAsString(folderScanResult)))
|
||||||
|
}
|
||||||
|
} ?: call.resolve(JSObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun removeFolder(call: PluginCall) {
|
||||||
|
val folderId = call.data.getString("folderId", "").toString()
|
||||||
|
DeviceManager.dbManager.removeLocalFolder(folderId)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun removeLocalLibraryItem(call: PluginCall) {
|
||||||
|
val localLibraryItemId = call.data.getString("localLibraryItemId", "").toString()
|
||||||
|
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun scanLocalLibraryItem(call: PluginCall) {
|
||||||
|
val localLibraryItemId = call.data.getString("localLibraryItemId", "").toString()
|
||||||
|
val forceAudioProbe = call.data.getBoolean("forceAudioProbe")
|
||||||
|
Log.d(TAG, "Scan Local library item $localLibraryItemId | Force Audio Probe $forceAudioProbe")
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val localLibraryItem: LocalLibraryItem? = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
localLibraryItem?.let {
|
||||||
|
val folderScanner = FolderScanner(context)
|
||||||
|
val scanResult = folderScanner.scanLocalLibraryItem(it, forceAudioProbe)
|
||||||
|
if (scanResult == null) {
|
||||||
|
Log.d(TAG, "NO Scan DATA")
|
||||||
|
call.resolve(JSObject())
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(scanResult)}")
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(scanResult)))
|
||||||
|
}
|
||||||
|
} ?: call.resolve(JSObject())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun deleteItem(call: PluginCall) {
|
||||||
|
val localLibraryItemId = call.data.getString("id", "").toString()
|
||||||
|
val absolutePath = call.data.getString("absolutePath", "").toString()
|
||||||
|
val contentUrl = call.data.getString("contentUrl", "").toString()
|
||||||
|
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
|
||||||
|
|
||||||
|
// Check if should delete subfolder
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
|
||||||
|
val success: Boolean
|
||||||
|
|
||||||
|
// If internal library item use File to delete
|
||||||
|
if (localLibraryItem?.folderId?.startsWith("internal-") == true) {
|
||||||
|
Log.d(tag, "Deleting internal library item at absolutePath $absolutePath")
|
||||||
|
val file = File(absolutePath)
|
||||||
|
success = file.deleteRecursively()
|
||||||
|
} else {
|
||||||
|
var subfolderPathToDelete = ""
|
||||||
|
localLibraryItem?.folderId?.let { folderId ->
|
||||||
|
val folder = DeviceManager.dbManager.getLocalFolder(folderId)
|
||||||
|
folder?.absolutePath?.let { folderPath ->
|
||||||
|
val splitAbsolutePath = absolutePath.split("/")
|
||||||
|
val fullSubDir = splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/")
|
||||||
|
if (fullSubDir != folderPath) {
|
||||||
|
val subdirHasAnItem = DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem ->
|
||||||
|
if (_localLibraryItem.id == localLibraryItemId) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
_localLibraryItem.absolutePath.startsWith(fullSubDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subfolderPathToDelete = if (subdirHasAnItem) "" else fullSubDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||||
|
success = docfile?.delete() == true
|
||||||
|
|
||||||
|
if (subfolderPathToDelete != "") {
|
||||||
|
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")
|
||||||
|
val docfilesub = DocumentFileCompat.fromFullPath(mainActivity, subfolderPathToDelete)
|
||||||
|
docfilesub?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
||||||
|
}
|
||||||
|
call.resolve(JSObject("{\"success\":$success}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun deleteTrackFromItem(call: PluginCall) {
|
||||||
|
val localLibraryItemId = call.data.getString("id", "").toString()
|
||||||
|
val trackLocalFileId = call.data.getString("trackLocalFileId", "").toString()
|
||||||
|
val contentUrl = call.data.getString("trackContentUrl", "").toString()
|
||||||
|
Log.d(tag, "deleteTrackFromItem $contentUrl")
|
||||||
|
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
if (localLibraryItem == null) {
|
||||||
|
Log.e(tag, "deleteTrackFromItem: LLI does not exist $localLibraryItemId")
|
||||||
|
return call.resolve(JSObject("{\"success\":false}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||||
|
val success = docfile?.delete() == true
|
||||||
|
if (success) {
|
||||||
|
localLibraryItem.media.removeAudioTrack(trackLocalFileId)
|
||||||
|
localLibraryItem.removeLocalFile(trackLocalFileId)
|
||||||
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||||
|
} else {
|
||||||
|
call.resolve(JSObject("{\"success\":false}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -272,7 +272,7 @@ export default {
|
||||||
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
|
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
|
||||||
},
|
},
|
||||||
localLibraryItemCoverSrc() {
|
localLibraryItemCoverSrc() {
|
||||||
var localItemCover = this.localLibraryItem ? this.localLibraryItem.coverContentUrl : null
|
var localItemCover = this.localLibraryItem?.coverContentUrl || null
|
||||||
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
|
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,8 +10,9 @@
|
||||||
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="folder in localFolders">
|
<template v-for="folder in localFolders">
|
||||||
<li :key="folder.id" :id="`folder-${folder.id}`" class="text-gray-50 select-none relative py-4" role="option" @click="clickedOption(folder)">
|
<li :key="folder.id" :id="`folder-${folder.id}`" class="text-gray-50 select-none relative py-5" role="option" @click="clickedOption(folder)">
|
||||||
<div class="relative flex items-center pl-3" style="padding-right: 4.5rem">
|
<div class="relative flex items-center pl-3" style="padding-right: 4.5rem">
|
||||||
|
<span class="material-icons-outlined text-xl mr-2 text-white text-opacity-80">folder</span>
|
||||||
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ folder.name }}</p>
|
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ folder.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -54,6 +55,14 @@ export default {
|
||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
var localFolders = (await this.$db.getLocalFolders()) || []
|
var localFolders = (await this.$db.getLocalFolders()) || []
|
||||||
|
|
||||||
|
if (!localFolders.some((lf) => lf.id === `internal-${this.mediaType}`)) {
|
||||||
|
localFolders.push({
|
||||||
|
id: `internal-${this.mediaType}`,
|
||||||
|
name: 'Internal App Storage',
|
||||||
|
mediaType: this.mediaType
|
||||||
|
})
|
||||||
|
}
|
||||||
this.localFolders = localFolders.filter((lf) => lf.mediaType == this.mediaType)
|
this.localFolders = localFolders.filter((lf) => lf.mediaType == this.mediaType)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -632,16 +632,26 @@ export default {
|
||||||
})
|
})
|
||||||
console.log('Folders with media type', this.mediaType, foldersWithMediaType.length)
|
console.log('Folders with media type', this.mediaType, foldersWithMediaType.length)
|
||||||
if (!foldersWithMediaType.length) {
|
if (!foldersWithMediaType.length) {
|
||||||
// No local folders or no local folders with this media type
|
localFolder = {
|
||||||
localFolder = await this.selectFolder()
|
id: `internal-${this.mediaType}`,
|
||||||
} else if (foldersWithMediaType.length == 1) {
|
name: 'App Storage',
|
||||||
console.log('Only 1 local folder with this media type - auto select it')
|
mediaType: this.mediaType
|
||||||
localFolder = foldersWithMediaType[0]
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Multiple folders with media type')
|
|
||||||
this.showSelectLocalFolder = true
|
this.showSelectLocalFolder = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// if (!foldersWithMediaType.length) {
|
||||||
|
// // No local folders or no local folders with this media type
|
||||||
|
// localFolder = await this.selectFolder()
|
||||||
|
// } else if (foldersWithMediaType.length == 1) {
|
||||||
|
// console.log('Only 1 local folder with this media type - auto select it')
|
||||||
|
// localFolder = foldersWithMediaType[0]
|
||||||
|
// } else {
|
||||||
|
// console.log('Multiple folders with media type')
|
||||||
|
// this.showSelectLocalFolder = true
|
||||||
|
// return
|
||||||
|
// }
|
||||||
if (!localFolder) {
|
if (!localFolder) {
|
||||||
return this.$toast.error('Invalid download folder')
|
return this.$toast.error('Invalid download folder')
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<p class="text-base font-semibold">Folder: {{ folderName }}</p>
|
<p class="text-base font-semibold">Folder: {{ folderName }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<span class="material-icons" @click="showDialog = true">more_vert</span>
|
<span v-if="dialogItems.length" class="material-icons" @click="showDialog = true">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm mb-4 text-white text-opacity-60">Media Type: {{ mediaType }}</p>
|
<p class="text-sm mb-4 text-white text-opacity-60">Media Type: {{ mediaType }}</p>
|
||||||
|
@ -58,26 +58,35 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
folderName() {
|
folderName() {
|
||||||
return this.folder ? this.folder.name : null
|
return this.folder?.name || null
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.folder ? this.folder.mediaType : null
|
return this.folder?.mediaType
|
||||||
|
},
|
||||||
|
isInternalStorage() {
|
||||||
|
return this.folder?.id.startsWith('internal-')
|
||||||
},
|
},
|
||||||
dialogItems() {
|
dialogItems() {
|
||||||
return [
|
if (this.isInternalStorage) return []
|
||||||
|
const items = [
|
||||||
{
|
{
|
||||||
text: 'Scan',
|
text: 'Scan',
|
||||||
value: 'scan'
|
value: 'scan'
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
|
|
||||||
|
if (this.localLibraryItems.length) {
|
||||||
|
items.push({
|
||||||
text: 'Force Re-Scan',
|
text: 'Force Re-Scan',
|
||||||
value: 'rescan'
|
value: 'rescan'
|
||||||
},
|
})
|
||||||
{
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
text: 'Remove',
|
text: 'Remove',
|
||||||
value: 'remove'
|
value: 'remove'
|
||||||
}
|
})
|
||||||
].filter((i) => i.value != 'rescan' || this.localLibraryItems.length) // Filter out rescan if there are no local library items
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -181,6 +181,9 @@ export default {
|
||||||
folderName() {
|
folderName() {
|
||||||
return this.folder?.name
|
return this.folder?.name
|
||||||
},
|
},
|
||||||
|
isInternalStorage() {
|
||||||
|
return this.folderId?.startsWith('internal-')
|
||||||
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.localLibraryItem?.mediaType
|
return this.localLibraryItem?.mediaType
|
||||||
},
|
},
|
||||||
|
@ -227,7 +230,7 @@ export default {
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
var options = []
|
var options = []
|
||||||
if (!this.isIos) {
|
if (!this.isIos && !this.isInternalStorage) {
|
||||||
options.push({ text: 'Scan', value: 'scan' })
|
options.push({ text: 'Scan', value: 'scan' })
|
||||||
options.push({ text: 'Force Re-Scan', value: 'rescan' })
|
options.push({ text: 'Force Re-Scan', value: 'rescan' })
|
||||||
options.push({ text: 'Remove', value: 'remove' })
|
options.push({ text: 'Remove', value: 'remove' })
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue