mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-09 21:44:41 +02:00
New data model classes, ffmpeg-kit, jackson json deserializer, add permission
This commit is contained in:
parent
461733854a
commit
4fc70cd3dd
30 changed files with 9058 additions and 9642 deletions
10
Server.js
10
Server.js
|
@ -1,13 +1,13 @@
|
|||
import { io } from 'socket.io-client'
|
||||
import { 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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.net.Uri
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class AudioProbeStream(
|
||||
val index:Int,
|
||||
val codec_name:String,
|
||||
val codec_long_name:String,
|
||||
val channels:Int,
|
||||
val channel_layout:String,
|
||||
val duration:Double,
|
||||
val bit_rate:Double
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class AudioProbeChapterTags(
|
||||
val title:String
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class AudioProbeChapter(
|
||||
val id:Int,
|
||||
val start:Int,
|
||||
val end:Int,
|
||||
val tags:AudioProbeChapterTags
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class AudioProbeFormatTags(
|
||||
val artist:String?,
|
||||
val album:String?,
|
||||
val comment:String?,
|
||||
val date:String?,
|
||||
val genre:String?,
|
||||
val title:String?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class AudioProbeFormat(
|
||||
val filename:String,
|
||||
val format_name:String,
|
||||
val duration:Double,
|
||||
val size:Long,
|
||||
val bit_rate:Double,
|
||||
val tags:AudioProbeFormatTags
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class AudioProbeResult (
|
||||
val streams:MutableList<AudioProbeStream>,
|
||||
val chapters:MutableList<AudioProbeChapter>,
|
||||
val format:AudioProbeFormat) {
|
||||
|
||||
val duration get() = format.duration
|
||||
val size get() = format.size
|
||||
val title get() = format.tags.title ?: format.filename.split("/").last()
|
||||
val artist get() = format.tags.artist ?: ""
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.net.Uri
|
||||
import com.getcapacitor.JSObject
|
||||
|
||||
data class ServerConfig(
|
||||
var id:String,
|
||||
var index:Int,
|
||||
var name:String,
|
||||
var address:String,
|
||||
var username:String,
|
||||
var token:String
|
||||
)
|
||||
|
||||
data class DeviceData(
|
||||
var serverConfigs:MutableList<ServerConfig>,
|
||||
var lastServerConfigId:String?
|
||||
)
|
||||
|
||||
data class LocalMediaItem(
|
||||
val name: String,
|
||||
val simplePath: String,
|
||||
val audioTracks:MutableList<AudioTrack>
|
||||
)
|
|
@ -0,0 +1,99 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.net.Uri
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import com.audiobookshelf.app.R
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.google.android.exoplayer2.MediaMetadata
|
||||
|
||||
val PLAYMETHOD_DIRECTPLAY = 0
|
||||
val PLAYMETHOD_DIRECTSTREAM = 1
|
||||
val PLAYMETHOD_TRANSCODE = 2
|
||||
val PLAYMETHOD_LOCAL = 3
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class PlaybackSession(
|
||||
var id:String,
|
||||
var userId:String,
|
||||
var libraryItemId:String,
|
||||
var episodeId:String,
|
||||
var mediaEntityId:String,
|
||||
var mediaType:String,
|
||||
var mediaMetadata:MediaTypeMetadata,
|
||||
var coverPath:String?,
|
||||
var duration:Double,
|
||||
var playMethod:Int,
|
||||
var audioTracks:MutableList<AudioTrack>,
|
||||
var currentTime:Double,
|
||||
var libraryItem:LibraryItem,
|
||||
var serverUrl:String,
|
||||
var token:String
|
||||
) {
|
||||
|
||||
@JsonIgnore
|
||||
fun getIsHls():Boolean {
|
||||
return playMethod == PLAYMETHOD_TRANSCODE
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getTitle():String {
|
||||
if (mediaMetadata == null) return "Unset"
|
||||
var metadata = mediaMetadata as BookMetadata
|
||||
return metadata.title
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getAuthor():String {
|
||||
if (mediaMetadata == null) return "Unset"
|
||||
var metadata = mediaMetadata as BookMetadata
|
||||
return metadata.authors.joinToString(",") { it.name }
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getCoverUri(): Uri {
|
||||
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getContentUri(): Uri {
|
||||
var audioTrack = audioTracks[0]
|
||||
return Uri.parse("$serverUrl${audioTrack.contentUrl}?token=$token")
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMimeType():String {
|
||||
var audioTrack = audioTracks[0]
|
||||
return audioTrack.mimeType
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, this.getTitle())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, getTitle())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, this.getAuthor())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||
return metadataBuilder.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getExoMediaMetadata(): MediaMetadata {
|
||||
var authorName = this.getAuthor()
|
||||
var metadataBuilder = MediaMetadata.Builder()
|
||||
.setTitle(this.getTitle())
|
||||
.setDisplayTitle(this.getTitle())
|
||||
.setArtist(authorName)
|
||||
.setAlbumArtist(authorName)
|
||||
.setSubtitle(authorName)
|
||||
|
||||
var contentUri = this.getContentUri()
|
||||
metadataBuilder.setMediaUri(contentUri)
|
||||
|
||||
return metadataBuilder.build()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.audiobookshelf.app.device
|
||||
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.data.DbManager
|
||||
import com.audiobookshelf.app.data.DeviceData
|
||||
import com.audiobookshelf.app.data.ServerConfig
|
||||
|
||||
object DeviceManager {
|
||||
val tag = "DeviceManager"
|
||||
val dbManager:DbManager = DbManager()
|
||||
var deviceData:DeviceData = dbManager.loadDeviceData()
|
||||
var currentServerConfig: ServerConfig? = null
|
||||
|
||||
val serverAddress get() = currentServerConfig?.address ?: ""
|
||||
val token get() = currentServerConfig?.token ?: ""
|
||||
|
||||
init {
|
||||
Log.d(tag, "Device Manager Singleton invoked")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package com.audiobookshelf.app.device
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.file.*
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import com.arthenica.ffmpegkit.FFprobeSession
|
||||
import com.arthenica.ffmpegkit.Level
|
||||
import com.audiobookshelf.app.data.AudioProbeResult
|
||||
import com.audiobookshelf.app.data.AudioTrack
|
||||
import com.audiobookshelf.app.data.LocalMediaItem
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
|
||||
|
||||
class FolderScanner(var ctx: Context) {
|
||||
private val tag = "FolderScanner"
|
||||
|
||||
fun scanForAudiobooks(folderUrl: String):MutableList<LocalMediaItem> {
|
||||
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(folderUrl))
|
||||
|
||||
if (df == null) {
|
||||
Log.e(tag, "Folder Doc File Invalid $folderUrl")
|
||||
return mutableListOf()
|
||||
}
|
||||
|
||||
var mediaFolders = mutableListOf<LocalMediaItem>()
|
||||
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||
|
||||
foldersFound.forEach {
|
||||
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
|
||||
var folderName = it.name ?: ""
|
||||
var mediaFiles = mutableListOf<LocalMediaItem>()
|
||||
|
||||
var audioTracks = mutableListOf<AudioTrack>()
|
||||
var index = 1
|
||||
|
||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
filesInFolder.forEach { it2 ->
|
||||
var mimeType = it2?.mimeType ?: ""
|
||||
var filename = it2?.name ?: ""
|
||||
var isAudio = mimeType.startsWith("audio")
|
||||
Log.d(tag, "Found $mimeType file $filename in folder $folderName")
|
||||
|
||||
if (isAudio) {
|
||||
var absolutePath = it2.getAbsolutePath(ctx)
|
||||
Log.d(tag, "Audio File Path $absolutePath")
|
||||
|
||||
// TODO: Make asynchronous
|
||||
var session = FFprobeKit.execute("-i \"$absolutePath\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||
var sessionData = session.output
|
||||
Log.d(tag, "AFTER FFPROBE STRING $sessionData")
|
||||
|
||||
val mapper = jacksonObjectMapper()
|
||||
val audioProbeResult = mapper.readValue<AudioProbeResult>(sessionData)
|
||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||
|
||||
var track = AudioTrack(index, 0.0, 0.0, filename, absolutePath, mimeType, true)
|
||||
audioTracks.add(track)
|
||||
} else {
|
||||
Log.d(tag, "Found non audio file $filename")
|
||||
}
|
||||
// var imageFile = StorageManager.MediaFile(it2.uri, filename, it2.getSimplePath(context), it2.length(), mimeType, isAudio)
|
||||
// mediaFiles.add(imageFile)
|
||||
}
|
||||
if (mediaFiles.size > 0) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return mediaFolders
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import android.content.SharedPreferences
|
|||
import android.util.Log
|
||||
import 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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
ext {
|
||||
minSdkVersion = 23
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
androidxActivityVersion = '1.2.0'
|
||||
|
|
|
@ -31,13 +31,13 @@
|
|||
|
||||
<div class="cover-wrapper absolute z-30 pointer-events-auto" :class="bookCoverAspectRatio === 1 ? 'square-cover' : ''" @click="clickContainer">
|
||||
<div class="cover-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))
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
17818
package-lock.json
generated
17818
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -22,7 +22,6 @@
|
|||
"@capacitor/storage": "^1.1.0",
|
||||
"@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",
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue