New data model classes, ffmpeg-kit, jackson json deserializer, add permission

This commit is contained in:
advplyr 2022-03-28 19:53:53 -05:00
parent 461733854a
commit 4fc70cd3dd
30 changed files with 9058 additions and 9642 deletions

View file

@ -1,13 +1,13 @@
import { io } from 'socket.io-client'
import { Storage } from '@capacitor/storage'
import axios from 'axios'
import EventEmitter from 'events'
class Server extends EventEmitter {
constructor(store) {
constructor(store, $axios) {
super()
this.store = store
this.$axios = $axios
this.url = null
this.socket = null
@ -119,7 +119,7 @@ class Server extends EventEmitter {
async login(url, username, password) {
var serverUrl = this.getServerUrl(url)
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) {
console.error(res.data.error)
return {
@ -160,7 +160,7 @@ class Server extends EventEmitter {
authorize(serverUrl, token) {
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
}).catch(error => {
console.error('[Server] Server auth failed', error)
@ -181,7 +181,7 @@ class Server extends EventEmitter {
ping(url) {
var pingUrl = url + '/ping'
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
}).catch(error => {
console.error('Server check failed', error)

View file

@ -88,6 +88,9 @@ dependencies {
// Jackson for JSON
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'

View file

@ -7,7 +7,7 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<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" />
<application

View file

@ -1,9 +1,12 @@
package com.audiobookshelf.app
import android.Manifest
import android.app.DownloadManager
import android.content.*
import android.content.pm.PackageManager
import android.os.*
import android.util.Log
import androidx.core.app.ActivityCompat
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper
import com.audiobookshelf.app.data.DbManager
@ -24,6 +27,11 @@ class MainActivity : BridgeActivity() {
val storageHelper = SimpleStorageHelper(this)
val storage = SimpleStorage(this)
val REQUEST_PERMISSIONS = 1
var PERMISSIONS_ALL = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE
)
val broadcastReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
@ -43,6 +51,14 @@ class MainActivity : BridgeActivity() {
super.onCreate(savedInstanceState)
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(AudioDownloader::class.java)
registerPlugin(StorageManager::class.java)

View file

@ -67,12 +67,13 @@ class MyNativeAudio : Plugin() {
fun prepareLibraryItem(call: PluginCall) {
var libraryItemId = call.getString("libraryItemId", "").toString()
var mediaEntityId = call.getString("mediaEntityId", "").toString()
var playWhenReady = call.getBoolean("playWhenReady") == true
apiHandler.playLibraryItem(libraryItemId) {
Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Player TEST ${jacksonObjectMapper().writeValueAsString(it)}")
playerNotificationService.preparePlayer(it)
playerNotificationService.preparePlayer(it, playWhenReady)
}
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it)))

View file

@ -381,8 +381,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
.setMediaId(currentPlaybackSession!!.id)
.setTitle(currentPlaybackSession!!.getTitle())
.setSubtitle(currentPlaybackSession!!.getAuthor())
// .setMediaUri(currentPlaybackSession!!.getContentUri())
// .setIconUri(currentAudiobookStreamData!!.)
.setMediaUri(currentPlaybackSession!!.getContentUri())
.setIconUri(currentPlaybackSession!!.getCoverUri())
return builder.build()
}
}
@ -666,21 +666,44 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
/*
User callable methods
*/
fun preparePlayer(playbackSession: PlaybackSession) {
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) {
currentPlaybackSession = playbackSession
var metadata = playbackSession.getMediaMetadataCompat()
mediaSession.setMetadata(metadata)
var mediaMetadata = playbackSession.getMediaMetadata()
var mediaUrl = playbackSession.getContentUri()
var mimeType = playbackSession.getMimeType()
Log.d(tag, "Media URL $mediaUrl")
var mediaUri = Uri.parse(mediaUrl)
var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
var mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
mPlayer.setMediaSource(mediaSource, 0L)
mPlayer.prepare()
mPlayer.playWhenReady = true
var mediaMetadata = playbackSession.getExoMediaMetadata()
// var mediaUri = playbackSession.getContentUri()
// var mimeType = playbackSession.getMimeType()
// var mediaItem = MediaItem.Builder().setUri(mediaUri).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
// var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
// var mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
// mPlayer.setMediaSource(mediaSource, 0L)
// 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) {

View file

@ -1,16 +1,20 @@
package com.audiobookshelf.app
import android.Manifest
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.callback.FolderPickerCallback
import com.anggrayudi.storage.callback.StorageAccessCallback
import com.anggrayudi.storage.file.*
import com.audiobookshelf.app.device.FolderScanner
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
@ -112,6 +116,7 @@ class StorageManager : Plugin() {
call.resolve(jsobj)
}
}
mainActivity.storage.openFolderPicker(6)
}
@ -126,12 +131,11 @@ class StorageManager : Plugin() {
@PluginMethod
fun checkStoragePermission(call: PluginCall) {
var res = false
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
res = SimpleStorage.hasStoragePermission(context)
Log.d(TAG, "Check Storage Access $res")
Log.d(TAG, "checkStoragePermission: Check Storage Access $res")
} else {
Log.d(TAG, "Has permission on Android 10 or up")
Log.d(TAG, "checkStoragePermission: Has permission on Android 10 or up")
res = true
}
@ -157,58 +161,63 @@ class StorageManager : Plugin() {
var folderUrl = call.data.getString("folderUrl", "").toString()
Log.d(TAG, "Searching folder $folderUrl")
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)
var folderScanner = FolderScanner(context)
var data = folderScanner.scanForAudiobooks(folderUrl)
Log.d(TAG, "Scan DATA $data")
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)
}

View file

@ -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 ?: ""
}

View file

@ -1,9 +1,6 @@
package com.audiobookshelf.app.data
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import com.fasterxml.jackson.annotation.*
import com.google.android.exoplayer2.MediaMetadata
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItem(
@ -23,7 +20,7 @@ data class LibraryItem(
var isMissing:Boolean,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaEntity,
var media:MediaType,
var libraryFiles:MutableList<LibraryFile>
)
@ -33,7 +30,7 @@ data class LibraryItem(
JsonSubTypes.Type(Book::class),
JsonSubTypes.Type(Podcast::class)
)
open class MediaEntity {}
open class MediaType {}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Podcast(
@ -42,15 +39,15 @@ data class Podcast(
var tags:MutableList<String>,
var episodes:MutableList<PodcastEpisode>,
var autoDownloadEpisodes:Boolean
) : MediaEntity()
) : MediaType()
@JsonIgnoreProperties(ignoreUnknown = true)
data class Book(
var metadata:BookMetadata,
var coverPath:String?,
var tags:MutableList<String>,
var audiobooks:MutableList<Audiobook>
) : MediaEntity()
var audioFiles:MutableList<AudioFile>
) : MediaType()
// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@ -58,14 +55,29 @@ data class Book(
JsonSubTypes.Type(BookMetadata::class),
JsonSubTypes.Type(PodcastMetadata::class)
)
open class MediaEntityMetadata {}
open class MediaTypeMetadata {}
@JsonIgnoreProperties(ignoreUnknown = true)
data class BookMetadata(
var title:String,
var subtitle:String?,
var authors:MutableList<Author>
) : MediaEntityMetadata()
var authors:MutableList<Author>,
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)
data class PodcastMetadata(
@ -73,7 +85,7 @@ data class PodcastMetadata(
var author:String?,
var feedUrl:String,
var genres:MutableList<String>
) : MediaEntityMetadata()
) : MediaTypeMetadata()
@JsonIgnoreProperties(ignoreUnknown = true)
data class Author(
@ -82,14 +94,6 @@ data class Author(
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)
data class PodcastEpisode(
var id:String,
@ -138,68 +142,6 @@ data class Folder(
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)
data class AudioTrack(
var index:Int,
@ -207,5 +149,6 @@ data class AudioTrack(
var duration:Double,
var title:String,
var contentUrl:String,
var mimeType:String
var mimeType:String,
var isLocal:Boolean
)

View file

@ -13,6 +13,15 @@ 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 saveDeviceData(deviceData:DeviceData) {
Paper.book("device").write("data", deviceData)
}
fun saveObject(db:String, key:String, value:JSONObject) {
Log.d(tag, "Saving Object $key ${value.toString()}")
Paper.book(db).write(key, value)

View file

@ -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>
)

View file

@ -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()
}
}

View file

@ -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")
}
}

View file

@ -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
}
}

View file

@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.util.Log
import com.audiobookshelf.app.data.Library
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.MediaTypeMetadata
import com.audiobookshelf.app.data.PlaybackSession
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

View file

@ -1,5 +1,5 @@
ext {
minSdkVersion = 23
minSdkVersion = 24
compileSdkVersion = 30
targetSdkVersion = 30
androidxActivityVersion = '1.2.0'

View file

@ -31,13 +31,13 @@
<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">
<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 class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden">
<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 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 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 class="material-icons jump-icon text-white cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</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="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">
<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" />
</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 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 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 && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div>
</div>
<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="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" />
@ -93,7 +93,11 @@ import MyNativeAudio from '@/plugins/my-native-audio'
export default {
props: {
playing: Boolean,
audiobook: {
libraryItem: {
type: Object,
default: () => {}
},
mediaEntity: {
type: Object,
default: () => {}
},
@ -105,7 +109,6 @@ export default {
type: Array,
default: () => []
},
loading: Boolean,
sleepTimerRunning: Boolean,
sleepTimeRemaining: Number
},
@ -140,7 +143,8 @@ export default {
listenTimeInterval: null,
listeningTimeSinceLastUpdate: 0,
totalListeningTimeInSession: 0,
useChapterTrack: false
useChapterTrack: false,
isLoading: true
}
},
computed: {
@ -175,17 +179,20 @@ export default {
}
return this.showFullscreen ? 200 : 60
},
book() {
return this.audiobook.book || {}
media() {
return this.libraryItem.media || {}
},
mediaMetadata() {
return this.media.metadata || {}
},
title() {
return this.book.title
return this.mediaMetadata.title
},
authorFL() {
return this.book.authorFL
authorName() {
return this.mediaMetadata.authorName
},
chapters() {
return (this.audiobook ? this.audiobook.chapters || [] : []).map((chapter) => {
return (this.mediaEntity ? this.mediaEntity.chapters || [] : []).map((chapter) => {
var chap = { ...chapter }
chap.start = Number(chap.start)
chap.end = Number(chap.end)
@ -193,7 +200,7 @@ export default {
})
},
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)
},
nextChapter() {
@ -329,12 +336,12 @@ export default {
this.forceCloseDropdownMenu()
},
jumpNextChapter() {
if (this.loading) return
if (this.isLoading) return
if (!this.nextChapter) return
this.seek(this.nextChapter.start)
},
jumpChapterStart() {
if (this.loading) return
if (this.isLoading) return
if (!this.currentChapter) {
return this.restart()
}
@ -362,11 +369,11 @@ export default {
this.seek(0)
},
backward10() {
if (this.loading) return
if (this.isLoading) return
MyNativeAudio.seekBackward({ amount: '10000' })
},
forward10() {
if (this.loading) return
if (this.isLoading) return
MyNativeAudio.seekForward({ amount: '10000' })
},
setStreamReady() {
@ -461,7 +468,7 @@ export default {
}
},
seek(time) {
if (this.loading) return
if (this.isLoading) return
if (this.seekLoading) {
console.error('Already seek loading', this.seekedTime)
return
@ -482,7 +489,7 @@ export default {
}
},
clickTrack(e) {
if (this.loading) return
if (this.isLoading) return
if (!this.showFullscreen) {
// Track not clickable on mini-player
return
@ -504,7 +511,7 @@ export default {
this.seek(time)
},
playPauseClick() {
if (this.loading) return
if (this.isLoading) return
if (this.isPaused) {
console.log('playPause PLAY')
this.play()
@ -641,6 +648,7 @@ export default {
MyNativeAudio.terminateStream()
},
onPlayingUpdate(data) {
console.log('onPlayingUpdate', JSON.stringify(data))
this.isPaused = !data.value
if (!this.isPaused) {
this.startPlayInterval()
@ -649,6 +657,9 @@ export default {
}
},
onMetadata(data) {
console.log('onMetadata', JSON.stringify(data))
this.isLoading = false
this.totalDuration = Number((data.duration / 1000).toFixed(2))
this.$emit('setTotalDuration', this.totalDuration)
this.currentTime = Number((data.currentTime / 1000).toFixed(2))

View file

@ -1,12 +1,12 @@
<template>
<div>
<div v-if="audiobook" id="streamContainer">
<div v-if="libraryItemPlaying" id="streamContainer">
<app-audio-player
ref="audioPlayer"
:playing.sync="isPlaying"
:audiobook="audiobook"
:library-item="libraryItemPlaying"
:media-entity="mediaEntityPlaying"
:download="download"
:loading="isLoading"
:bookmarks="bookmarks"
:sleep-timer-running="isSleepTimerRunning"
:sleep-time-remaining="sleepTimeRemaining"
@ -69,6 +69,12 @@ export default {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemPlaying() {
return this.$store.state.globals.libraryItemPlaying
},
mediaEntityPlaying() {
return this.$store.state.globals.mediaEntityPlaying
},
userAudiobook() {
if (!this.audiobookId) return
return this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
@ -84,11 +90,6 @@ export default {
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() {
return this.$store.state.playingDownload
},
@ -476,13 +477,15 @@ export default {
return null
})
if (!libraryItem) return
this.$store.commit('setLibraryItemStream', libraryItem)
this.$store.commit('globals/setLibraryItemPlaying', libraryItem)
// TODO: Call load library item in native
console.log('TEST prepare library item', libraryItemId)
MyNativeAudio.prepareLibraryItem({ libraryItemId }).then((data) => {
MyNativeAudio.prepareLibraryItem({ libraryItemId, playWhenReady: true })
.then((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)
})
}

View file

@ -2,7 +2,7 @@
<div class="w-full relative">
<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">
<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" />
</template>
</div>

View file

@ -52,8 +52,6 @@
</template>
<script>
import Vue from 'vue'
export default {
props: {
index: Number,
@ -126,12 +124,10 @@ export default {
return this._libraryItem.libraryId
},
hasEbook() {
if (!this.media.ebooks) return 0
return this.media.ebooks.length
return this.media.ebookFile
},
hasAudiobook() {
if (!this.media.audiobooks) return 0
return this.media.audiobooks.length
numTracks() {
return this.media.numTracks
},
processingBatch() {
return this.store.state.processingBatch
@ -211,7 +207,7 @@ export default {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasAudiobook && !this.isStreaming
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook

View file

@ -29,14 +29,6 @@ export default {
processingRemove: false
}
},
watch: {
userIsRead: {
immediate: true,
handler(newVal) {
this.isRead = newVal
}
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
@ -71,15 +63,6 @@ export default {
},
showPlayBtn() {
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: {
@ -89,39 +72,6 @@ export default {
},
clickEdit() {
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() {}

View file

@ -9,14 +9,14 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
pod 'CapacitorDataStorageSqlite', :path => '../../node_modules/capacitor-data-storage-sqlite'
pod 'CapacitorCommunitySqlite', :path => '..\..\node_modules\@capacitor-community\sqlite'
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
pod 'RobingenzCapacitorAppUpdate', :path => '..\..\node_modules\@robingenz\capacitor-app-update'
pod 'CapacitorDataStorageSqlite', :path => '..\..\node_modules\capacitor-data-storage-sqlite'
end
target 'App' do

View file

@ -269,7 +269,7 @@ export default {
var downloadFolder = await this.$localStore.getDownloadFolder()
if (downloadFolder) {
await this.syncDownloads(downloads, downloadFolder)
// await this.syncDownloads(downloads, downloadFolder)
}
this.$eventBus.$emit('downloads-loaded')

17824
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,6 @@
"@capacitor/storage": "^1.1.0",
"@nuxtjs/axios": "^5.13.6",
"@robingenz/capacitor-app-update": "^1.0.0",
"axios": "^0.21.1",
"capacitor-data-storage-sqlite": "^3.2.0",
"core-js": "^3.15.1",
"date-fns": "^2.25.0",

View file

@ -134,16 +134,13 @@ export default {
methods: {
async changeDownloadFolderClick() {
if (!this.hasStoragePermission) {
console.log('Requesting Storage Permission')
StorageManager.requestStoragePermission()
} else {
var folderObj = await StorageManager.selectFolder()
if (folderObj.error) {
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
}
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
console.log('Storage Permission check folder ' + permissionsGood)
if (!permissionsGood) {
this.$toast.error('Folder permissions failed')

View file

@ -115,20 +115,11 @@ export default {
series() {
return this.mediaMetadata.series || []
},
audiobooks() {
return this.media.audiobooks || []
},
defaultAudiobook() {
if (!this.audiobooks.length) return null
return this.audiobooks[0]
},
duration() {
if (!this.defaultAudiobook) return 0
return this.defaultAudiobook.duration
return this.media.duration
},
size() {
if (!this.defaultAudiobook) return 0
return this.defaultAudiobook.size
return this.media.size
},
userToken() {
return this.$store.getters['user/getToken']
@ -160,8 +151,8 @@ export default {
return this.$store.getters['isAudiobookPlaying'](this.libraryItemId)
},
numTracks() {
if (!this.defaultAudiobook) return 0
return this.defaultAudiobook.tracks.length || 0
if (!this.media.tracks) return 0
return this.media.tracks.length || 0
},
isMissing() {
return this.libraryItem.isMissing
@ -173,17 +164,17 @@ export default {
return this.downloadObj ? this.downloadObj.isDownloading : false
},
showPlay() {
return !this.isMissing && !this.isIncomplete && this.defaultAudiobook
return !this.isMissing && !this.isIncomplete && this.numTracks
},
showRead() {
return this.ebooks.length && this.ebookFormat !== '.pdf'
return this.ebookFile && this.ebookFormat !== '.pdf'
},
ebooks() {
return this.media.ebooks || []
ebookFile() {
return this.media.ebookFile
},
ebookFormat() {
if (!this.ebooks.length) return null
return this.ebooks[0].ebookFile.ebookFormat
if (!this.ebookFile) return null
return this.ebookFile.ebookFormat
},
isDownloadPreparing() {
return this.downloadObj ? this.downloadObj.isPreparing : false

View file

@ -1,5 +1,5 @@
import Server from '../Server'
export default function ({ store }, inject) {
inject('server', new Server(store))
export default function ({ store, $axios }, inject) {
inject('server', new Server(store, $axios))
}

View file

@ -1,5 +1,6 @@
export const state = () => ({
libraryItemPlaying: null,
mediaEntityPlaying: null
})
export const getters = {
@ -28,5 +29,10 @@ export const actions = {
}
export const mutations = {
setLibraryItemPlaying(state, libraryItem) {
state.libraryItemPlaying = libraryItem
},
setMediaEntityPlaying(state, mediaEntity) {
state.mediaEntityPlaying = mediaEntity
}
}

View file

@ -2,7 +2,6 @@ import Vue from 'vue'
import { Network } from '@capacitor/network'
export const state = () => ({
streamLibraryItem: null,
streamAudiobook: null,
playingDownload: null,
playOnLoad: false,
@ -81,9 +80,6 @@ export const mutations = {
setPlayOnLoad(state, val) {
state.playOnLoad = val
},
setLibraryItemStream(state, libraryItem) {
state.streamLibraryItem = libraryItem
},
setStreamAudiobook(state, audiobook) {
if (audiobook) {
state.playingDownload = null