diff --git a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt index 86a96822..533526cf 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt @@ -5,6 +5,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import androidx.core.content.ContextCompat +import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.server.ApiHandler import com.capacitorjs.plugins.app.AppPlugin import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -80,6 +81,23 @@ class MyNativeAudio : Plugin() { } } + @PluginMethod + fun playLocalLibraryItem(call:PluginCall) { + var localMediaItemId = call.getString("localMediaItemId", "").toString() + var playWhenReady = call.getBoolean("playWhenReady") == true + + DeviceManager.dbManager.loadLocalMediaItem(localMediaItemId)?.let { + Handler(Looper.getMainLooper()).post() { + Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}") + playerNotificationService.playLocalMediaItem(it, playWhenReady) + } + return call.resolve(JSObject()) + } + var errObj = JSObject() + errObj.put("error", "Item Not Found") + call.resolve(errObj) + } + @PluginMethod fun getLibraryItems(call: PluginCall) { var libraryId = call.getString("libraryId", "").toString() diff --git a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt index ce79dc82..cd95a341 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt @@ -24,6 +24,7 @@ import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants import com.anggrayudi.storage.file.isExternalStorageDocument import com.audiobookshelf.app.data.DbManager +import com.audiobookshelf.app.data.LocalMediaItem import com.audiobookshelf.app.data.PlaybackSession import com.getcapacitor.Bridge import com.getcapacitor.JSObject @@ -706,6 +707,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings } + fun playLocalMediaItem(localMediaItem: LocalMediaItem, playWhenReady:Boolean) { + Log.d(tag, "playLocalMediaItem ${localMediaItem}") + } + fun initPlayer(audiobookStreamData: AudiobookStreamData) { currentAudiobookStreamData = audiobookStreamData diff --git a/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt b/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt index c8cd79ec..0f6d5cbc 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt @@ -10,6 +10,8 @@ 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.data.LocalFolder +import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.FolderScanner import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.getcapacitor.* @@ -21,30 +23,6 @@ class StorageManager : Plugin() { lateinit var mainActivity:MainActivity - data class MediaFile(val uri: Uri, val name: String, val simplePath: String, val size: Long, val type: String, val isAudio: Boolean) { - fun toJSObject() : JSObject { - var obj = JSObject() - obj.put("uri", this.uri) - obj.put("name", this.name) - obj.put("simplePath", this.simplePath) - obj.put("size", this.size) - obj.put("type", this.type) - obj.put("isAudio", this.isAudio) - return obj - } - } - - data class MediaFolder(val uri: Uri, val name: String, val simplePath: String, val mediaFiles:List) { - fun toJSObject() : JSObject { - var obj = JSObject() - obj.put("uri", this.uri) - obj.put("name", this.name) - obj.put("simplePath", this.simplePath) - obj.put("files", this.mediaFiles.map { it.toJSObject() }) - return obj - } - } - override fun load() { mainActivity = (activity as MainActivity) @@ -79,24 +57,19 @@ class StorageManager : Plugin() { @PluginMethod fun selectFolder(call: PluginCall) { + var mediaType = call.data.getString("mediaType", "book").toString() + mainActivity.storage.folderPickerCallback = object : FolderPickerCallback { override fun onFolderSelected(requestCode: Int, folder: DocumentFile) { Log.d(TAG, "ON FOLDER SELECTED ${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) + var localFolder = LocalFolder(folder.id, folder.name, folder.uri.toString(),absolutePath, simplePath, storageType.toString(), mediaType) + + DeviceManager.dbManager.saveLocalFolder(localFolder) + call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localFolder))) } override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) { @@ -155,19 +128,22 @@ class StorageManager : Plugin() { @PluginMethod fun searchFolder(call: PluginCall) { - var folderUrl = call.data.getString("folderUrl", "").toString() - var mediaType = call.data.getString("mediaType", "book").toString() - Log.d(TAG, "Searching folder $folderUrl") - - var folderScanner = FolderScanner(context) - var folderScanResult = folderScanner.scanForMediaItems(folderUrl, mediaType) - if (folderScanResult == null) { - Log.d(TAG, "NO Scan DATA") - call.resolve(JSObject()) - } else { - Log.d(TAG, "Scan DATA ${jacksonObjectMapper().writeValueAsString(folderScanResult)}") - call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(folderScanResult))) + var folderId = call.data.getString("folderId", "").toString() + var folder: LocalFolder? = DeviceManager.dbManager.loadLocalFolder(folderId) + folder?.let { + Log.d(TAG, "Searching folder ${it.contentUrl}") + var folderScanner = FolderScanner(context) + var folderScanResult = folderScanner.scanForMediaItems(it.contentUrl, it.mediaType) + if (folderScanResult == null) { + Log.d(TAG, "NO Scan DATA") + return call.resolve(JSObject()) + } else { + 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)) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt index dc86d58d..fd04bf07 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt @@ -1,6 +1,7 @@ package com.audiobookshelf.app.data import android.util.Log +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.getcapacitor.JSObject import com.getcapacitor.Plugin import com.getcapacitor.PluginCall @@ -13,15 +14,54 @@ import org.json.JSONObject class DbManager : Plugin() { val tag = "DbManager" - fun loadDeviceData():DeviceData { - var deviceData:DeviceData? = Paper.book("device").read("data") - return deviceData ?: DeviceData(mutableListOf(),null) + fun loadDeviceData(): DeviceData { + return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null) } fun saveDeviceData(deviceData:DeviceData) { Paper.book("device").write("data", deviceData) } + fun loadLocalMediaItems():List { + var localMediaItems:MutableList = mutableListOf() + Paper.book("localMediaItems").allKeys.forEach { + var localMediaItem:LocalMediaItem? = Paper.book("localMediaItems").read(it) + if (localMediaItem != null) { + localMediaItems.add(localMediaItem) + } + } + return localMediaItems + } + + fun loadLocalMediaItem(localMediaItemId:String):LocalMediaItem? { + return Paper.book("localMediaItems").read(localMediaItemId) + } + + fun saveLocalMediaItems(localMediaItems:List) { + 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? { + return Paper.book("localFolders").read(folderId) + } + + fun getAllLocalFolders():List { + var localFolders:MutableList = mutableListOf() + Paper.book("localFolders").allKeys.forEach { + var localFolder:LocalFolder? = Paper.book("localFolders").read(it) + if (localFolder != null) { + localFolders.add(localFolder) + } + } + return localFolders + } + fun saveObject(db:String, key:String, value:JSONObject) { Log.d(tag, "Saving Object $key ${value.toString()}") Paper.book(db).write(key, value) @@ -60,4 +100,26 @@ class DbManager : Plugin() { var jsobj = JSObject.fromJSONObject(json) call.resolve(jsobj) } + + @PluginMethod + fun localFoldersFromWebView(call:PluginCall) { + 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 + } + + var mediaItemsArray = jacksonObjectMapper().writeValueAsString(localMediaItems) + var jsobj = JSObject() + jsobj.put("localMediaItems", mediaItemsArray) + call.resolve(jsobj) + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index 052ba958..54259c75 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -20,7 +20,10 @@ data class DeviceData( @JsonIgnoreProperties(ignoreUnknown = true) data class LocalMediaItem( + var id:String, var name: String, + var mediaType:String, + var folderId:String, var contentUrl:String, var simplePath: String, var absolutePath:String, @@ -39,3 +42,14 @@ data class LocalFile( var mimeType:String?, var size:Long ) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LocalFolder( + var id:String, + var name:String?, + var contentUrl:String, + var absolutePath:String, + var simplePath:String, + var storageType:String, + var mediaType:String +) diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index cb39c283..e5ffea44 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -20,6 +20,10 @@ class FolderScanner(var ctx: Context) { Log.e(tag, "Folder Doc File Invalid $folderUrl") return null } + var folderName = df.name ?: "" + var folderPath = df.getAbsolutePath(ctx) + var folderUrl = df.uri.toString() + var folderId = df.id var foldersFound = df.search(false, DocumentFileType.FOLDER) @@ -27,7 +31,7 @@ class FolderScanner(var ctx: Context) { foldersFound.forEach { Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}") - var folderName = it.name ?: "" + var itemFolderName = it.name ?: "" var audioTracks = mutableListOf() var localFiles = mutableListOf() @@ -40,7 +44,7 @@ class FolderScanner(var ctx: Context) { var mimeType = it2?.mimeType ?: "" var filename = it2?.name ?: "" var isAudio = mimeType.startsWith("audio") - Log.d(tag, "Found $mimeType file $filename in folder $folderName") + 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()) localFiles.add(localFile) @@ -70,15 +74,16 @@ class FolderScanner(var ctx: Context) { } } if (audioTracks.size > 0) { - Log.d(tag, "Found local media item named $folderName with ${audioTracks.size} tracks") - var localMediaItem = LocalMediaItem(folderName, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverPath) + 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) mediaItems.add(localMediaItem) } } return if (mediaItems.size > 0) { Log.d(tag, "Found ${mediaItems.size} Media Items") - FolderScanResult(df.name, df.getAbsolutePath(ctx), mediaType, df.uri.toString(), mediaItems) + DeviceManager.dbManager.saveLocalMediaItems(mediaItems) + FolderScanResult(folderName, folderPath, mediaType, folderUrl, mediaItems) } else { Log.d(tag, "No Media Items Found") null diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index 6dac10c1..9fa1cfad 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -488,6 +488,18 @@ export default { .catch((error) => { console.error('TEST failed', error) }) + }, + async playLocalItem(localMediaItemId) { + console.log('Called play local media item for lmi', localMediaItemId) + MyNativeAudio.playLocalLibraryItem({ localMediaItemId, playWhenReady: true }) + .then((data) => { + console.log('TEST library item play response', JSON.stringify(data)) + var mediaEntity = data.mediaEntity + this.$store.commit('globals/setMediaEntityPlaying', mediaEntity) + }) + .catch((error) => { + console.error('TEST failed', error) + }) } }, mounted() { @@ -499,6 +511,7 @@ export default { this.setListeners() this.$eventBus.$on('play-item', this.playLibraryItem) + this.$eventBus.$on('play-local-item', this.playLocalItem) this.$eventBus.$on('close_stream', this.closeStreamOnly) this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated }) }, @@ -514,6 +527,7 @@ export default { this.$server.socket.off('stream_reset', this.streamReset) } this.$eventBus.$off('play-item', this.playLibraryItem) + this.$eventBus.$off('play-local-item', this.playLocalItem) this.$eventBus.$off('close_stream', this.closeStreamOnly) this.$store.commit('user/removeSettingsListener', 'streamContainer') } diff --git a/components/app/SideDrawer.vue b/components/app/SideDrawer.vue index bcab1bc2..cd39fb84 100644 --- a/components/app/SideDrawer.vue +++ b/components/app/SideDrawer.vue @@ -83,8 +83,8 @@ export default { { icon: 'folder', iconOutlined: true, - text: 'Downloads', - to: '/downloads' + text: 'Local Media', + to: '/localMedia/folders' } // { // icon: 'settings', diff --git a/components/ui/Btn.vue b/components/ui/Btn.vue index 734e0d76..0b0f187f 100644 --- a/components/ui/Btn.vue +++ b/components/ui/Btn.vue @@ -1,8 +1,15 @@