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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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", "@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",

View file

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

View file

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

View file

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

View file

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

View file

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