mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-12 15:04:43 +02:00
Media folder management page, android media folder scanner
This commit is contained in:
parent
a259883979
commit
94b9dbb8b3
12 changed files with 456 additions and 339 deletions
|
@ -65,8 +65,9 @@ class StorageManager : Plugin() {
|
|||
var absolutePath = folder.getAbsolutePath(activity)
|
||||
var storageType = folder.getStorageType(activity)
|
||||
var simplePath = folder.getSimplePath(activity)
|
||||
var folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
|
||||
|
||||
var localFolder = LocalFolder(folder.id, folder.name, folder.uri.toString(),absolutePath, simplePath, storageType.toString(), mediaType)
|
||||
var localFolder = LocalFolder(folderId, folder.name, folder.uri.toString(),absolutePath, simplePath, storageType.toString(), mediaType)
|
||||
|
||||
DeviceManager.dbManager.saveLocalFolder(localFolder)
|
||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localFolder)))
|
||||
|
@ -127,13 +128,15 @@ class StorageManager : Plugin() {
|
|||
}
|
||||
|
||||
@PluginMethod
|
||||
fun searchFolder(call: PluginCall) {
|
||||
fun scanFolder(call: PluginCall) {
|
||||
var folderId = call.data.getString("folderId", "").toString()
|
||||
var folder: LocalFolder? = DeviceManager.dbManager.loadLocalFolder(folderId)
|
||||
var forceAudioProbe = call.data.getBoolean("forceAudioProbe")
|
||||
Log.d(TAG, "Scan Folder $folderId | Force Audio Probe $forceAudioProbe")
|
||||
|
||||
var folder: LocalFolder? = DeviceManager.dbManager.getLocalFolder(folderId)
|
||||
folder?.let {
|
||||
Log.d(TAG, "Searching folder ${it.contentUrl}")
|
||||
var folderScanner = FolderScanner(context)
|
||||
var folderScanResult = folderScanner.scanForMediaItems(it.contentUrl, it.mediaType)
|
||||
var folderScanResult = folderScanner.scanForMediaItems(it, forceAudioProbe)
|
||||
if (folderScanResult == null) {
|
||||
Log.d(TAG, "NO Scan DATA")
|
||||
return call.resolve(JSObject())
|
||||
|
@ -141,65 +144,15 @@ class StorageManager : Plugin() {
|
|||
Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(folderScanResult)}")
|
||||
return call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(folderScanResult)))
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Folder not found $folderId")
|
||||
call.resolve(JSObject())
|
||||
|
||||
//
|
||||
// var df: DocumentFile? = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
||||
//
|
||||
// if (df == null) {
|
||||
// Log.e(TAG, "Folder Doc File Invalid $folderUrl")
|
||||
// var jsobj = JSObject()
|
||||
// jsobj.put("folders", JSArray())
|
||||
// jsobj.put("files", JSArray())
|
||||
// call.resolve(jsobj)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// Log.d(TAG, "Folder as DF ${df.isDirectory} | ${df.getSimplePath(context)} | ${df.getBasePath(context)} | ${df.name}")
|
||||
//
|
||||
// var mediaFolders = mutableListOf<MediaFolder>()
|
||||
// var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||
//
|
||||
// foldersFound.forEach {
|
||||
// Log.d(TAG, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(context)} | URI: ${it.uri}")
|
||||
// var folderName = it.name ?: ""
|
||||
// var mediaFiles = mutableListOf<MediaFile>()
|
||||
//
|
||||
// var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
// filesInFolder.forEach { it2 ->
|
||||
// var mimeType = it2?.mimeType ?: ""
|
||||
// var filename = it2?.name ?: ""
|
||||
// var isAudio = mimeType.startsWith("audio")
|
||||
// Log.d(TAG, "Found $mimeType file $filename in folder $folderName")
|
||||
// var imageFile = MediaFile(it2.uri, filename, it2.getSimplePath(context), it2.length(), mimeType, isAudio)
|
||||
// mediaFiles.add(imageFile)
|
||||
// }
|
||||
// if (mediaFiles.size > 0) {
|
||||
// mediaFolders.add(MediaFolder(it.uri, folderName, it.getSimplePath(context), mediaFiles))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Files in root dir
|
||||
// var rootMediaFiles = mutableListOf<MediaFile>()
|
||||
// var mediaFilesFound:List<DocumentFile> = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
// mediaFilesFound.forEach {
|
||||
// Log.d(TAG, "Folder Root File Found ${it.name} | ${it.getSimplePath(context)} | URI: ${it.uri} | ${it.mimeType}")
|
||||
// var mimeType = it?.mimeType ?: ""
|
||||
// var filename = it?.name ?: ""
|
||||
// var isAudio = mimeType.startsWith("audio")
|
||||
// Log.d(TAG, "Found $mimeType file $filename in root folder")
|
||||
// var imageFile = MediaFile(it.uri, filename, it.getSimplePath(context), it.length(), mimeType, isAudio)
|
||||
// rootMediaFiles.add(imageFile)
|
||||
// }
|
||||
//
|
||||
// var jsobj = JSObject()
|
||||
// jsobj.put("folders", mediaFolders.map{ it.toJSObject() })
|
||||
// jsobj.put("files", rootMediaFiles.map{ it.toJSObject() })
|
||||
// call.resolve(jsobj)
|
||||
} ?: call.resolve(JSObject())
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun removeFolder(call: PluginCall) {
|
||||
var folderId = call.data.getString("folderId", "").toString()
|
||||
DeviceManager.dbManager.removeLocalFolder(folderId)
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun delete(call: PluginCall) {
|
||||
|
|
|
@ -151,5 +151,6 @@ data class AudioTrack(
|
|||
var contentUrl:String,
|
||||
var mimeType:String,
|
||||
var isLocal:Boolean,
|
||||
var localFileId:String?,
|
||||
var audioProbeResult:AudioProbeResult?
|
||||
)
|
||||
|
|
|
@ -8,6 +8,9 @@ import com.getcapacitor.PluginCall
|
|||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import io.paperdb.Paper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
||||
@CapacitorPlugin(name = "DbManager")
|
||||
|
@ -33,21 +36,34 @@ class DbManager : Plugin() {
|
|||
return localMediaItems
|
||||
}
|
||||
|
||||
fun getLocalMediaItemsInFolder(folderId:String):List<LocalMediaItem> {
|
||||
var localMediaItems = loadLocalMediaItems()
|
||||
return localMediaItems.filter {
|
||||
it.folderId == folderId
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLocalMediaItem(localMediaItemId:String):LocalMediaItem? {
|
||||
return Paper.book("localMediaItems").read(localMediaItemId)
|
||||
}
|
||||
|
||||
fun removeLocalMediaItem(localMediaItemId:String) {
|
||||
Paper.book("localMediaItems").delete(localMediaItemId)
|
||||
}
|
||||
|
||||
fun saveLocalMediaItems(localMediaItems:List<LocalMediaItem>) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
localMediaItems.map {
|
||||
Paper.book("localMediaItems").write(it.id, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLocalFolder(localFolder:LocalFolder) {
|
||||
Paper.book("localFolders").write(localFolder.id,localFolder)
|
||||
}
|
||||
|
||||
fun loadLocalFolder(folderId:String):LocalFolder? {
|
||||
fun getLocalFolder(folderId:String):LocalFolder? {
|
||||
return Paper.book("localFolders").read(folderId)
|
||||
}
|
||||
|
||||
|
@ -62,6 +78,14 @@ class DbManager : Plugin() {
|
|||
return localFolders
|
||||
}
|
||||
|
||||
fun removeLocalFolder(folderId:String) {
|
||||
var localMediaItems = getLocalMediaItemsInFolder(folderId)
|
||||
localMediaItems.forEach {
|
||||
Paper.book("localMediaItems").delete(it.id)
|
||||
}
|
||||
Paper.book("localFolders").delete(folderId)
|
||||
}
|
||||
|
||||
fun saveObject(db:String, key:String, value:JSONObject) {
|
||||
Log.d(tag, "Saving Object $key ${value.toString()}")
|
||||
Paper.book(db).write(key, value)
|
||||
|
@ -78,6 +102,8 @@ class DbManager : Plugin() {
|
|||
var db = call.getString("db", "").toString()
|
||||
var key = call.getString("key", "").toString()
|
||||
var value = call.getObject("value")
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
if (db == "" || key == "" || value == null) {
|
||||
Log.d(tag, "saveFromWebview Invalid key/value")
|
||||
} else {
|
||||
|
@ -86,6 +112,7 @@ class DbManager : Plugin() {
|
|||
}
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun loadFromWebview(call:PluginCall) {
|
||||
|
@ -102,24 +129,36 @@ class DbManager : Plugin() {
|
|||
}
|
||||
|
||||
@PluginMethod
|
||||
fun localFoldersFromWebView(call:PluginCall) {
|
||||
fun getLocalFolders_WV(call:PluginCall) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var folders = getAllLocalFolders()
|
||||
var folderObjArray = jacksonObjectMapper().writeValueAsString(folders)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("folders", folderObjArray)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun loadMediaItemsInFolder(call:PluginCall) {
|
||||
var folderId = call.getString("folderId", "").toString()
|
||||
var localMediaItems = loadLocalMediaItems().filter {
|
||||
it.folderId == folderId
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getLocalFolder_WV(call:PluginCall) {
|
||||
var folderId = call.getString("folderId", "").toString()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
getLocalFolder(folderId)?.let {
|
||||
var folderObj = jacksonObjectMapper().writeValueAsString(it)
|
||||
call.resolve(JSObject(folderObj))
|
||||
} ?: call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getLocalMediaItemsInFolder_WV(call:PluginCall) {
|
||||
var folderId = call.getString("folderId", "").toString()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var localMediaItems = getLocalMediaItemsInFolder(folderId)
|
||||
var mediaItemsArray = jacksonObjectMapper().writeValueAsString(localMediaItems)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("localMediaItems", mediaItemsArray)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
data class FolderScanResult(
|
||||
val name:String?,
|
||||
val absolutePath:String,
|
||||
val mediaType:String,
|
||||
val contentUrl:String,
|
||||
val localMediaItems:MutableList<LocalMediaItem>,
|
||||
var itemsAdded:Int,
|
||||
var itemsUpdated:Int,
|
||||
var itemsRemoved:Int,
|
||||
var itemsUpToDate:Int,
|
||||
val localFolder:LocalFolder,
|
||||
val localMediaItems:List<LocalMediaItem>,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.audiobookshelf.app.device
|
||||
|
||||
import android.util.Log
|
||||
import com.anggrayudi.storage.file.id
|
||||
import com.audiobookshelf.app.data.DbManager
|
||||
import com.audiobookshelf.app.data.DeviceData
|
||||
import com.audiobookshelf.app.data.ServerConfig
|
||||
|
@ -17,4 +18,8 @@ object DeviceManager {
|
|||
init {
|
||||
Log.d(tag, "Device Manager Singleton invoked")
|
||||
}
|
||||
|
||||
fun getBase64Id(id:String):String {
|
||||
return android.util.Base64.encodeToString(id.toByteArray(), android.util.Base64.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import android.net.Uri
|
|||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.*
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import com.arthenica.ffmpegkit.Level
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
|
@ -13,25 +15,55 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
|||
class FolderScanner(var ctx: Context) {
|
||||
private val tag = "FolderScanner"
|
||||
|
||||
fun scanForMediaItems(folderUrl: String, mediaType:String):FolderScanResult? {
|
||||
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(folderUrl))
|
||||
// TODO: CLEAN this monster! Divide into bite-size methods
|
||||
fun scanForMediaItems(localFolder:LocalFolder, forceAudioProbe:Boolean):FolderScanResult? {
|
||||
FFmpegKitConfig.enableLogCallback { log ->
|
||||
if (log.level != Level.AV_LOG_STDERR) { // STDERR is filled with junk
|
||||
Log.d(tag, "FFmpeg-Kit Log: (${log.level}) ${log.message}")
|
||||
}
|
||||
}
|
||||
|
||||
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localFolder.contentUrl))
|
||||
|
||||
if (df == null) {
|
||||
Log.e(tag, "Folder Doc File Invalid $folderUrl")
|
||||
Log.e(tag, "Folder Doc File Invalid $localFolder.contentUrl")
|
||||
return null
|
||||
}
|
||||
var folderName = df.name ?: ""
|
||||
var folderPath = df.getAbsolutePath(ctx)
|
||||
var folderUrl = df.uri.toString()
|
||||
var folderId = df.id
|
||||
|
||||
var mediaItemsUpdated = 0
|
||||
var mediaItemsAdded = 0
|
||||
var mediaItemsRemoved = 0
|
||||
var mediaItemsUpToDate = 0
|
||||
|
||||
// Search for files in media item folder
|
||||
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||
|
||||
// Match folders found with media items already saved in db
|
||||
var existingMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(localFolder.id)
|
||||
|
||||
// Remove existing items no longer there
|
||||
existingMediaItems = existingMediaItems.filter { lmi ->
|
||||
var fileFound = foldersFound.find { f -> lmi.id == DeviceManager.getBase64Id(f.id) }
|
||||
if (fileFound == null) {
|
||||
Log.d(tag, "Existing media item is no longer in file system ${lmi.name}")
|
||||
DeviceManager.dbManager.removeLocalMediaItem(lmi.id)
|
||||
mediaItemsRemoved++
|
||||
}
|
||||
fileFound != null
|
||||
}
|
||||
|
||||
var mediaItems = mutableListOf<LocalMediaItem>()
|
||||
|
||||
foldersFound.forEach {
|
||||
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
|
||||
|
||||
var itemFolderName = it.name ?: ""
|
||||
var itemId = DeviceManager.getBase64Id(it.id)
|
||||
|
||||
var existingMediaItem = existingMediaItems.find { emi -> emi.id == itemId }
|
||||
var existingLocalFiles = existingMediaItem?.localFiles ?: mutableListOf()
|
||||
var existingAudioTracks = existingMediaItem?.audioTracks ?: mutableListOf()
|
||||
var isNewOrUpdated = existingMediaItem == null
|
||||
|
||||
var audioTracks = mutableListOf<AudioTrack>()
|
||||
var localFiles = mutableListOf<LocalFile>()
|
||||
|
@ -40,53 +72,119 @@ class FolderScanner(var ctx: Context) {
|
|||
var coverPath:String? = null
|
||||
|
||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
filesInFolder.forEach { it2 ->
|
||||
var mimeType = it2?.mimeType ?: ""
|
||||
var filename = it2?.name ?: ""
|
||||
|
||||
var existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||
}
|
||||
if (existingLocalFilesRemoved.isNotEmpty()) {
|
||||
Log.d(tag, "${existingLocalFilesRemoved.size} Local files were removed from local media item ${existingMediaItem?.name}")
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
|
||||
filesInFolder.forEach { file ->
|
||||
var mimeType = file?.mimeType ?: ""
|
||||
var filename = file?.name ?: ""
|
||||
var isAudio = mimeType.startsWith("audio")
|
||||
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
||||
|
||||
var localFile = LocalFile(it2.id,it2.name,it2.uri.toString(),it2.getAbsolutePath(ctx),it2.getSimplePath(ctx),it2.mimeType,it2.length())
|
||||
var localFileId = DeviceManager.getBase64Id(file.id)
|
||||
|
||||
var localFile = LocalFile(localFileId,filename,file.uri.toString(),file.getAbsolutePath(ctx),file.getSimplePath(ctx),mimeType,file.length())
|
||||
localFiles.add(localFile)
|
||||
|
||||
Log.d(tag, "File attributes Id:${it2.id}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${it2.isDownloadsDocument}")
|
||||
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
||||
|
||||
if (isAudio) {
|
||||
var audioTrackToAdd:AudioTrack? = null
|
||||
|
||||
var existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
||||
if (existingAudioTrack != null) { // Update existing audio track
|
||||
if (existingAudioTrack.index != index) {
|
||||
Log.d(tag, "Updating Audio track index from ${existingAudioTrack.index} to $index")
|
||||
existingAudioTrack.index = index
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
if (existingAudioTrack.startOffset != startOffset) {
|
||||
Log.d(tag, "Updating Audio track startOffset ${existingAudioTrack.startOffset} to $startOffset")
|
||||
existingAudioTrack.startOffset = startOffset
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (existingAudioTrack == null || forceAudioProbe) {
|
||||
Log.d(tag, "Scanning Audio File Path ${localFile.absolutePath}")
|
||||
|
||||
// TODO: Make asynchronous
|
||||
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||
var sessionData = session.output
|
||||
Log.d(tag, "AFTER FFPROBE STRING $sessionData")
|
||||
|
||||
val mapper = jacksonObjectMapper()
|
||||
val audioProbeResult = mapper.readValue<AudioProbeResult>(sessionData)
|
||||
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||
|
||||
var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, true, audioProbeResult)
|
||||
audioTracks.add(track)
|
||||
startOffset += audioProbeResult.duration
|
||||
if (existingAudioTrack != null) {
|
||||
// Update audio probe data on existing audio track
|
||||
existingAudioTrack.audioProbeResult = audioProbeResult
|
||||
audioTrackToAdd = existingAudioTrack
|
||||
} else {
|
||||
// Create new audio track
|
||||
var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, true, localFileId, audioProbeResult)
|
||||
audioTrackToAdd = track
|
||||
}
|
||||
|
||||
startOffset += audioProbeResult.duration
|
||||
index++
|
||||
isNewOrUpdated = true
|
||||
} else {
|
||||
audioTrackToAdd = existingAudioTrack
|
||||
}
|
||||
|
||||
startOffset += audioTrackToAdd.duration
|
||||
index++
|
||||
audioTracks.add(audioTrackToAdd)
|
||||
} else {
|
||||
var existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
||||
|
||||
if (existingLocalFile == null) {
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
if (existingMediaItem != null && existingMediaItem.coverPath == null) {
|
||||
// Existing media item did not have a cover - cover found on scan
|
||||
isNewOrUpdated = true
|
||||
}
|
||||
|
||||
// First image file use as cover path
|
||||
if (coverPath == null) {
|
||||
coverPath = localFile.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
if (audioTracks.size > 0) {
|
||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks")
|
||||
var localMediaItem = LocalMediaItem(it.id, itemFolderName, mediaType, folderId, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverPath)
|
||||
|
||||
if (existingMediaItem != null && audioTracks.isEmpty()) {
|
||||
Log.d(tag, "Local media item ${existingMediaItem.name} no longer has audio tracks - removing item")
|
||||
DeviceManager.dbManager.removeLocalMediaItem(existingMediaItem.id)
|
||||
mediaItemsRemoved++
|
||||
} else if (existingMediaItem != null && !isNewOrUpdated) {
|
||||
Log.d(tag, "Local media item ${existingMediaItem.name} has no updates")
|
||||
mediaItemsUpToDate++
|
||||
} else if (audioTracks.isNotEmpty()) {
|
||||
if (existingMediaItem != null) mediaItemsUpdated++
|
||||
else mediaItemsAdded++
|
||||
|
||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
||||
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverPath)
|
||||
mediaItems.add(localMediaItem)
|
||||
}
|
||||
}
|
||||
|
||||
return if (mediaItems.size > 0) {
|
||||
Log.d(tag, "Found ${mediaItems.size} Media Items")
|
||||
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
||||
|
||||
return if (mediaItems.isNotEmpty()) {
|
||||
DeviceManager.dbManager.saveLocalMediaItems(mediaItems)
|
||||
FolderScanResult(folderName, folderPath, mediaType, folderUrl, mediaItems)
|
||||
|
||||
var folderMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(localFolder.id) // Get all local media items
|
||||
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderMediaItems)
|
||||
} else {
|
||||
Log.d(tag, "No Media Items Found")
|
||||
null
|
||||
Log.d(tag, "No Media Items to save")
|
||||
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
71
components/ui/Checkbox.vue
Normal file
71
components/ui/Checkbox.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<label class="flex justify-start items-center" :class="!disabled ? 'cursor-pointer' : ''">
|
||||
<div class="border-2 rounded flex flex-shrink-0 justify-center items-center" :class="wrapperClass">
|
||||
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
|
||||
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
|
||||
</div>
|
||||
<div v-if="label" class="select-none text-gray-100" :class="labelClassname">{{ label }}</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
label: String,
|
||||
small: Boolean,
|
||||
checkboxBg: {
|
||||
type: String,
|
||||
default: 'white'
|
||||
},
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: 'gray-400'
|
||||
},
|
||||
checkColor: {
|
||||
type: String,
|
||||
default: 'green-500'
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
selected: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', !!val)
|
||||
}
|
||||
},
|
||||
wrapperClass() {
|
||||
var classes = [`bg-${this.checkboxBg} border-${this.borderColor}`]
|
||||
if (this.small) classes.push('w-4 h-4')
|
||||
else classes.push('w-6 h-6')
|
||||
|
||||
return classes.join(' ')
|
||||
},
|
||||
labelClassname() {
|
||||
if (this.labelClass) return this.labelClass
|
||||
var classes = ['pl-1']
|
||||
if (this.small) classes.push('text-xs md:text-sm')
|
||||
return classes.join(' ')
|
||||
},
|
||||
svgClass() {
|
||||
var classes = [`text-${this.checkColor}`]
|
||||
if (this.small) classes.push('w-3 h-3')
|
||||
else classes.push('w-4 h-4')
|
||||
|
||||
return classes.join(' ')
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -3,7 +3,7 @@
|
|||
<p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText }}</span>
|
||||
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText || placeholder || '' }}</span>
|
||||
</span>
|
||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span class="material-icons">expand_more</span>
|
||||
|
@ -37,7 +37,8 @@ export default {
|
|||
default: () => []
|
||||
},
|
||||
disabled: Boolean,
|
||||
small: Boolean
|
||||
small: Boolean,
|
||||
placeholder: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
80
components/ui/IconBtn.vue
Normal file
80
components/ui/IconBtn.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<svg class="animate-spin" style="width: 24px; height: 24px" 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>
|
||||
<span v-else :class="outlined ? 'material-icons-outlined' : 'material-icons'" :style="{ fontSize }">{{ icon }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: String,
|
||||
disabled: Boolean,
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: 'primary'
|
||||
},
|
||||
outlined: Boolean,
|
||||
borderless: Boolean,
|
||||
loading: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
className() {
|
||||
var classes = []
|
||||
if (!this.borderless) {
|
||||
classes.push(`bg-${this.bgColor} border border-gray-600`)
|
||||
}
|
||||
return classes.join(' ')
|
||||
},
|
||||
fontSize() {
|
||||
if (this.icon === 'edit') return '1.25rem'
|
||||
return '1.4rem'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickBtn(e) {
|
||||
if (this.disabled || this.loading) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
this.$emit('click')
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
button.icon-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
button.icon-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
button.icon-btn:hover:not(:disabled)::before {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
button.icon-btn:disabled::before {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
button.icon-btn:disabled span {
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,12 @@
|
|||
<template>
|
||||
<div class="w-full h-full py-6 px-2">
|
||||
<div class="flex justify-between mb-4">
|
||||
<ui-btn to="/localMedia/folders">Back</ui-btn>
|
||||
<ui-btn :loading="isScanning" @click="searchFolder">Scan</ui-btn>
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="!removingFolder" :loading="isScanning" small @click="clickScan">Scan</ui-btn>
|
||||
<ui-btn v-if="!removingFolder && localMediaItems.length" :loading="isScanning" small class="ml-2" color="warning" @click="clickForceRescan">Force Re-Scan</ui-btn>
|
||||
<ui-icon-btn class="ml-2" bg-color="error" outlined :loading="removingFolder" icon="delete" @click="clickDeleteFolder" />
|
||||
</div>
|
||||
<p class="text-lg mb-0.5 text-white text-opacity-75">Folder: {{ folderId }}</p>
|
||||
<p class="text-lg mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
|
||||
<p class="mb-4 text-xl">Local Media Items ({{ localMediaItems.length }})</p>
|
||||
<div v-if="isScanning" class="w-full text-center p-4">
|
||||
<p>Scanning...</p>
|
||||
|
@ -32,6 +34,7 @@
|
|||
|
||||
<script>
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import StorageManager from '@/plugins/storage-manager'
|
||||
|
||||
export default {
|
||||
|
@ -44,19 +47,60 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
localMediaItems: [],
|
||||
isScanning: false
|
||||
folder: null,
|
||||
isScanning: false,
|
||||
removingFolder: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
folderName() {
|
||||
return this.folder ? this.folder.name : null
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
clickScan() {
|
||||
this.scanFolder()
|
||||
},
|
||||
clickForceRescan() {
|
||||
this.scanFolder(true)
|
||||
},
|
||||
async clickDeleteFolder() {
|
||||
var deleteMessage = 'Are you sure you want to remove this folder? (does not delete anything in your file system)'
|
||||
if (this.localMediaItems.length) {
|
||||
deleteMessage = `Are you sure you want to remove this folder and ${this.localMediaItems.length} media items? (does not delete anything in your file system)`
|
||||
}
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: deleteMessage
|
||||
})
|
||||
if (value) {
|
||||
this.removingFolder = true
|
||||
await StorageManager.removeFolder({ folderId: this.folderId })
|
||||
this.removingFolder = false
|
||||
this.$router.replace('/localMedia/folders')
|
||||
}
|
||||
},
|
||||
play(mediaItem) {
|
||||
this.$eventBus.$emit('play-local-item', mediaItem.id)
|
||||
},
|
||||
async searchFolder() {
|
||||
async scanFolder(forceAudioProbe = false) {
|
||||
this.isScanning = true
|
||||
var response = await StorageManager.searchFolder({ folderId: this.folderId })
|
||||
var response = await StorageManager.scanFolder({ folderId: this.folderId, forceAudioProbe })
|
||||
|
||||
if (response && response.localMediaItems) {
|
||||
var itemsAdded = response.itemsAdded
|
||||
var itemsUpdated = response.itemsUpdated
|
||||
var itemsRemoved = response.itemsRemoved
|
||||
var itemsUpToDate = response.itemsUpToDate
|
||||
var toastMessages = []
|
||||
if (itemsAdded) toastMessages.push(`${itemsAdded} Added`)
|
||||
if (itemsUpdated) toastMessages.push(`${itemsUpdated} Updated`)
|
||||
if (itemsRemoved) toastMessages.push(`${itemsRemoved} Removed`)
|
||||
if (itemsUpToDate) toastMessages.push(`${itemsUpToDate} Up-to-date`)
|
||||
this.$toast.info(`Folder scan complete:\n${toastMessages.join(' | ')}`)
|
||||
|
||||
// When all items are up-to-date then local media items are not returned
|
||||
if (response.localMediaItems.length) {
|
||||
this.localMediaItems = response.localMediaItems.map((mi) => {
|
||||
if (mi.coverPath) {
|
||||
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverPath)
|
||||
|
@ -64,14 +108,17 @@ export default {
|
|||
return mi
|
||||
})
|
||||
console.log('Set Local Media Items', this.localMediaItems.length)
|
||||
}
|
||||
} else {
|
||||
console.log('No Local media items found')
|
||||
}
|
||||
|
||||
this.isScanning = false
|
||||
},
|
||||
async init() {
|
||||
var items = (await this.$db.loadLocalMediaItemsInFolder(this.folderId)) || []
|
||||
var folder = await this.$db.getLocalFolder(this.folderId)
|
||||
this.folder = folder
|
||||
|
||||
var items = (await this.$db.getLocalMediaItemsInFolder(this.folderId)) || []
|
||||
console.log('Init folder', this.folderId, items)
|
||||
this.localMediaItems = items.map((lmi) => {
|
||||
return {
|
||||
|
@ -80,7 +127,7 @@ export default {
|
|||
}
|
||||
})
|
||||
if (this.shouldScan) {
|
||||
this.searchFolder()
|
||||
this.scanFolder()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<div class="w-full h-full py-6">
|
||||
<h1 class="text-2xl px-4">Downloads</h1>
|
||||
<h1 class="text-2xl px-4 mb-2">Local Folders</h1>
|
||||
|
||||
<div v-if="!isIos" class="w-full max-w-full px-2 py-2">
|
||||
<template v-for="folder in localFolders">
|
||||
<nuxt-link :to="`/localMedia/folders/${folder.id}`" :key="folder.id" class="flex items-center px-2 py-4 bg-primary rounded-md border-bg mb-1">
|
||||
<span class="material-icons text-xl text-yellow-400">folder</span>
|
||||
<p class="ml-2">{{ folder.id }}</p>
|
||||
<p class="ml-2">{{ folder.name }}</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-sm italic text-gray-300 px-2 capitalize">{{ folder.mediaType }}s</p>
|
||||
<span class="material-icons text-base text-gray-300">arrow_right</span>
|
||||
<p class="text-sm italic text-gray-300 px-3 capitalize">{{ folder.mediaType }}s</p>
|
||||
<span class="material-icons text-xl text-gray-300">arrow_right</span>
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<div v-if="!localFolders.length" class="flex justify-center">
|
||||
|
@ -17,84 +17,22 @@
|
|||
</div>
|
||||
<div class="flex p-2 border-t border-primary mt-2">
|
||||
<div class="flex-grow pr-1">
|
||||
<ui-dropdown v-model="newFolderMediaType" :items="mediaTypeItems" />
|
||||
<ui-dropdown v-model="newFolderMediaType" placeholder="Select media type" :items="mediaTypeItems" />
|
||||
</div>
|
||||
<ui-btn small class="w-28" @click="selectFolder">Add Folder</ui-btn>
|
||||
<ui-btn small class="w-28" color="success" @click="selectFolder">New Folder</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div v-if="!isIos" class="list-content-body relative w-full overflow-x-hidden overflow-y-auto bg-primary">
|
||||
<template v-if="showingDownloads">
|
||||
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||
<p>No Downloads</p>
|
||||
</div>
|
||||
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li v-for="download in downloadsDownloading" :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-3/4">
|
||||
<span class="text-xs">({{ downloadingProgress[download.id] || 0 }}%) {{ download.isPreparing ? 'Preparing' : 'Downloading' }}...</span>
|
||||
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<div class="shadow-sm text-white flex items-center justify-center rounded-full animate-spin">
|
||||
<span class="material-icons">refresh</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-for="download in downloadsReady" :key="download.id" class="text-gray-50 select-none relative pr-4 pl-2 py-5 border-b border-white border-opacity-10" @click="jumpToAudiobook(download)">
|
||||
<modals-downloads-download-item :download="download" @play="playDownload" @delete="clickDeleteDownload" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full flex justify-around py-4 px-2">
|
||||
<ui-btn small color="error" @click="resetFolder">Reset</ui-btn>
|
||||
</div>
|
||||
|
||||
<p v-if="isScanning" class="text-center my-8">Scanning Folder..</p>
|
||||
<p v-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
|
||||
<div v-else>
|
||||
<div v-for="mediaFolder in mediaScanResults.folders" :key="mediaFolder.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">folder</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFolder.name }}</p>
|
||||
</div>
|
||||
<div v-for="mediaFile in mediaFolder.files" :key="mediaFile.uri" class="ml-3 flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="mediaFile in mediaScanResults.files" :key="mediaFile.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
import StorageManager from '@/plugins/storage-manager'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
downloadingProgress: {},
|
||||
totalSize: 0,
|
||||
showingDownloads: true,
|
||||
isScanning: false,
|
||||
localMediaItems: [],
|
||||
localFolders: [],
|
||||
newFolderMediaType: 'book',
|
||||
newFolderMediaType: null,
|
||||
mediaTypeItems: [
|
||||
{
|
||||
value: 'book',
|
||||
|
@ -113,37 +51,13 @@ export default {
|
|||
},
|
||||
isSocketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
hasStoragePermission() {
|
||||
return this.$store.state.hasStoragePermission
|
||||
},
|
||||
downloadFolder() {
|
||||
return this.$store.state.downloadFolder
|
||||
},
|
||||
downloadFolderSimplePath() {
|
||||
return this.downloadFolder ? this.downloadFolder.simplePath : null
|
||||
},
|
||||
downloadFolderUri() {
|
||||
return this.downloadFolder ? this.downloadFolder.uri : null
|
||||
},
|
||||
totalDownloads() {
|
||||
return this.downloadsReady.length + this.downloadsDownloading.length
|
||||
},
|
||||
downloadsDownloading() {
|
||||
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
|
||||
},
|
||||
downloadsReady() {
|
||||
return this.downloads.filter((d) => !d.isDownloading && !d.isPreparing)
|
||||
},
|
||||
downloads() {
|
||||
return this.$store.state.downloads.downloads
|
||||
},
|
||||
mediaScanResults() {
|
||||
return this.$store.state.downloads.mediaScanResults
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async selectFolder() {
|
||||
if (!this.newFolderMediaType) {
|
||||
return this.$toast.warn('Must select a media type')
|
||||
}
|
||||
var folderObj = await StorageManager.selectFolder({ mediaType: this.newFolderMediaType })
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
|
@ -165,114 +79,14 @@ export default {
|
|||
this.$toast.success('Folder permission success')
|
||||
}
|
||||
|
||||
// await this.searchFolder(folderObj.id)
|
||||
|
||||
this.$router.push(`/localMedia/folders/${folderObj.id}?scan=1`)
|
||||
},
|
||||
async changeDownloadFolderClick() {
|
||||
if (!this.hasStoragePermission) {
|
||||
StorageManager.requestStoragePermission()
|
||||
} else {
|
||||
var folderObj = await StorageManager.selectFolder({ mediaType: 'book' })
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
|
||||
var indexOfExisting = this.localFolders.findIndex((lf) => lf.id == folderObj.id)
|
||||
if (indexOfExisting >= 0) {
|
||||
this.localFolders.splice(indexOfExisting, 1, folderObj)
|
||||
} else {
|
||||
this.localFolders.push(folderObj)
|
||||
}
|
||||
|
||||
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.contentUrl })
|
||||
|
||||
if (!permissionsGood) {
|
||||
this.$toast.error('Folder permissions failed')
|
||||
return
|
||||
} else {
|
||||
this.$toast.success('Folder permission success')
|
||||
}
|
||||
await this.searchFolder(folderObj.id)
|
||||
|
||||
if (this.isSocketConnected) {
|
||||
this.$store.dispatch('downloads/linkOrphanDownloads')
|
||||
}
|
||||
}
|
||||
},
|
||||
async searchFolder(folderId) {
|
||||
this.isScanning = true
|
||||
var response = await StorageManager.searchFolder({ folderId })
|
||||
|
||||
if (response && response.localMediaItems) {
|
||||
this.localMediaItems = response.localMediaItems.map((mi) => {
|
||||
if (mi.coverPath) {
|
||||
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverPath)
|
||||
}
|
||||
return mi
|
||||
})
|
||||
console.log('Set Local Media Items', this.localMediaItems.length)
|
||||
} else {
|
||||
console.log('No Local media items found')
|
||||
}
|
||||
this.isScanning = false
|
||||
},
|
||||
async resetFolder() {
|
||||
await this.$localStore.setDownloadFolder(null)
|
||||
this.$store.commit('downloads/setMediaScanResults', {})
|
||||
this.$toast.info('Unlinked Folder')
|
||||
},
|
||||
jumpToAudiobook(download) {
|
||||
this.show = false
|
||||
this.$router.push(`/audiobook/${download.id}`)
|
||||
},
|
||||
async clickDeleteDownload(download) {
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: 'Delete this download?'
|
||||
})
|
||||
if (value) {
|
||||
this.deleteDownload(download)
|
||||
}
|
||||
},
|
||||
playDownload(download) {
|
||||
this.$store.commit('setPlayOnLoad', true)
|
||||
this.$store.commit('setPlayingDownload', download)
|
||||
this.show = false
|
||||
},
|
||||
async deleteDownload(download) {
|
||||
console.log('Delete download', download.filename)
|
||||
|
||||
if (this.$store.state.playingDownload && this.$store.state.playingDownload.id === download.id) {
|
||||
console.warn('Deleting download when currently playing download - terminate play')
|
||||
if (this.$refs.streamContainer) {
|
||||
this.$refs.streamContainer.cancelStream()
|
||||
}
|
||||
}
|
||||
if (download.contentUrl) {
|
||||
await StorageManager.delete(download)
|
||||
}
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
},
|
||||
onDownloadProgress(data) {
|
||||
var progress = data.progress
|
||||
var audiobookId = data.audiobookId
|
||||
|
||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||
if (downloadObj) {
|
||||
this.$set(this.downloadingProgress, audiobookId, progress)
|
||||
}
|
||||
},
|
||||
async init() {
|
||||
this.localFolders = (await this.$db.loadFolders()) || []
|
||||
AudioDownloader.addListener('onDownloadProgress', this.onDownloadProgress)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
AudioDownloader.removeListener('onDownloadProgress', this.onDownloadProgress)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -24,7 +24,7 @@ class DbService {
|
|||
}
|
||||
|
||||
loadFolders() {
|
||||
return DbManager.localFoldersFromWebView().then((data) => {
|
||||
return DbManager.getLocalFolders_WV().then((data) => {
|
||||
console.log('Loaded local folders', JSON.stringify(data))
|
||||
if (data.folders && typeof data.folders == 'string') {
|
||||
return JSON.parse(data.folders)
|
||||
|
@ -36,8 +36,15 @@ class DbService {
|
|||
})
|
||||
}
|
||||
|
||||
loadLocalMediaItemsInFolder(folderId) {
|
||||
return DbManager.loadMediaItemsInFolder({ folderId }).then((data) => {
|
||||
getLocalFolder(folderId) {
|
||||
return DbManager.getLocalFolder_WV({ folderId }).then((data) => {
|
||||
console.log('Got local folder', JSON.stringify(data))
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
getLocalMediaItemsInFolder(folderId) {
|
||||
return DbManager.getLocalMediaItemsInFolder_WV({ folderId }).then((data) => {
|
||||
console.log('Loaded local media items in folder', JSON.stringify(data))
|
||||
if (data.localMediaItems && typeof data.localMediaItems == 'string') {
|
||||
return JSON.parse(data.localMediaItems)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue