mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-10 14:04:41 +02:00
New data model classes, ffmpeg-kit, jackson json deserializer, add permission
This commit is contained in:
parent
461733854a
commit
4fc70cd3dd
30 changed files with 9058 additions and 9642 deletions
10
Server.js
10
Server.js
|
@ -1,13 +1,13 @@
|
||||||
import { io } from 'socket.io-client'
|
import { io } from 'socket.io-client'
|
||||||
import { Storage } from '@capacitor/storage'
|
import { Storage } from '@capacitor/storage'
|
||||||
import axios from 'axios'
|
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
|
|
||||||
class Server extends EventEmitter {
|
class Server extends EventEmitter {
|
||||||
constructor(store) {
|
constructor(store, $axios) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.store = store
|
this.store = store
|
||||||
|
this.$axios = $axios
|
||||||
|
|
||||||
this.url = null
|
this.url = null
|
||||||
this.socket = null
|
this.socket = null
|
||||||
|
@ -119,7 +119,7 @@ class Server extends EventEmitter {
|
||||||
async login(url, username, password) {
|
async login(url, username, password) {
|
||||||
var serverUrl = this.getServerUrl(url)
|
var serverUrl = this.getServerUrl(url)
|
||||||
var authUrl = serverUrl + '/login'
|
var authUrl = serverUrl + '/login'
|
||||||
return axios.post(authUrl, { username, password }).then((res) => {
|
return this.$axios.post(authUrl, { username, password }).then((res) => {
|
||||||
if (!res.data || !res.data.user) {
|
if (!res.data || !res.data.user) {
|
||||||
console.error(res.data.error)
|
console.error(res.data.error)
|
||||||
return {
|
return {
|
||||||
|
@ -160,7 +160,7 @@ class Server extends EventEmitter {
|
||||||
|
|
||||||
authorize(serverUrl, token) {
|
authorize(serverUrl, token) {
|
||||||
var authUrl = serverUrl + '/api/authorize'
|
var authUrl = serverUrl + '/api/authorize'
|
||||||
return axios.post(authUrl, null, { headers: { Authorization: `Bearer ${token}` } }).then((res) => {
|
return this.$axios.post(authUrl, null, { headers: { Authorization: `Bearer ${token}` } }).then((res) => {
|
||||||
return res.data
|
return res.data
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('[Server] Server auth failed', error)
|
console.error('[Server] Server auth failed', error)
|
||||||
|
@ -181,7 +181,7 @@ class Server extends EventEmitter {
|
||||||
ping(url) {
|
ping(url) {
|
||||||
var pingUrl = url + '/ping'
|
var pingUrl = url + '/ping'
|
||||||
console.log('[Server] Check server', pingUrl)
|
console.log('[Server] Check server', pingUrl)
|
||||||
return axios.get(pingUrl, { timeout: 1000 }).then((res) => {
|
return this.$axios.get(pingUrl, { timeout: 1000 }).then((res) => {
|
||||||
return res.data
|
return res.data
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Server check failed', error)
|
console.error('Server check failed', error)
|
||||||
|
|
|
@ -88,6 +88,9 @@ dependencies {
|
||||||
|
|
||||||
// Jackson for JSON
|
// Jackson for JSON
|
||||||
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1'
|
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1'
|
||||||
|
|
||||||
|
// FFMPEG-Kit
|
||||||
|
implementation 'com.arthenica:ffmpeg-kit-full:4.5.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'capacitor.build.gradle'
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.*
|
import android.content.*
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.audiobookshelf.app.data.DbManager
|
import com.audiobookshelf.app.data.DbManager
|
||||||
|
@ -24,6 +27,11 @@ class MainActivity : BridgeActivity() {
|
||||||
val storageHelper = SimpleStorageHelper(this)
|
val storageHelper = SimpleStorageHelper(this)
|
||||||
val storage = SimpleStorage(this)
|
val storage = SimpleStorage(this)
|
||||||
|
|
||||||
|
val REQUEST_PERMISSIONS = 1
|
||||||
|
var PERMISSIONS_ALL = arrayOf(
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
)
|
||||||
|
|
||||||
val broadcastReceiver = object: BroadcastReceiver() {
|
val broadcastReceiver = object: BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
|
@ -43,6 +51,14 @@ class MainActivity : BridgeActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
|
|
||||||
|
var permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
PERMISSIONS_ALL,
|
||||||
|
REQUEST_PERMISSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
registerPlugin(MyNativeAudio::class.java)
|
registerPlugin(MyNativeAudio::class.java)
|
||||||
registerPlugin(AudioDownloader::class.java)
|
registerPlugin(AudioDownloader::class.java)
|
||||||
registerPlugin(StorageManager::class.java)
|
registerPlugin(StorageManager::class.java)
|
||||||
|
|
|
@ -67,12 +67,13 @@ class MyNativeAudio : Plugin() {
|
||||||
fun prepareLibraryItem(call: PluginCall) {
|
fun prepareLibraryItem(call: PluginCall) {
|
||||||
var libraryItemId = call.getString("libraryItemId", "").toString()
|
var libraryItemId = call.getString("libraryItemId", "").toString()
|
||||||
var mediaEntityId = call.getString("mediaEntityId", "").toString()
|
var mediaEntityId = call.getString("mediaEntityId", "").toString()
|
||||||
|
var playWhenReady = call.getBoolean("playWhenReady") == true
|
||||||
|
|
||||||
apiHandler.playLibraryItem(libraryItemId) {
|
apiHandler.playLibraryItem(libraryItemId) {
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
|
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
|
||||||
playerNotificationService.preparePlayer(it)
|
playerNotificationService.preparePlayer(it, playWhenReady)
|
||||||
}
|
}
|
||||||
|
|
||||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it)))
|
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it)))
|
||||||
|
|
|
@ -381,8 +381,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
.setMediaId(currentPlaybackSession!!.id)
|
.setMediaId(currentPlaybackSession!!.id)
|
||||||
.setTitle(currentPlaybackSession!!.getTitle())
|
.setTitle(currentPlaybackSession!!.getTitle())
|
||||||
.setSubtitle(currentPlaybackSession!!.getAuthor())
|
.setSubtitle(currentPlaybackSession!!.getAuthor())
|
||||||
// .setMediaUri(currentPlaybackSession!!.getContentUri())
|
.setMediaUri(currentPlaybackSession!!.getContentUri())
|
||||||
// .setIconUri(currentAudiobookStreamData!!.)
|
.setIconUri(currentPlaybackSession!!.getCoverUri())
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -666,21 +666,44 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
/*
|
/*
|
||||||
User callable methods
|
User callable methods
|
||||||
*/
|
*/
|
||||||
fun preparePlayer(playbackSession: PlaybackSession) {
|
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
|
||||||
currentPlaybackSession = playbackSession
|
currentPlaybackSession = playbackSession
|
||||||
var metadata = playbackSession.getMediaMetadataCompat()
|
var metadata = playbackSession.getMediaMetadataCompat()
|
||||||
mediaSession.setMetadata(metadata)
|
mediaSession.setMetadata(metadata)
|
||||||
var mediaMetadata = playbackSession.getMediaMetadata()
|
var mediaMetadata = playbackSession.getExoMediaMetadata()
|
||||||
var mediaUrl = playbackSession.getContentUri()
|
|
||||||
var mimeType = playbackSession.getMimeType()
|
|
||||||
Log.d(tag, "Media URL $mediaUrl")
|
// var mediaUri = playbackSession.getContentUri()
|
||||||
var mediaUri = Uri.parse(mediaUrl)
|
// var mimeType = playbackSession.getMimeType()
|
||||||
var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
|
// var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
|
||||||
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
// var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
||||||
var mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
// var mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||||
mPlayer.setMediaSource(mediaSource, 0L)
|
// mPlayer.setMediaSource(mediaSource, 0L)
|
||||||
mPlayer.prepare()
|
|
||||||
mPlayer.playWhenReady = true
|
// if (mPlayer == currentPlayer) {
|
||||||
|
// var mediaSource:MediaSource
|
||||||
|
//
|
||||||
|
// if (currentAudiobookStreamData!!.isLocal) {
|
||||||
|
// Log.d(tag, "Playing Local File")
|
||||||
|
// var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
||||||
|
// mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||||
|
// } else {
|
||||||
|
// Log.d(tag, "Playing HLS File")
|
||||||
|
// var dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||||
|
// dataSourceFactory.setUserAgent(channelId)
|
||||||
|
// dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobookStreamData!!.token}"))
|
||||||
|
// mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||||
|
// }
|
||||||
|
// mPlayer.setMediaSource(mediaSource, currentAudiobookStreamData!!.startTime)
|
||||||
|
// } else if (castPlayer != null) {
|
||||||
|
//// var mediaQueue = currentAudiobookStreamData!!.getCastQueue()
|
||||||
|
// // TODO: Start position will need to be adjusted if using multi-track queue
|
||||||
|
//// castPlayer?.setMediaItems(mediaQueue, 0, 0)
|
||||||
|
// }
|
||||||
|
|
||||||
|
currentPlayer.prepare()
|
||||||
|
currentPlayer.playWhenReady = playWhenReady
|
||||||
|
currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initPlayer(audiobookStreamData: AudiobookStreamData) {
|
fun initPlayer(audiobookStreamData: AudiobookStreamData) {
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
package com.audiobookshelf.app
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.callback.FolderPickerCallback
|
import com.anggrayudi.storage.callback.FolderPickerCallback
|
||||||
import com.anggrayudi.storage.callback.StorageAccessCallback
|
import com.anggrayudi.storage.callback.StorageAccessCallback
|
||||||
import com.anggrayudi.storage.file.*
|
import com.anggrayudi.storage.file.*
|
||||||
|
import com.audiobookshelf.app.device.FolderScanner
|
||||||
import com.getcapacitor.*
|
import com.getcapacitor.*
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
|
|
||||||
|
@ -112,6 +116,7 @@ class StorageManager : Plugin() {
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainActivity.storage.openFolderPicker(6)
|
mainActivity.storage.openFolderPicker(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,12 +131,11 @@ class StorageManager : Plugin() {
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun checkStoragePermission(call: PluginCall) {
|
fun checkStoragePermission(call: PluginCall) {
|
||||||
var res = false
|
var res = false
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||||
res = SimpleStorage.hasStoragePermission(context)
|
res = SimpleStorage.hasStoragePermission(context)
|
||||||
Log.d(TAG, "Check Storage Access $res")
|
Log.d(TAG, "checkStoragePermission: Check Storage Access $res")
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Has permission on Android 10 or up")
|
Log.d(TAG, "checkStoragePermission: Has permission on Android 10 or up")
|
||||||
res = true
|
res = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,58 +161,63 @@ class StorageManager : Plugin() {
|
||||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||||
Log.d(TAG, "Searching folder $folderUrl")
|
Log.d(TAG, "Searching folder $folderUrl")
|
||||||
|
|
||||||
var df: DocumentFile? = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
var folderScanner = FolderScanner(context)
|
||||||
|
var data = folderScanner.scanForAudiobooks(folderUrl)
|
||||||
if (df == null) {
|
Log.d(TAG, "Scan DATA $data")
|
||||||
Log.e(TAG, "Folder Doc File Invalid $folderUrl")
|
call.resolve(JSObject())
|
||||||
var jsobj = JSObject()
|
//
|
||||||
jsobj.put("folders", JSArray())
|
// var df: DocumentFile? = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
||||||
jsobj.put("files", JSArray())
|
//
|
||||||
call.resolve(jsobj)
|
// if (df == null) {
|
||||||
return
|
// Log.e(TAG, "Folder Doc File Invalid $folderUrl")
|
||||||
}
|
// var jsobj = JSObject()
|
||||||
|
// jsobj.put("folders", JSArray())
|
||||||
Log.d(TAG, "Folder as DF ${df.isDirectory} | ${df.getSimplePath(context)} | ${df.getBasePath(context)} | ${df.name}")
|
// jsobj.put("files", JSArray())
|
||||||
|
// call.resolve(jsobj)
|
||||||
var mediaFolders = mutableListOf<MediaFolder>()
|
// return
|
||||||
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
// }
|
||||||
|
//
|
||||||
foldersFound.forEach {
|
// Log.d(TAG, "Folder as DF ${df.isDirectory} | ${df.getSimplePath(context)} | ${df.getBasePath(context)} | ${df.name}")
|
||||||
Log.d(TAG, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(context)} | URI: ${it.uri}")
|
//
|
||||||
var folderName = it.name ?: ""
|
// var mediaFolders = mutableListOf<MediaFolder>()
|
||||||
var mediaFiles = mutableListOf<MediaFile>()
|
// var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||||
|
//
|
||||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
// foldersFound.forEach {
|
||||||
filesInFolder.forEach { it2 ->
|
// Log.d(TAG, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(context)} | URI: ${it.uri}")
|
||||||
var mimeType = it2?.mimeType ?: ""
|
// var folderName = it.name ?: ""
|
||||||
var filename = it2?.name ?: ""
|
// var mediaFiles = mutableListOf<MediaFile>()
|
||||||
var isAudio = mimeType.startsWith("audio")
|
//
|
||||||
Log.d(TAG, "Found $mimeType file $filename in folder $folderName")
|
// var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||||
var imageFile = MediaFile(it2.uri, filename, it2.getSimplePath(context), it2.length(), mimeType, isAudio)
|
// filesInFolder.forEach { it2 ->
|
||||||
mediaFiles.add(imageFile)
|
// var mimeType = it2?.mimeType ?: ""
|
||||||
}
|
// var filename = it2?.name ?: ""
|
||||||
if (mediaFiles.size > 0) {
|
// var isAudio = mimeType.startsWith("audio")
|
||||||
mediaFolders.add(MediaFolder(it.uri, folderName, it.getSimplePath(context), mediaFiles))
|
// 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)
|
||||||
|
// }
|
||||||
// Files in root dir
|
// if (mediaFiles.size > 0) {
|
||||||
var rootMediaFiles = mutableListOf<MediaFile>()
|
// mediaFolders.add(MediaFolder(it.uri, folderName, it.getSimplePath(context), mediaFiles))
|
||||||
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 ?: ""
|
// // Files in root dir
|
||||||
var filename = it?.name ?: ""
|
// var rootMediaFiles = mutableListOf<MediaFile>()
|
||||||
var isAudio = mimeType.startsWith("audio")
|
// var mediaFilesFound:List<DocumentFile> = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||||
Log.d(TAG, "Found $mimeType file $filename in root folder")
|
// mediaFilesFound.forEach {
|
||||||
var imageFile = MediaFile(it.uri, filename, it.getSimplePath(context), it.length(), mimeType, isAudio)
|
// Log.d(TAG, "Folder Root File Found ${it.name} | ${it.getSimplePath(context)} | URI: ${it.uri} | ${it.mimeType}")
|
||||||
rootMediaFiles.add(imageFile)
|
// var mimeType = it?.mimeType ?: ""
|
||||||
}
|
// var filename = it?.name ?: ""
|
||||||
|
// var isAudio = mimeType.startsWith("audio")
|
||||||
var jsobj = JSObject()
|
// Log.d(TAG, "Found $mimeType file $filename in root folder")
|
||||||
jsobj.put("folders", mediaFolders.map{ it.toJSObject() })
|
// var imageFile = MediaFile(it.uri, filename, it.getSimplePath(context), it.length(), mimeType, isAudio)
|
||||||
jsobj.put("files", rootMediaFiles.map{ it.toJSObject() })
|
// rootMediaFiles.add(imageFile)
|
||||||
call.resolve(jsobj)
|
// }
|
||||||
|
//
|
||||||
|
// var jsobj = JSObject()
|
||||||
|
// jsobj.put("folders", mediaFolders.map{ it.toJSObject() })
|
||||||
|
// jsobj.put("files", rootMediaFiles.map{ it.toJSObject() })
|
||||||
|
// call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class AudioProbeStream(
|
||||||
|
val index:Int,
|
||||||
|
val codec_name:String,
|
||||||
|
val codec_long_name:String,
|
||||||
|
val channels:Int,
|
||||||
|
val channel_layout:String,
|
||||||
|
val duration:Double,
|
||||||
|
val bit_rate:Double
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class AudioProbeChapterTags(
|
||||||
|
val title:String
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class AudioProbeChapter(
|
||||||
|
val id:Int,
|
||||||
|
val start:Int,
|
||||||
|
val end:Int,
|
||||||
|
val tags:AudioProbeChapterTags
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class AudioProbeFormatTags(
|
||||||
|
val artist:String?,
|
||||||
|
val album:String?,
|
||||||
|
val comment:String?,
|
||||||
|
val date:String?,
|
||||||
|
val genre:String?,
|
||||||
|
val title:String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class AudioProbeFormat(
|
||||||
|
val filename:String,
|
||||||
|
val format_name:String,
|
||||||
|
val duration:Double,
|
||||||
|
val size:Long,
|
||||||
|
val bit_rate:Double,
|
||||||
|
val tags:AudioProbeFormatTags
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class AudioProbeResult (
|
||||||
|
val streams:MutableList<AudioProbeStream>,
|
||||||
|
val chapters:MutableList<AudioProbeChapter>,
|
||||||
|
val format:AudioProbeFormat) {
|
||||||
|
|
||||||
|
val duration get() = format.duration
|
||||||
|
val size get() = format.size
|
||||||
|
val title get() = format.tags.title ?: format.filename.split("/").last()
|
||||||
|
val artist get() = format.tags.artist ?: ""
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
|
||||||
import com.fasterxml.jackson.annotation.*
|
import com.fasterxml.jackson.annotation.*
|
||||||
import com.google.android.exoplayer2.MediaMetadata
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LibraryItem(
|
data class LibraryItem(
|
||||||
|
@ -23,7 +20,7 @@ data class LibraryItem(
|
||||||
var isMissing:Boolean,
|
var isMissing:Boolean,
|
||||||
var isInvalid:Boolean,
|
var isInvalid:Boolean,
|
||||||
var mediaType:String,
|
var mediaType:String,
|
||||||
var media:MediaEntity,
|
var media:MediaType,
|
||||||
var libraryFiles:MutableList<LibraryFile>
|
var libraryFiles:MutableList<LibraryFile>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,7 +30,7 @@ data class LibraryItem(
|
||||||
JsonSubTypes.Type(Book::class),
|
JsonSubTypes.Type(Book::class),
|
||||||
JsonSubTypes.Type(Podcast::class)
|
JsonSubTypes.Type(Podcast::class)
|
||||||
)
|
)
|
||||||
open class MediaEntity {}
|
open class MediaType {}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Podcast(
|
data class Podcast(
|
||||||
|
@ -42,15 +39,15 @@ data class Podcast(
|
||||||
var tags:MutableList<String>,
|
var tags:MutableList<String>,
|
||||||
var episodes:MutableList<PodcastEpisode>,
|
var episodes:MutableList<PodcastEpisode>,
|
||||||
var autoDownloadEpisodes:Boolean
|
var autoDownloadEpisodes:Boolean
|
||||||
) : MediaEntity()
|
) : MediaType()
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Book(
|
data class Book(
|
||||||
var metadata:BookMetadata,
|
var metadata:BookMetadata,
|
||||||
var coverPath:String?,
|
var coverPath:String?,
|
||||||
var tags:MutableList<String>,
|
var tags:MutableList<String>,
|
||||||
var audiobooks:MutableList<Audiobook>
|
var audioFiles:MutableList<AudioFile>
|
||||||
) : MediaEntity()
|
) : MediaType()
|
||||||
|
|
||||||
// This auto-detects whether it is a Book or Podcast
|
// This auto-detects whether it is a Book or Podcast
|
||||||
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
||||||
|
@ -58,14 +55,29 @@ data class Book(
|
||||||
JsonSubTypes.Type(BookMetadata::class),
|
JsonSubTypes.Type(BookMetadata::class),
|
||||||
JsonSubTypes.Type(PodcastMetadata::class)
|
JsonSubTypes.Type(PodcastMetadata::class)
|
||||||
)
|
)
|
||||||
open class MediaEntityMetadata {}
|
open class MediaTypeMetadata {}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class BookMetadata(
|
data class BookMetadata(
|
||||||
var title:String,
|
var title:String,
|
||||||
var subtitle:String?,
|
var subtitle:String?,
|
||||||
var authors:MutableList<Author>
|
var authors:MutableList<Author>,
|
||||||
) : MediaEntityMetadata()
|
var narrators:MutableList<String>,
|
||||||
|
var genres:MutableList<String>,
|
||||||
|
var publishedYear:String?,
|
||||||
|
var publishedDate:String?,
|
||||||
|
var publisher:String?,
|
||||||
|
var description:String?,
|
||||||
|
var isbn:String?,
|
||||||
|
var asin:String?,
|
||||||
|
var language:String?,
|
||||||
|
var explicit:Boolean,
|
||||||
|
// In toJSONExpanded
|
||||||
|
var authorName:String?,
|
||||||
|
var authorNameLF:String?,
|
||||||
|
var narratorName:String?,
|
||||||
|
var seriesName:String?
|
||||||
|
) : MediaTypeMetadata()
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class PodcastMetadata(
|
data class PodcastMetadata(
|
||||||
|
@ -73,7 +85,7 @@ data class PodcastMetadata(
|
||||||
var author:String?,
|
var author:String?,
|
||||||
var feedUrl:String,
|
var feedUrl:String,
|
||||||
var genres:MutableList<String>
|
var genres:MutableList<String>
|
||||||
) : MediaEntityMetadata()
|
) : MediaTypeMetadata()
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Author(
|
data class Author(
|
||||||
|
@ -82,14 +94,6 @@ data class Author(
|
||||||
var coverPath:String?
|
var coverPath:String?
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
data class Audiobook(
|
|
||||||
var id:String,
|
|
||||||
var index:Int,
|
|
||||||
var name:String,
|
|
||||||
var audioFiles:MutableList<AudioFile>
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class PodcastEpisode(
|
data class PodcastEpisode(
|
||||||
var id:String,
|
var id:String,
|
||||||
|
@ -138,68 +142,6 @@ data class Folder(
|
||||||
var fullPath:String
|
var fullPath:String
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
class PlaybackSession(
|
|
||||||
var id:String,
|
|
||||||
var userId:String,
|
|
||||||
var libraryItemId:String,
|
|
||||||
var mediaEntityId:String,
|
|
||||||
var mediaType:String,
|
|
||||||
var mediaMetadata:MediaEntityMetadata,
|
|
||||||
var duration:Double,
|
|
||||||
var playMethod:Int,
|
|
||||||
var audioTracks:MutableList<AudioTrack>,
|
|
||||||
var currentTime:Double,
|
|
||||||
var serverUrl:String,
|
|
||||||
var token:String
|
|
||||||
) {
|
|
||||||
fun getTitle():String {
|
|
||||||
var metadata = mediaMetadata as BookMetadata
|
|
||||||
return metadata.title
|
|
||||||
}
|
|
||||||
fun getAuthor():String {
|
|
||||||
var metadata = mediaMetadata as BookMetadata
|
|
||||||
return metadata.authors.joinToString(",") { it.name }
|
|
||||||
}
|
|
||||||
fun getContentUri():String {
|
|
||||||
// TODO: Using Uri.parse here is throwing error with jackson
|
|
||||||
var audioTrack = audioTracks[0]
|
|
||||||
return "$serverUrl${audioTrack.contentUrl}?token=$token"
|
|
||||||
}
|
|
||||||
fun getMimeType():String {
|
|
||||||
var audioTrack = audioTracks[0]
|
|
||||||
return audioTrack.mimeType
|
|
||||||
}
|
|
||||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
|
||||||
var metadata = mediaMetadata as BookMetadata
|
|
||||||
|
|
||||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, metadata.title)
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, metadata.title)
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, metadata.authors.joinToString(",") { it.name })
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, metadata.authors.joinToString(",") { it.name })
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, metadata.authors.joinToString(",") { it.name })
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
|
||||||
return metadataBuilder.build()
|
|
||||||
}
|
|
||||||
fun getMediaMetadata(): MediaMetadata {
|
|
||||||
var metadata = mediaMetadata as BookMetadata
|
|
||||||
var authorName = metadata.authors.joinToString(",") { it.name }
|
|
||||||
var metadataBuilder = MediaMetadata.Builder()
|
|
||||||
.setTitle(metadata.title)
|
|
||||||
.setDisplayTitle(metadata.title)
|
|
||||||
.setArtist(authorName)
|
|
||||||
.setAlbumArtist(authorName)
|
|
||||||
.setSubtitle(authorName)
|
|
||||||
|
|
||||||
// var contentUri = this.getContentUri()
|
|
||||||
// metadataBuilder.setMediaUri(contentUri)
|
|
||||||
|
|
||||||
return metadataBuilder.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class AudioTrack(
|
data class AudioTrack(
|
||||||
var index:Int,
|
var index:Int,
|
||||||
|
@ -207,5 +149,6 @@ data class AudioTrack(
|
||||||
var duration:Double,
|
var duration:Double,
|
||||||
var title:String,
|
var title:String,
|
||||||
var contentUrl:String,
|
var contentUrl:String,
|
||||||
var mimeType:String
|
var mimeType:String,
|
||||||
|
var isLocal:Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,6 +13,15 @@ import org.json.JSONObject
|
||||||
class DbManager : Plugin() {
|
class DbManager : Plugin() {
|
||||||
val tag = "DbManager"
|
val tag = "DbManager"
|
||||||
|
|
||||||
|
fun loadDeviceData():DeviceData {
|
||||||
|
var deviceData:DeviceData? = Paper.book("device").read("data")
|
||||||
|
return deviceData ?: DeviceData(mutableListOf(),null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDeviceData(deviceData:DeviceData) {
|
||||||
|
Paper.book("device").write("data", deviceData)
|
||||||
|
}
|
||||||
|
|
||||||
fun saveObject(db:String, key:String, value:JSONObject) {
|
fun saveObject(db:String, key:String, value:JSONObject) {
|
||||||
Log.d(tag, "Saving Object $key ${value.toString()}")
|
Log.d(tag, "Saving Object $key ${value.toString()}")
|
||||||
Paper.book(db).write(key, value)
|
Paper.book(db).write(key, value)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.getcapacitor.JSObject
|
||||||
|
|
||||||
|
data class ServerConfig(
|
||||||
|
var id:String,
|
||||||
|
var index:Int,
|
||||||
|
var name:String,
|
||||||
|
var address:String,
|
||||||
|
var username:String,
|
||||||
|
var token:String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DeviceData(
|
||||||
|
var serverConfigs:MutableList<ServerConfig>,
|
||||||
|
var lastServerConfigId:String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LocalMediaItem(
|
||||||
|
val name: String,
|
||||||
|
val simplePath: String,
|
||||||
|
val audioTracks:MutableList<AudioTrack>
|
||||||
|
)
|
|
@ -0,0 +1,99 @@
|
||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
import com.audiobookshelf.app.R
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import com.google.android.exoplayer2.MediaMetadata
|
||||||
|
|
||||||
|
val PLAYMETHOD_DIRECTPLAY = 0
|
||||||
|
val PLAYMETHOD_DIRECTSTREAM = 1
|
||||||
|
val PLAYMETHOD_TRANSCODE = 2
|
||||||
|
val PLAYMETHOD_LOCAL = 3
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class PlaybackSession(
|
||||||
|
var id:String,
|
||||||
|
var userId:String,
|
||||||
|
var libraryItemId:String,
|
||||||
|
var episodeId:String,
|
||||||
|
var mediaEntityId:String,
|
||||||
|
var mediaType:String,
|
||||||
|
var mediaMetadata:MediaTypeMetadata,
|
||||||
|
var coverPath:String?,
|
||||||
|
var duration:Double,
|
||||||
|
var playMethod:Int,
|
||||||
|
var audioTracks:MutableList<AudioTrack>,
|
||||||
|
var currentTime:Double,
|
||||||
|
var libraryItem:LibraryItem,
|
||||||
|
var serverUrl:String,
|
||||||
|
var token:String
|
||||||
|
) {
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getIsHls():Boolean {
|
||||||
|
return playMethod == PLAYMETHOD_TRANSCODE
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getTitle():String {
|
||||||
|
if (mediaMetadata == null) return "Unset"
|
||||||
|
var metadata = mediaMetadata as BookMetadata
|
||||||
|
return metadata.title
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getAuthor():String {
|
||||||
|
if (mediaMetadata == null) return "Unset"
|
||||||
|
var metadata = mediaMetadata as BookMetadata
|
||||||
|
return metadata.authors.joinToString(",") { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getCoverUri(): Uri {
|
||||||
|
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||||
|
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getContentUri(): Uri {
|
||||||
|
var audioTrack = audioTracks[0]
|
||||||
|
return Uri.parse("$serverUrl${audioTrack.contentUrl}?token=$token")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getMimeType():String {
|
||||||
|
var audioTrack = audioTracks[0]
|
||||||
|
return audioTrack.mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
||||||
|
var metadataBuilder = MediaMetadataCompat.Builder()
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, this.getTitle())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, getTitle())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, this.getAuthor())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, this.getAuthor())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, this.getAuthor())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
return metadataBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getExoMediaMetadata(): MediaMetadata {
|
||||||
|
var authorName = this.getAuthor()
|
||||||
|
var metadataBuilder = MediaMetadata.Builder()
|
||||||
|
.setTitle(this.getTitle())
|
||||||
|
.setDisplayTitle(this.getTitle())
|
||||||
|
.setArtist(authorName)
|
||||||
|
.setAlbumArtist(authorName)
|
||||||
|
.setSubtitle(authorName)
|
||||||
|
|
||||||
|
var contentUri = this.getContentUri()
|
||||||
|
metadataBuilder.setMediaUri(contentUri)
|
||||||
|
|
||||||
|
return metadataBuilder.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.audiobookshelf.app.device
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.data.DbManager
|
||||||
|
import com.audiobookshelf.app.data.DeviceData
|
||||||
|
import com.audiobookshelf.app.data.ServerConfig
|
||||||
|
|
||||||
|
object DeviceManager {
|
||||||
|
val tag = "DeviceManager"
|
||||||
|
val dbManager:DbManager = DbManager()
|
||||||
|
var deviceData:DeviceData = dbManager.loadDeviceData()
|
||||||
|
var currentServerConfig: ServerConfig? = null
|
||||||
|
|
||||||
|
val serverAddress get() = currentServerConfig?.address ?: ""
|
||||||
|
val token get() = currentServerConfig?.token ?: ""
|
||||||
|
|
||||||
|
init {
|
||||||
|
Log.d(tag, "Device Manager Singleton invoked")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.audiobookshelf.app.device
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.FFprobeSession
|
||||||
|
import com.arthenica.ffmpegkit.Level
|
||||||
|
import com.audiobookshelf.app.data.AudioProbeResult
|
||||||
|
import com.audiobookshelf.app.data.AudioTrack
|
||||||
|
import com.audiobookshelf.app.data.LocalMediaItem
|
||||||
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
|
||||||
|
|
||||||
|
class FolderScanner(var ctx: Context) {
|
||||||
|
private val tag = "FolderScanner"
|
||||||
|
|
||||||
|
fun scanForAudiobooks(folderUrl: String):MutableList<LocalMediaItem> {
|
||||||
|
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(folderUrl))
|
||||||
|
|
||||||
|
if (df == null) {
|
||||||
|
Log.e(tag, "Folder Doc File Invalid $folderUrl")
|
||||||
|
return mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaFolders = mutableListOf<LocalMediaItem>()
|
||||||
|
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||||
|
|
||||||
|
foldersFound.forEach {
|
||||||
|
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
|
||||||
|
var folderName = it.name ?: ""
|
||||||
|
var mediaFiles = mutableListOf<LocalMediaItem>()
|
||||||
|
|
||||||
|
var audioTracks = mutableListOf<AudioTrack>()
|
||||||
|
var index = 1
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
if (isAudio) {
|
||||||
|
var absolutePath = it2.getAbsolutePath(ctx)
|
||||||
|
Log.d(tag, "Audio File Path $absolutePath")
|
||||||
|
|
||||||
|
// TODO: Make asynchronous
|
||||||
|
var session = FFprobeKit.execute("-i \"$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)
|
||||||
|
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||||
|
|
||||||
|
var track = AudioTrack(index, 0.0, 0.0, filename, absolutePath, mimeType, true)
|
||||||
|
audioTracks.add(track)
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "Found non audio file $filename")
|
||||||
|
}
|
||||||
|
// var imageFile = StorageManager.MediaFile(it2.uri, filename, it2.getSimplePath(context), it2.length(), mimeType, isAudio)
|
||||||
|
// mediaFiles.add(imageFile)
|
||||||
|
}
|
||||||
|
if (mediaFiles.size > 0) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaFolders
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.Library
|
import com.audiobookshelf.app.data.Library
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
|
import com.audiobookshelf.app.data.MediaTypeMetadata
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
ext {
|
ext {
|
||||||
minSdkVersion = 23
|
minSdkVersion = 24
|
||||||
compileSdkVersion = 30
|
compileSdkVersion = 30
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 30
|
||||||
androidxActivityVersion = '1.2.0'
|
androidxActivityVersion = '1.2.0'
|
||||||
|
|
|
@ -31,13 +31,13 @@
|
||||||
|
|
||||||
<div class="cover-wrapper absolute z-30 pointer-events-auto" :class="bookCoverAspectRatio === 1 ? 'square-cover' : ''" @click="clickContainer">
|
<div class="cover-wrapper absolute z-30 pointer-events-auto" :class="bookCoverAspectRatio === 1 ? 'square-cover' : ''" @click="clickContainer">
|
||||||
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
|
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
|
||||||
<covers-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :download-cover="downloadedCover" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden">
|
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden">
|
||||||
<p class="title-text font-book truncate">{{ title }}</p>
|
<p class="title-text font-book truncate">{{ title }}</p>
|
||||||
<p class="author-text text-white text-opacity-75 truncate">by {{ authorFL }}</p>
|
<p class="author-text text-white text-opacity-75 truncate">by {{ authorName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
||||||
|
@ -58,19 +58,19 @@
|
||||||
|
|
||||||
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||||
<span class="material-icons jump-icon text-white cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
|
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
|
||||||
<div class="play-btn cursor-pointer shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
<div class="play-btn cursor-pointer shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||||
<span v-if="!loading" class="material-icons">{{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}</span>
|
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}</span>
|
||||||
<widgets-spinner-icon v-else class="h-8 w-8" />
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons jump-icon text-white cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="forward10">forward_10</span>
|
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="forward10">forward_10</span>
|
||||||
<span v-show="showFullscreen" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !loading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
<span v-show="showFullscreen" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
||||||
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="loading ? 'animate-pulse' : ''" @click="clickTrack">
|
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="isLoading ? 'animate-pulse' : ''" @click="clickTrack">
|
||||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
@ -93,7 +93,11 @@ import MyNativeAudio from '@/plugins/my-native-audio'
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
playing: Boolean,
|
playing: Boolean,
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
mediaEntity: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
|
@ -105,7 +109,6 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
loading: Boolean,
|
|
||||||
sleepTimerRunning: Boolean,
|
sleepTimerRunning: Boolean,
|
||||||
sleepTimeRemaining: Number
|
sleepTimeRemaining: Number
|
||||||
},
|
},
|
||||||
|
@ -140,7 +143,8 @@ export default {
|
||||||
listenTimeInterval: null,
|
listenTimeInterval: null,
|
||||||
listeningTimeSinceLastUpdate: 0,
|
listeningTimeSinceLastUpdate: 0,
|
||||||
totalListeningTimeInSession: 0,
|
totalListeningTimeInSession: 0,
|
||||||
useChapterTrack: false
|
useChapterTrack: false,
|
||||||
|
isLoading: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -175,17 +179,20 @@ export default {
|
||||||
}
|
}
|
||||||
return this.showFullscreen ? 200 : 60
|
return this.showFullscreen ? 200 : 60
|
||||||
},
|
},
|
||||||
book() {
|
media() {
|
||||||
return this.audiobook.book || {}
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title
|
return this.mediaMetadata.title
|
||||||
},
|
},
|
||||||
authorFL() {
|
authorName() {
|
||||||
return this.book.authorFL
|
return this.mediaMetadata.authorName
|
||||||
},
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
return (this.audiobook ? this.audiobook.chapters || [] : []).map((chapter) => {
|
return (this.mediaEntity ? this.mediaEntity.chapters || [] : []).map((chapter) => {
|
||||||
var chap = { ...chapter }
|
var chap = { ...chapter }
|
||||||
chap.start = Number(chap.start)
|
chap.start = Number(chap.start)
|
||||||
chap.end = Number(chap.end)
|
chap.end = Number(chap.end)
|
||||||
|
@ -193,7 +200,7 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
currentChapter() {
|
currentChapter() {
|
||||||
if (!this.audiobook || !this.chapters.length) return null
|
if (!this.mediaEntity || !this.chapters.length) return null
|
||||||
return this.chapters.find((ch) => Number(Number(ch.start).toFixed(2)) <= this.currentTime && Number(Number(ch.end).toFixed(2)) > this.currentTime)
|
return this.chapters.find((ch) => Number(Number(ch.start).toFixed(2)) <= this.currentTime && Number(Number(ch.end).toFixed(2)) > this.currentTime)
|
||||||
},
|
},
|
||||||
nextChapter() {
|
nextChapter() {
|
||||||
|
@ -329,12 +336,12 @@ export default {
|
||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
},
|
},
|
||||||
jumpNextChapter() {
|
jumpNextChapter() {
|
||||||
if (this.loading) return
|
if (this.isLoading) return
|
||||||
if (!this.nextChapter) return
|
if (!this.nextChapter) return
|
||||||
this.seek(this.nextChapter.start)
|
this.seek(this.nextChapter.start)
|
||||||
},
|
},
|
||||||
jumpChapterStart() {
|
jumpChapterStart() {
|
||||||
if (this.loading) return
|
if (this.isLoading) return
|
||||||
if (!this.currentChapter) {
|
if (!this.currentChapter) {
|
||||||
return this.restart()
|
return this.restart()
|
||||||
}
|
}
|
||||||
|
@ -362,11 +369,11 @@ export default {
|
||||||
this.seek(0)
|
this.seek(0)
|
||||||
},
|
},
|
||||||
backward10() {
|
backward10() {
|
||||||
if (this.loading) return
|
if (this.isLoading) return
|
||||||
MyNativeAudio.seekBackward({ amount: '10000' })
|
MyNativeAudio.seekBackward({ amount: '10000' })
|
||||||
},
|
},
|
||||||
forward10() {
|
forward10() {
|
||||||
if (this.loading) return
|
if (this.isLoading) return
|
||||||
MyNativeAudio.seekForward({ amount: '10000' })
|
MyNativeAudio.seekForward({ amount: '10000' })
|
||||||
},
|
},
|
||||||
setStreamReady() {
|
setStreamReady() {
|
||||||
|
@ -461,7 +468,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
seek(time) {
|
seek(time) {
|
||||||
if (this.loading) return
|
if (this.isLoading) return
|
||||||
if (this.seekLoading) {
|
if (this.seekLoading) {
|
||||||
console.error('Already seek loading', this.seekedTime)
|
console.error('Already seek loading', this.seekedTime)
|
||||||
return
|
return
|
||||||
|
@ -482,7 +489,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickTrack(e) {
|
clickTrack(e) {
|
||||||
if (this.loading) return
|
if (this.isLoading) return
|
||||||
if (!this.showFullscreen) {
|
if (!this.showFullscreen) {
|
||||||
// Track not clickable on mini-player
|
// Track not clickable on mini-player
|
||||||
return
|
return
|
||||||
|
@ -504,7 +511,7 @@ export default {
|
||||||
this.seek(time)
|
this.seek(time)
|
||||||
},
|
},
|
||||||
playPauseClick() {
|
playPauseClick() {
|
||||||
if (this.loading) return
|
if (this.isLoading) return
|
||||||
if (this.isPaused) {
|
if (this.isPaused) {
|
||||||
console.log('playPause PLAY')
|
console.log('playPause PLAY')
|
||||||
this.play()
|
this.play()
|
||||||
|
@ -641,6 +648,7 @@ export default {
|
||||||
MyNativeAudio.terminateStream()
|
MyNativeAudio.terminateStream()
|
||||||
},
|
},
|
||||||
onPlayingUpdate(data) {
|
onPlayingUpdate(data) {
|
||||||
|
console.log('onPlayingUpdate', JSON.stringify(data))
|
||||||
this.isPaused = !data.value
|
this.isPaused = !data.value
|
||||||
if (!this.isPaused) {
|
if (!this.isPaused) {
|
||||||
this.startPlayInterval()
|
this.startPlayInterval()
|
||||||
|
@ -649,6 +657,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMetadata(data) {
|
onMetadata(data) {
|
||||||
|
console.log('onMetadata', JSON.stringify(data))
|
||||||
|
this.isLoading = false
|
||||||
|
|
||||||
this.totalDuration = Number((data.duration / 1000).toFixed(2))
|
this.totalDuration = Number((data.duration / 1000).toFixed(2))
|
||||||
this.$emit('setTotalDuration', this.totalDuration)
|
this.$emit('setTotalDuration', this.totalDuration)
|
||||||
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="audiobook" id="streamContainer">
|
<div v-if="libraryItemPlaying" id="streamContainer">
|
||||||
<app-audio-player
|
<app-audio-player
|
||||||
ref="audioPlayer"
|
ref="audioPlayer"
|
||||||
:playing.sync="isPlaying"
|
:playing.sync="isPlaying"
|
||||||
:audiobook="audiobook"
|
:library-item="libraryItemPlaying"
|
||||||
|
:media-entity="mediaEntityPlaying"
|
||||||
:download="download"
|
:download="download"
|
||||||
:loading="isLoading"
|
|
||||||
:bookmarks="bookmarks"
|
:bookmarks="bookmarks"
|
||||||
:sleep-timer-running="isSleepTimerRunning"
|
:sleep-timer-running="isSleepTimerRunning"
|
||||||
:sleep-time-remaining="sleepTimeRemaining"
|
:sleep-time-remaining="sleepTimeRemaining"
|
||||||
|
@ -69,6 +69,12 @@ export default {
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
|
libraryItemPlaying() {
|
||||||
|
return this.$store.state.globals.libraryItemPlaying
|
||||||
|
},
|
||||||
|
mediaEntityPlaying() {
|
||||||
|
return this.$store.state.globals.mediaEntityPlaying
|
||||||
|
},
|
||||||
userAudiobook() {
|
userAudiobook() {
|
||||||
if (!this.audiobookId) return
|
if (!this.audiobookId) return
|
||||||
return this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
|
return this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
|
||||||
|
@ -84,11 +90,6 @@ export default {
|
||||||
socketConnected() {
|
socketConnected() {
|
||||||
return this.$store.state.socketConnected
|
return this.$store.state.socketConnected
|
||||||
},
|
},
|
||||||
isLoading() {
|
|
||||||
if (this.playingDownload) return false
|
|
||||||
if (!this.streamAudiobook) return false
|
|
||||||
return !this.stream || this.streamAudiobook.id !== this.stream.audiobook.id
|
|
||||||
},
|
|
||||||
playingDownload() {
|
playingDownload() {
|
||||||
return this.$store.state.playingDownload
|
return this.$store.state.playingDownload
|
||||||
},
|
},
|
||||||
|
@ -476,13 +477,15 @@ export default {
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
this.$store.commit('setLibraryItemStream', libraryItem)
|
this.$store.commit('globals/setLibraryItemPlaying', libraryItem)
|
||||||
|
|
||||||
// TODO: Call load library item in native
|
MyNativeAudio.prepareLibraryItem({ libraryItemId, playWhenReady: true })
|
||||||
console.log('TEST prepare library item', libraryItemId)
|
.then((data) => {
|
||||||
MyNativeAudio.prepareLibraryItem({ libraryItemId }).then((data) => {
|
|
||||||
console.log('TEST library item play response', JSON.stringify(data))
|
console.log('TEST library item play response', JSON.stringify(data))
|
||||||
}).catch((error) => {
|
var mediaEntity = data.mediaEntity
|
||||||
|
this.$store.commit('globals/setMediaEntityPlaying', mediaEntity)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
console.error('TEST failed', error)
|
console.error('TEST failed', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }">
|
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }">
|
||||||
<template v-for="(entity, index) in entities">
|
<template v-for="(entity, index) in entities">
|
||||||
<cards-lazy-book-card v-if="type === 'book'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
<cards-lazy-book-card v-if="type === 'book' || type === 'podcast'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
||||||
<cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
<cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,8 +52,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
index: Number,
|
index: Number,
|
||||||
|
@ -126,12 +124,10 @@ export default {
|
||||||
return this._libraryItem.libraryId
|
return this._libraryItem.libraryId
|
||||||
},
|
},
|
||||||
hasEbook() {
|
hasEbook() {
|
||||||
if (!this.media.ebooks) return 0
|
return this.media.ebookFile
|
||||||
return this.media.ebooks.length
|
|
||||||
},
|
},
|
||||||
hasAudiobook() {
|
numTracks() {
|
||||||
if (!this.media.audiobooks) return 0
|
return this.media.numTracks
|
||||||
return this.media.audiobooks.length
|
|
||||||
},
|
},
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
|
@ -211,7 +207,7 @@ export default {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasAudiobook && !this.isStreaming
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||||
|
|
|
@ -29,14 +29,6 @@ export default {
|
||||||
processingRemove: false
|
processingRemove: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
userIsRead: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newVal) {
|
|
||||||
this.isRead = newVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
|
@ -71,15 +63,6 @@ export default {
|
||||||
},
|
},
|
||||||
showPlayBtn() {
|
showPlayBtn() {
|
||||||
return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks
|
return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks
|
||||||
},
|
|
||||||
userAudiobooks() {
|
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
|
||||||
},
|
|
||||||
userAudiobook() {
|
|
||||||
return this.userAudiobooks[this.book.id] || null
|
|
||||||
},
|
|
||||||
userIsRead() {
|
|
||||||
return this.userAudiobook ? !!this.userAudiobook.isRead : false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -89,39 +72,6 @@ export default {
|
||||||
},
|
},
|
||||||
clickEdit() {
|
clickEdit() {
|
||||||
this.$emit('edit', this.book)
|
this.$emit('edit', this.book)
|
||||||
},
|
|
||||||
toggleRead() {
|
|
||||||
var updatePayload = {
|
|
||||||
isRead: !this.isRead
|
|
||||||
}
|
|
||||||
this.isProcessingReadUpdate = true
|
|
||||||
this.$axios
|
|
||||||
.$patch(`/api/me/audiobook/${this.book.id}`, updatePayload)
|
|
||||||
.then(() => {
|
|
||||||
this.isProcessingReadUpdate = false
|
|
||||||
this.$toast.success(`"${this.bookTitle}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
this.isProcessingReadUpdate = false
|
|
||||||
this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
removeClick() {
|
|
||||||
this.processingRemove = true
|
|
||||||
|
|
||||||
this.$axios
|
|
||||||
.$delete(`/api/collections/${this.collectionId}/book/${this.book.id}`)
|
|
||||||
.then((updatedCollection) => {
|
|
||||||
console.log(`Book removed from collection`, updatedCollection)
|
|
||||||
this.$toast.success('Book removed from collection')
|
|
||||||
this.processingRemove = false
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to remove book from collection', error)
|
|
||||||
this.$toast.error('Failed to remove book from collection')
|
|
||||||
this.processingRemove = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|
|
@ -9,14 +9,14 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
pod 'CapacitorCommunitySqlite', :path => '..\..\node_modules\@capacitor-community\sqlite'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||||
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
|
pod 'RobingenzCapacitorAppUpdate', :path => '..\..\node_modules\@robingenz\capacitor-app-update'
|
||||||
pod 'CapacitorDataStorageSqlite', :path => '../../node_modules/capacitor-data-storage-sqlite'
|
pod 'CapacitorDataStorageSqlite', :path => '..\..\node_modules\capacitor-data-storage-sqlite'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|
|
@ -269,7 +269,7 @@ export default {
|
||||||
var downloadFolder = await this.$localStore.getDownloadFolder()
|
var downloadFolder = await this.$localStore.getDownloadFolder()
|
||||||
|
|
||||||
if (downloadFolder) {
|
if (downloadFolder) {
|
||||||
await this.syncDownloads(downloads, downloadFolder)
|
// await this.syncDownloads(downloads, downloadFolder)
|
||||||
}
|
}
|
||||||
this.$eventBus.$emit('downloads-loaded')
|
this.$eventBus.$emit('downloads-loaded')
|
||||||
|
|
||||||
|
|
17818
package-lock.json
generated
17818
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -22,7 +22,6 @@
|
||||||
"@capacitor/storage": "^1.1.0",
|
"@capacitor/storage": "^1.1.0",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||||
"axios": "^0.21.1",
|
|
||||||
"capacitor-data-storage-sqlite": "^3.2.0",
|
"capacitor-data-storage-sqlite": "^3.2.0",
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
|
|
|
@ -134,16 +134,13 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async changeDownloadFolderClick() {
|
async changeDownloadFolderClick() {
|
||||||
if (!this.hasStoragePermission) {
|
if (!this.hasStoragePermission) {
|
||||||
console.log('Requesting Storage Permission')
|
|
||||||
StorageManager.requestStoragePermission()
|
StorageManager.requestStoragePermission()
|
||||||
} else {
|
} else {
|
||||||
var folderObj = await StorageManager.selectFolder()
|
var folderObj = await StorageManager.selectFolder()
|
||||||
if (folderObj.error) {
|
if (folderObj.error) {
|
||||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
|
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
|
||||||
console.log('Storage Permission check folder ' + permissionsGood)
|
|
||||||
|
|
||||||
if (!permissionsGood) {
|
if (!permissionsGood) {
|
||||||
this.$toast.error('Folder permissions failed')
|
this.$toast.error('Folder permissions failed')
|
||||||
|
|
|
@ -115,20 +115,11 @@ export default {
|
||||||
series() {
|
series() {
|
||||||
return this.mediaMetadata.series || []
|
return this.mediaMetadata.series || []
|
||||||
},
|
},
|
||||||
audiobooks() {
|
|
||||||
return this.media.audiobooks || []
|
|
||||||
},
|
|
||||||
defaultAudiobook() {
|
|
||||||
if (!this.audiobooks.length) return null
|
|
||||||
return this.audiobooks[0]
|
|
||||||
},
|
|
||||||
duration() {
|
duration() {
|
||||||
if (!this.defaultAudiobook) return 0
|
return this.media.duration
|
||||||
return this.defaultAudiobook.duration
|
|
||||||
},
|
},
|
||||||
size() {
|
size() {
|
||||||
if (!this.defaultAudiobook) return 0
|
return this.media.size
|
||||||
return this.defaultAudiobook.size
|
|
||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
@ -160,8 +151,8 @@ export default {
|
||||||
return this.$store.getters['isAudiobookPlaying'](this.libraryItemId)
|
return this.$store.getters['isAudiobookPlaying'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
numTracks() {
|
numTracks() {
|
||||||
if (!this.defaultAudiobook) return 0
|
if (!this.media.tracks) return 0
|
||||||
return this.defaultAudiobook.tracks.length || 0
|
return this.media.tracks.length || 0
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.libraryItem.isMissing
|
return this.libraryItem.isMissing
|
||||||
|
@ -173,17 +164,17 @@ export default {
|
||||||
return this.downloadObj ? this.downloadObj.isDownloading : false
|
return this.downloadObj ? this.downloadObj.isDownloading : false
|
||||||
},
|
},
|
||||||
showPlay() {
|
showPlay() {
|
||||||
return !this.isMissing && !this.isIncomplete && this.defaultAudiobook
|
return !this.isMissing && !this.isIncomplete && this.numTracks
|
||||||
},
|
},
|
||||||
showRead() {
|
showRead() {
|
||||||
return this.ebooks.length && this.ebookFormat !== '.pdf'
|
return this.ebookFile && this.ebookFormat !== '.pdf'
|
||||||
},
|
},
|
||||||
ebooks() {
|
ebookFile() {
|
||||||
return this.media.ebooks || []
|
return this.media.ebookFile
|
||||||
},
|
},
|
||||||
ebookFormat() {
|
ebookFormat() {
|
||||||
if (!this.ebooks.length) return null
|
if (!this.ebookFile) return null
|
||||||
return this.ebooks[0].ebookFile.ebookFormat
|
return this.ebookFile.ebookFormat
|
||||||
},
|
},
|
||||||
isDownloadPreparing() {
|
isDownloadPreparing() {
|
||||||
return this.downloadObj ? this.downloadObj.isPreparing : false
|
return this.downloadObj ? this.downloadObj.isPreparing : false
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Server from '../Server'
|
import Server from '../Server'
|
||||||
|
|
||||||
export default function ({ store }, inject) {
|
export default function ({ store, $axios }, inject) {
|
||||||
inject('server', new Server(store))
|
inject('server', new Server(store, $axios))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
|
libraryItemPlaying: null,
|
||||||
|
mediaEntityPlaying: null
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -28,5 +29,10 @@ export const actions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setLibraryItemPlaying(state, libraryItem) {
|
||||||
|
state.libraryItemPlaying = libraryItem
|
||||||
|
},
|
||||||
|
setMediaEntityPlaying(state, mediaEntity) {
|
||||||
|
state.mediaEntityPlaying = mediaEntity
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ import Vue from 'vue'
|
||||||
import { Network } from '@capacitor/network'
|
import { Network } from '@capacitor/network'
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
streamLibraryItem: null,
|
|
||||||
streamAudiobook: null,
|
streamAudiobook: null,
|
||||||
playingDownload: null,
|
playingDownload: null,
|
||||||
playOnLoad: false,
|
playOnLoad: false,
|
||||||
|
@ -81,9 +80,6 @@ export const mutations = {
|
||||||
setPlayOnLoad(state, val) {
|
setPlayOnLoad(state, val) {
|
||||||
state.playOnLoad = val
|
state.playOnLoad = val
|
||||||
},
|
},
|
||||||
setLibraryItemStream(state, libraryItem) {
|
|
||||||
state.streamLibraryItem = libraryItem
|
|
||||||
},
|
|
||||||
setStreamAudiobook(state, audiobook) {
|
setStreamAudiobook(state, audiobook) {
|
||||||
if (audiobook) {
|
if (audiobook) {
|
||||||
state.playingDownload = null
|
state.playingDownload = null
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue