diff --git a/Server.js b/Server.js index 14f6ad5e..c173a0d3 100644 --- a/Server.js +++ b/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) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1531b342..121c2674 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1e6ea18f..f9c84bb3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ - + () - 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() - - 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() - var mediaFilesFound:List = 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() +// 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() +// +// 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() +// var mediaFilesFound:List = 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) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt b/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt new file mode 100644 index 00000000..803c2c8c --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt @@ -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, + val chapters:MutableList, + 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 ?: "" +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index db9c643b..8fe06dba 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -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 ) @@ -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, var episodes:MutableList, var autoDownloadEpisodes:Boolean -) : MediaEntity() +) : MediaType() @JsonIgnoreProperties(ignoreUnknown = true) data class Book( var metadata:BookMetadata, var coverPath:String?, var tags:MutableList, - var audiobooks:MutableList -) : MediaEntity() + var audioFiles:MutableList +) : 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 -) : MediaEntityMetadata() + var authors:MutableList, + var narrators:MutableList, + var genres:MutableList, + 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 -) : 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 -) - @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, - 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 ) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt index 71a87c5a..dc86d58d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt @@ -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) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt new file mode 100644 index 00000000..8af0a744 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -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, + var lastServerConfigId:String? +) + +data class LocalMediaItem( + val name: String, + val simplePath: String, + val audioTracks:MutableList +) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt new file mode 100644 index 00000000..0390f3f7 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -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, + 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() + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt new file mode 100644 index 00000000..cb240568 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt @@ -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") + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt new file mode 100644 index 00000000..143643d1 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -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 { + 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() + 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() + + var audioTracks = mutableListOf() + 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(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 + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 94c2d8e2..6f602662 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -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 diff --git a/android/variables.gradle b/android/variables.gradle index 633d13f2..0a287a3c 100644 --- a/android/variables.gradle +++ b/android/variables.gradle @@ -1,5 +1,5 @@ ext { - minSdkVersion = 23 + minSdkVersion = 24 compileSdkVersion = 30 targetSdkVersion = 30 androidxActivityVersion = '1.2.0' diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 7a207f5d..7877ab4d 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -31,13 +31,13 @@
- +

{{ title }}

-

by {{ authorFL }}

+

by {{ authorName }}

@@ -58,19 +58,19 @@
- first_page - replay_10 + first_page + replay_10
- {{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }} + {{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}
- forward_10 - last_page + forward_10 + last_page
-
+
@@ -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)) diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index f4f0565c..6dac10c1 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -1,12 +1,12 @@