Compare commits

...
Author SHA1 Message Date
advplyr 92f069d1e6 iOS version bump 0.9.46 2022-05-22 17:51:35 -05:00
advplyr 62c5042ecc Fix:Cover image stretch to fill 2022-05-22 17:49:10 -05:00
advplyr 604b086c0b Android version bump 0.9.46-beta 2022-05-22 17:04:19 -05:00
advplyr 0e0b356f6b Fix:Reset volume when playing from notification after sleep timer decreases volume #182 2022-05-22 16:24:38 -05:00
advplyr 573768e2b2 Fix:Select podcast folder when none is set yet 2022-05-22 15:56:32 -05:00
advplyr 236fd09c94 Fix:Hide download button if user does not have download permission #189 2022-05-22 15:49:42 -05:00
advplyr cb6cb5f637 Fix:M4B downloads on Android 9 & 10 not detected as audio files (mimeType is showing as application/octet-stream) #154 2022-05-22 15:31:25 -05:00
advplyr eb916a3ab0 Add:Duration sort 2022-05-22 14:47:07 -05:00
advplyr 61b8ca1510 Add:Persist scroll position for bookshelves #196, Fix:Hide clear filter button for series and collections 2022-05-22 14:19:13 -05:00
advplyr 5d2da97dc5 Update:Android auto to show libraries and browseable podcasts, Fix:Download podcast set episodes and cover art correctly 2022-05-22 13:40:01 -05:00
advplyr d626686614 Add start of downloading library item on iOS 2022-05-14 11:50:53 -05:00
advplyr 60ee33cb72 Update:Show download button on iOS and call downloadLibraryItem without a folderId 2022-05-14 09:08:52 -05:00
advplyr 8d2498e96d Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2022-05-13 16:27:10 -05:00
advplyr 1794484bed Fix:Realtime updates for book cards when series sequence changes 2022-05-13 16:27:05 -05:00
advplyr f2b6331843 Add iOS AbsDownloader and AbsFileSystem plugins 2022-05-13 15:18:21 -05:00
advplyr 8d5f33245f Version bump 0.9.45 2022-05-13 13:53:37 -05:00
advplyr 1ee544b842 Fix:LazyBookshelf default to square cover before server settings are loaded 2022-05-13 13:42:38 -05:00
advplyr 0fd9463c7c Update:Set installLocation in android manifest to preferExternal for amazon app store 2022-05-13 11:04:52 -05:00
advplyr ad5edf3aee Fix:iOS audio player cutting off timestamps #173 2022-05-13 09:28:43 -05:00
advplyr d5fafd8cab Fix:Bookshelf list view wrong entity height #179 2022-05-12 18:24:36 -05:00
advplyr 92256f2fed Fix:Create bookmark was requiring a socket connection still 2022-05-11 18:59:24 -05:00
advplyr 5fa8d4c989 Fix:Check local library item is linked to a server item before sending playback session progress sync to server 2022-05-11 17:05:51 -05:00
advplyr 6b75f79f00 Fix wrong android version in comment 2022-05-08 15:01:36 -05:00
advplyr b0c9f29d90 Fix:Downloaded covers when used in media notification crashing android 11 #35 #164 2022-05-08 14:34:28 -05:00
advplyr 7721afc116 Remove unnecessary background-image 2022-05-07 20:19:49 -05:00
advplyr bc85e9bbfa Merge 2022-05-07 17:33:07 -05:00
advplyr e8b6602fe5 Remove app update package 2022-05-07 17:32:18 -05:00
57 changed files with 19545 additions and 372 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 73
versionName "0.9.44-beta"
versionCode 75
versionName "0.9.46-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
-1
View File
@@ -15,7 +15,6 @@ dependencies {
implementation project(':capacitor-network')
implementation project(':capacitor-status-bar')
implementation project(':capacitor-storage')
implementation project(':robingenz-capacitor-app-update')
}
+1
View File
@@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="preferExternal"
package="com.audiobookshelf.app">
<!-- Permissions -->
@@ -22,9 +22,5 @@
{
"pkg": "@capacitor/storage",
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
},
{
"pkg": "@robingenz/capacitor-app-update",
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
}
]
@@ -42,6 +42,15 @@ data class LibraryItem(
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun checkHasTracks():Boolean {
return if (mediaType == "podcast") {
((media as Podcast).numEpisodes ?: 0) > 0
} else {
((media as Book).numTracks ?: 0) > 0
}
}
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
@@ -74,6 +83,7 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
open fun removeAudioTrack(localFileId:String) { }
@JsonIgnore
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -82,7 +92,8 @@ class Podcast(
coverPath:String?,
var tags:MutableList<String>,
var episodes:MutableList<PodcastEpisode>?,
var autoDownloadEpisodes:Boolean
var autoDownloadEpisodes:Boolean,
var numEpisodes:Int?
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
@@ -99,7 +110,7 @@ class Podcast(
// Add new episodes
audioTracks.forEach { at ->
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
val newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
episodes?.add(newEpisode)
}
}
@@ -147,7 +158,7 @@ class Podcast(
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
@JsonIgnore
override fun getLocalCopy(): Podcast {
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes)
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
}
}
@@ -160,7 +171,8 @@ class Book(
var chapters:List<BookChapter>?,
var tracks:MutableList<AudioTrack>?,
var size:Long?,
var duration:Double?
var duration:Double?,
var numTracks:Int?
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
@@ -209,7 +221,7 @@ class Book(
@JsonIgnore
override fun getLocalCopy(): Book {
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null)
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
}
}
@@ -281,7 +293,29 @@ data class PodcastEpisode(
var duration:Double?,
var size:Long?,
var serverEpisodeId:String? // For local podcasts to match with server podcasts
)
) {
@JsonIgnore
fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat {
var coverUri:Uri = Uri.EMPTY
val podcast = if(libraryItem is LocalLibraryItem) {
coverUri = libraryItem.getCoverUri()
libraryItem.media as Podcast
} else {
coverUri = (libraryItem as LibraryItem).getCoverUri()
(libraryItem as LibraryItem).media as Podcast
}
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, podcast.metadata.getAuthorDisplayName())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
}.build()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryFile(
@@ -312,7 +346,16 @@ data class Library(
var folders:MutableList<Folder>,
var icon:String,
var mediaType:String
)
) {
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
}.build()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Folder(
@@ -371,3 +414,9 @@ data class MediaProgress(
var startedAt:Long,
var finishedAt:Long?
)
// Helper class
data class LibraryItemWithEpisode(
var libraryItemWrapper:LibraryItemWrapper,
var episode:PodcastEpisode
)
@@ -42,6 +42,20 @@ class DbManager {
return Paper.book("localLibraryItems").read(localLibraryItemId)
}
fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String):LibraryItemWithEpisode? {
var podcastEpisode:PodcastEpisode? = null
val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem ->
val podcast = localLibraryItem.media as Podcast
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
podcastEpisode != null
}
return if (localLibraryItem != null) {
LibraryItemWithEpisode(localLibraryItem, podcastEpisode!!)
} else {
null
}
}
fun removeLocalLibraryItem(localLibraryItemId:String) {
Paper.book("localLibraryItems").delete(localLibraryItemId)
}
@@ -42,6 +42,8 @@ data class LocalFile(
) {
@JsonIgnore
fun isAudioFile():Boolean {
if (mimeType == "application/octet-stream") return true
if (mimeType == "video/mp4") return true
return mimeType?.startsWith("audio") == true
}
}
@@ -1,12 +1,20 @@
package com.audiobookshelf.app.data
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.NOTIFICATION_LARGE_ICON_SIZE
import com.bumptech.glide.Glide
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -44,7 +52,7 @@ data class LocalLibraryItem(
@JsonIgnore
fun getDuration():Double {
var total = 0.0
var audioTracks = media.getAudioTracks()
val audioTracks = media.getAudioTracks()
audioTracks.forEach{ total += it.duration }
return total
}
@@ -94,15 +102,17 @@ data class LocalLibraryItem(
}
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
fun getMediaMetadata(ctx: Context): MediaMetadataCompat {
val coverUri = getCoverUri()
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build()
}
@@ -58,13 +58,13 @@ data class LocalMediaItem(
@JsonIgnore
fun getLocalLibraryItem():LocalLibraryItem {
var mediaMetadata = getMediaMetadata()
val mediaMetadata = getMediaMetadata()
if (mediaType == "book") {
var chapters = getAudiobookChapters()
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
val chapters = getAudiobookChapters()
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size)
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
} else {
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
}
@@ -2,13 +2,11 @@ package com.audiobookshelf.app.data
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import androidx.core.app.NotificationCompat
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.MediaProgressSyncData
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MediaMetadata
import com.google.android.gms.cast.MediaInfo
@@ -68,7 +66,7 @@ class PlaybackSession(
@JsonIgnore
fun getCurrentTrackIndex():Int {
for (i in 0..(audioTracks.size - 1)) {
var track = audioTracks[i]
val track = audioTracks[i]
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs) > currentTimeMs) {
return i
}
@@ -78,14 +76,14 @@ class PlaybackSession(
@JsonIgnore
fun getCurrentTrackTimeMs():Long {
var currentTrack = audioTracks[this.getCurrentTrackIndex()]
var time = currentTime - currentTrack.startOffset
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
val time = currentTime - currentTrack.startOffset
return (time * 1000L).toLong()
}
@JsonIgnore
fun getTrackStartOffsetMs(index:Int):Long {
var currentTrack = audioTracks[index]
val currentTrack = audioTracks[index]
return (currentTrack.startOffset * 1000L).toLong()
}
@@ -112,7 +110,7 @@ class PlaybackSession(
@JsonIgnore
fun getMediaMetadataCompat(): MediaMetadataCompat {
var metadataBuilder = MediaMetadataCompat.Builder()
val metadataBuilder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
@@ -125,14 +123,14 @@ class PlaybackSession(
@JsonIgnore
fun getExoMediaMetadata(audioTrack:AudioTrack): MediaMetadata {
var metadataBuilder = MediaMetadata.Builder()
val metadataBuilder = MediaMetadata.Builder()
.setTitle(displayTitle)
.setDisplayTitle(displayTitle)
.setArtist(displayAuthor)
.setAlbumArtist(displayAuthor)
.setSubtitle(displayAuthor)
var contentUri = this.getContentUri(audioTrack)
val contentUri = this.getContentUri(audioTrack)
metadataBuilder.setMediaUri(contentUri)
return metadataBuilder.build()
@@ -140,15 +138,15 @@ class PlaybackSession(
@JsonIgnore
fun getMediaItems():List<MediaItem> {
var mediaItems:MutableList<MediaItem> = mutableListOf()
val mediaItems:MutableList<MediaItem> = mutableListOf()
for (audioTrack in audioTracks) {
var mediaMetadata = this.getExoMediaMetadata(audioTrack)
var mediaUri = this.getContentUri(audioTrack)
var mimeType = audioTrack.mimeType
val mediaMetadata = this.getExoMediaMetadata(audioTrack)
val mediaUri = this.getContentUri(audioTrack)
val mimeType = audioTrack.mimeType
var queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
var mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
val queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
val mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
mediaItems.add(mediaItem)
}
return mediaItems
@@ -156,7 +154,7 @@ class PlaybackSession(
@JsonIgnore
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
val castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
coverPath?.let {
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
@@ -171,11 +169,11 @@ class PlaybackSession(
@JsonIgnore
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem {
var castMetadata = getCastMediaMetadata(audioTrack)
val castMetadata = getCastMediaMetadata(audioTrack)
var mediaUri = getContentUri(audioTrack)
val mediaUri = getContentUri(audioTrack)
var mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
val mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
setContentUrl(mediaUri.toString())
setContentType(audioTrack.mimeType)
setMetadata(castMetadata)
@@ -248,9 +248,10 @@ class FolderScanner(var ctx: Context) {
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
// Search for files in media item folder
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
var localEpisodeId:String? = null
@@ -288,7 +289,7 @@ class FolderScanner(var ctx: Context) {
val audioProbeResult = probeAudioFile(localFile.absolutePath)
// Create new audio track
val track = AudioTrack(audioTrackFromServer?.index ?: -1, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
audioTracks.add(track)
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
@@ -296,7 +297,7 @@ class FolderScanner(var ctx: Context) {
// Add podcast episodes to library
itemPart.episode?.let { podcastEpisode ->
val podcast = localLibraryItem.media as Podcast
var newEpisode = podcast.addEpisode(track, podcastEpisode)
val newEpisode = podcast.addEpisode(track, podcastEpisode)
localEpisodeId = newEpisode.id
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
}
@@ -366,7 +367,7 @@ class FolderScanner(var ctx: Context) {
}
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
if (df == null) {
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
@@ -377,7 +378,7 @@ class FolderScanner(var ctx: Context) {
var wasUpdated = false
// Search for files in media item folder
var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
filesFound.forEach {
@@ -388,10 +389,10 @@ class FolderScanner(var ctx: Context) {
}
}
var existingAudioTracks = localLibraryItem.media.getAudioTracks()
val existingAudioTracks = localLibraryItem.media.getAudioTracks()
// Remove any files no longer found in library item folder
var existingLocalFileIds = localLibraryItem.localFiles.map { it.id }
val existingLocalFileIds = localLibraryItem.localFiles.map { it.id }
existingLocalFileIds.forEach { localFileId ->
Log.d(tag, "Checking local file id is there $localFileId")
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
@@ -407,12 +408,12 @@ class FolderScanner(var ctx: Context) {
}
filesFound.forEach { docFile ->
var localFileId = DeviceManager.getBase64Id(docFile.id)
var existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
val localFileId = DeviceManager.getBase64Id(docFile.id)
val existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
var localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
val localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
if (existingLocalFile == null) {
localLibraryItem.localFiles.add(localFile)
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
@@ -420,22 +421,26 @@ class FolderScanner(var ctx: Context) {
if (localFile.isAudioFile()) {
// TODO: Make asynchronous
var audioProbeResult = probeAudioFile(localFile.absolutePath)
val audioProbeResult = probeAudioFile(localFile.absolutePath)
var existingTrack = existingAudioTracks.find { audioTrack ->
val existingTrack = existingAudioTracks.find { audioTrack ->
audioTrack.localFileId == localFile.id
}
if (existingTrack == null) {
// Create new audio track
var lastTrack = existingAudioTracks.lastOrNull()
var startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
var track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
val lastTrack = existingAudioTracks.lastOrNull()
val startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
val track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
localLibraryItem.media.addAudioTrack(track)
Log.d(tag, "Added New Audio Track ${track.title}")
wasUpdated = true
} else {
existingTrack.audioProbeResult = audioProbeResult
// TODO: Update data found from probe
Log.d(tag, "Updated Audio Track Probe Data ${existingTrack.title}")
wasUpdated = true
}
} else { // Check if cover is empty
@@ -2,6 +2,8 @@ package com.audiobookshelf.app.media
import android.bluetooth.BluetoothClass
import android.content.Context
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
@@ -14,6 +16,12 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager"
var serverLibraryItems = listOf<LibraryItem>()
var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
var selectedPodcast:Podcast? = null
var selectedLibraryItemId:String? = null
var serverPodcastEpisodes = listOf<PodcastEpisode>()
var serverLibraryCategories = listOf<LibraryCategory>()
var serverLibraries = listOf<Library>()
@@ -22,6 +30,10 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
Paper.init(ctx)
}
fun getIsLibrary(id:String) : Boolean {
return serverLibraries.find { it.id == id } != null
}
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
if (serverLibraryCategories.isNotEmpty()) {
cb(serverLibraryCategories)
@@ -33,17 +45,75 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
}
}
fun loadLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
if (serverLibraryItems.isNotEmpty()) {
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
cb(serverLibraryItems)
} else {
apiHandler.getLibraryItems(libraryId) { libraryItems ->
serverLibraryItems = libraryItems
cb(libraryItems)
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
serverLibraryItems = libraryItemsWithAudio
cb(libraryItemsWithAudio)
}
}
}
fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
if (libraryItemId.startsWith("local")) {
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
} else {
Log.d(tag, "loadLibraryItem: $libraryItemId")
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
Log.d(tag, "loadLibraryItem: Got library item $libraryItem")
cb(libraryItem)
}
}
}
fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, cb: (MutableList<MediaBrowserCompat.MediaItem>) -> Unit) {
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
selectedLibraryItemWrapper = libraryItemWrapper
libraryItemWrapper?.let {
if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
serverPodcastEpisodes = listOf()
cb(mutableListOf())
} else {
val podcast = libraryItemWrapper.media as Podcast
serverPodcastEpisodes = podcast.episodes ?: listOf()
selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode ->
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
}
} else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
serverPodcastEpisodes = listOf()
cb(mutableListOf())
} else {
val podcast = libraryItemWrapper.media as Podcast
serverPodcastEpisodes = podcast.episodes ?: listOf()
selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode ->
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
}
}
}
}
}
fun loadLibraries(cb: (List<Library>) -> Unit) {
if (serverLibraries.isNotEmpty()) {
cb(serverLibraries)
@@ -57,9 +127,9 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
// TODO: Load currently listening category for local items
fun loadLocalCategory():List<LibraryCategory> {
var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
var localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
var cats = mutableListOf<LibraryCategory>()
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val cats = mutableListOf<LibraryCategory>()
if (localBooks.isNotEmpty()) {
cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true))
}
@@ -69,11 +139,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
return cats
}
fun loadAndroidAutoItems(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
Log.d(tag, "Load android auto items for library id $libraryId")
var cats = mutableListOf<LibraryCategory>()
fun loadAndroidAutoItems(cb: (List<LibraryCategory>) -> Unit) {
Log.d(tag, "Load android auto items")
val cats = mutableListOf<LibraryCategory>()
var localCategories = loadLocalCategory()
val localCategories = loadLocalCategory()
cats.addAll(localCategories)
// Connected to server and has internet - load other cats
@@ -84,26 +154,21 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
}
loadLibraries { libraries ->
var library = libraries.find { it.id == libraryId } ?: libraries[0]
val library = libraries[0]
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
loadLibraryCategories(libraryId) { libraryCategories ->
loadLibraryCategories(library.id) { libraryCategories ->
// Only using book or podcast library categories for now
libraryCategories.forEach {
Log.d(tag, "Found library category ${it.label} with type ${it.type}")
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
if (it.type == library.mediaType) {
Log.d(tag, "Using library category ${it.id}")
// Log.d(tag, "Using library category ${it.id}")
cats.add(it)
}
}
loadLibraryItems(libraryId) { libraryItems ->
var mainCat = LibraryCategory("library", "Library", library.mediaType, libraryItems, false)
cats.add(mainCat)
cb(cats)
}
cb(cats)
}
}
} else { // Not connected/no internet sent downloaded cats only
@@ -115,11 +180,24 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
if (serverLibraryItems.isNotEmpty()) {
return serverLibraryItems[0]
} else {
var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
return if (localBooks.isNotEmpty()) return localBooks[0] else null
}
}
fun getPodcastWithEpisodeByEpisodeId(id:String) : LibraryItemWithEpisode? {
if (id.startsWith("local")) {
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
} else {
val podcastEpisode = serverPodcastEpisodes.find { it.id == id }
return if (podcastEpisode != null && selectedLibraryItemWrapper != null) {
LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode)
} else {
null
}
}
}
fun getById(id:String) : LibraryItemWrapper? {
if (id.startsWith("local")) {
return DeviceManager.dbManager.getLocalLibraryItem(id)
@@ -135,13 +213,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
}
}
fun play(libraryItemWrapper:LibraryItemWrapper, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
if (libraryItemWrapper is LocalLibraryItem) {
var localLibraryItem = libraryItemWrapper as LocalLibraryItem
cb(localLibraryItem.getPlaybackSession(null))
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
cb(localLibraryItem.getPlaybackSession(episode))
} else {
var libraryItem = libraryItemWrapper as LibraryItem
apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) {
val libraryItem = libraryItemWrapper as LibraryItem
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",false, mediaPlayer) {
cb(it)
}
}
@@ -5,9 +5,6 @@ import android.graphics.Bitmap
import android.net.Uri
import android.support.v4.media.session.MediaControllerCompat
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.toRawFile
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
@@ -62,26 +59,10 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
// Block on downloading artwork.
val context = playerNotificationService.getContext()
// Fix attempt for #35 local cover crashing
// Convert content uri to a file and pass to Glide
var urival:Any = uri
if (uri.toString().startsWith("content:")) {
val imageDocFile = DocumentFile.fromSingleUri(context, uri)
Log.d(tag, "Converting local content url $uri to file with path ${imageDocFile?.getAbsolutePath(context)}")
val file = imageDocFile?.toRawFile(context)
file?.let {
Log.d(tag, "Using local file image instead of content uri ${it.absolutePath}")
urival = it
}
}
try {
Glide.with(context).applyDefaultRequestOptions(glideOptions)
Glide.with(playerNotificationService)
.asBitmap()
.load(urival)
.load(uri)
.placeholder(R.drawable.icon)
.error(R.drawable.icon)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
@@ -89,7 +70,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
} catch (e: Exception) {
e.printStackTrace()
Glide.with(context).applyDefaultRequestOptions(glideOptions)
Glide.with(playerNotificationService)
.asBitmap()
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
@@ -7,14 +7,15 @@ import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.annotation.AnyRes
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.Library
import com.audiobookshelf.app.data.LibraryCategory
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LocalLibraryItem
class BrowseTree(
val context: Context,
libraryCategories: List<LibraryCategory>
libraryCategories: List<LibraryCategory>,
libraries: List<Library>
) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
@@ -41,22 +42,22 @@ class BrowseTree(
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
}.build()
val allMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Library Items")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_books).toString())
}.build()
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
}.build()
val librariesMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
}.build()
// Server continue Listening cat
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
var continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
var libraryItem = liw as LibraryItem
val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
val libraryItem = liw as LibraryItem
libraryItem.getMediaMetadata()
}
if (continueListeningMediaMetadata.isNotEmpty()) {
@@ -69,30 +70,32 @@ class BrowseTree(
}
}
rootList += allMetadata
rootList += downloadsMetadata
if (libraries.isNotEmpty()) {
rootList += librariesMetadata
// Server library cat
libraryCategories.find { it.id == "library" }?.let { libraryCategory ->
var libraryMediaMetadata = libraryCategory.entities.map { libc ->
var libraryItem = libc as LibraryItem
libraryItem.getMediaMetadata()
}
libraryMediaMetadata.forEach {
val children = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
children += it
mediaIdToChildren[ALL_ROOT] = children
libraries.forEach { library ->
val libraryMediaMetadata = library.getMediaMetadata()
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
children += libraryMediaMetadata
mediaIdToChildren[LIBRARIES_ROOT] = children
}
}
rootList += downloadsMetadata
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
var localMediaMetadata = localBooksCat.entities.map { libc ->
var libraryItem = libc as LocalLibraryItem
libraryItem.getMediaMetadata()
localBooksCat.entities.forEach { libc ->
val libraryItem = libc as LocalLibraryItem
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
children += libraryItem.getMediaMetadata(context)
mediaIdToChildren[DOWNLOADS_ROOT] = children
}
localMediaMetadata.forEach {
}
libraryCategories.find { it.id == "local-podcasts" }?.let { localPodcastsCat ->
localPodcastsCat.entities.forEach { libc ->
val libraryItem = libc as LocalLibraryItem
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
children += it
children += libraryItem.getMediaMetadata(context)
mediaIdToChildren[DOWNLOADS_ROOT] = children
}
}
@@ -104,6 +107,6 @@ class BrowseTree(
}
const val AUTO_BROWSE_ROOT = "/"
const val ALL_ROOT = "__ALL__"
const val CONTINUE_ROOT = "__CONTINUE__"
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
const val LIBRARIES_ROOT = "__LIBRARIES__"
@@ -17,10 +17,8 @@ data class MediaProgressSyncData(
var currentTime:Double // seconds
)
class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, apiHandler: ApiHandler) {
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
private val tag = "MediaProgressSync"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private val apiHandler = apiHandler
private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false
@@ -54,7 +52,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) {
var currentTime = playerNotificationService.getCurrentTimeSeconds()
val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime)
}
}
@@ -65,20 +63,20 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
if (!listeningTimerRunning) return
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
var currentTime = playerNotificationService.getCurrentTimeSeconds()
val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime)
reset()
}
fun sync(currentTime:Double) {
var diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) {
return
}
var listeningTimeToAdd = diffSinceLastSync / 1000L
val listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis()
var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
currentPlaybackSession?.syncData(syncData)
if (currentIsLocal) {
@@ -87,10 +85,16 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
// Send sync to server also if connected to this server and local item belongs to this server
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
apiHandler.sendLocalProgressSync(it) {
Log.d(tag, "Local progress sync data sent to server $currentDisplayTitle for time $currentTime")
// Local library item is linked to a server library item
if (!it.libraryItemId.isNullOrEmpty()) {
// Send sync to server also if connected to this server and local item belongs to this server
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
apiHandler.sendLocalProgressSync(it) {
Log.d(
tag,
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
)
}
}
}
}
@@ -103,7 +107,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
private fun saveLocalProgress(playbackSession:PlaybackSession) {
if (currentLocalMediaProgress == null) {
var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
if (mediaProgress == null) {
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
} else {
@@ -11,6 +11,7 @@ import android.util.Log
import android.view.KeyEvent
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -27,7 +28,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun onPrepare() {
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null)
@@ -49,7 +50,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null)
@@ -90,14 +91,20 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
var libraryItemWrapper: LibraryItemWrapper? = null
var podcastEpisode: PodcastEpisode? = null
if (mediaId.isNullOrEmpty()) {
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
} else if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
podcastEpisode = libraryItemWithEpisode?.episode
} else {
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
}
libraryItemWrapper?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null)
@@ -7,8 +7,8 @@ import android.os.Looper
import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
@@ -30,7 +30,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepare(playWhenReady: Boolean) {
Log.d(tag, "ON PREPARE $playWhenReady")
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady,null)
}
@@ -41,9 +41,19 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
var libraryItemWrapper: LibraryItemWrapper? = playerNotificationService.mediaManager.getById(mediaId)
var libraryItemWrapper: LibraryItemWrapper? = null
var podcastEpisode: PodcastEpisode? = null
if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
podcastEpisode = libraryItemWithEpisode?.episode
} else {
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
}
libraryItemWrapper?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady,null)
@@ -55,7 +65,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady,null)
@@ -15,7 +15,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
private var onSeekBack: Boolean = false
override fun onPlayerError(error: PlaybackException) {
var errorMessage = error.message ?: "Unknown Error"
val errorMessage = error.message ?: "Unknown Error"
Log.e(tag, "onPlayerError $errorMessage")
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
}
@@ -90,6 +90,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
// Start/stop progress sync interval
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
if (player.isPlaying) {
player.volume = 1F // Volume on sleep timer might have decreased this
playerNotificationService.mediaProgressSyncer.start()
} else {
playerNotificationService.mediaProgressSyncer.stop()
@@ -66,7 +66,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private lateinit var transportControls:MediaControllerCompat.TransportControls
lateinit var mediaManager: MediaManager
lateinit var apiHandler: ApiHandler
private lateinit var apiHandler: ApiHandler
lateinit var mPlayer: ExoPlayer
lateinit var currentPlayer:Player
@@ -75,7 +75,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
lateinit var sleepTimerManager:SleepTimerManager
lateinit var mediaProgressSyncer:MediaProgressSyncer
private var notificationId = 10;
private var notificationId = 10
private var channelId = "audiobookshelf_channel"
private var channelName = "Audiobookshelf Channel"
@@ -100,7 +100,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// Android Auto Media Browser Service
if (SERVICE_INTERFACE == intent.action) {
Log.d(tag, "Is Media Browser Service")
return super.onBind(intent);
return super.onBind(intent)
}
return binder
}
@@ -245,11 +245,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector = MediaSessionConnector(mediaSession)
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
val coverUri = currentPlaybackSession!!.getCoverUri()
// Fix for local images crashing on Android 11 for specific devices
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
ctx.grantUriPermission(
"com.android.systemui",
coverUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
return MediaDescriptionCompat.Builder()
.setMediaId(currentPlaybackSession!!.id)
.setTitle(currentPlaybackSession!!.displayTitle)
.setSubtitle(currentPlaybackSession!!.displayAuthor)
.setIconUri(currentPlaybackSession!!.getCoverUri()).build()
.setIconUri(coverUri).build()
}
}
@@ -346,7 +356,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
currentPlayer.playWhenReady = playWhenReady
currentPlayer.setPlaybackSpeed(playbackRateToUse)
currentPlayer.prepare()
} else if (castPlayer != null) {
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
@@ -367,7 +379,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
val episodeId = playbackSession.episodeId
apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
Handler(Looper.getMainLooper()).post() {
Handler(Looper.getMainLooper()).post {
preparePlayer(it, true, null)
}
}
@@ -510,12 +522,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun seekForward(amount: Long) {
seekPlayer(getCurrentTime() + amount)
// currentPlayer.seekTo(currentPlayer.currentPosition + amount)
}
fun seekBackward(amount: Long) {
seekPlayer(getCurrentTime() - amount)
// currentPlayer.seekTo(currentPlayer.currentPosition - amount)
}
fun setPlaybackSpeed(speed: Float) {
@@ -555,6 +565,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private val AUTO_MEDIA_ROOT = "/"
private val ALL_ROOT = "__ALL__"
private val LIBRARIES_ROOT = "__LIBRARIES__"
private lateinit var browseTree:BrowseTree
@@ -600,32 +611,66 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
val flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
var flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
result.detach()
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
browseTree = BrowseTree(this, libraryCategories)
val children = browseTree[parentMediaId]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, flag)
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
Log.d(tag, "Loading podcast episodes")
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId) {
result.sendResult(it)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
// TODO: For using sub menus. Check if this is the root menu:
// if (AUTO_MEDIA_ROOT == parentMediaId) {
// build the MediaItem objects for the top level,
// and put them in the mediaItems list
// } else {
// examine the passed parentMediaId to see which submenu we're at,
// and put the children of that menu in the mediaItems list
// }
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
val children = libraryItems.map { libraryItem ->
val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
}
MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (parentMediaId == "__DOWNLOADS__") { // Load downloads
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
localBooks.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
localPodcasts.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(localBrowseItems)
} else { // Load categories
mediaManager.loadAndroidAutoItems() { libraryCategories ->
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag")
MediaBrowserCompat.MediaItem(item.description, flag)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
}
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
result.detach()
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
browseTree = BrowseTree(this, libraryCategories)
mediaManager.loadAndroidAutoItems() { libraryCategories ->
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
val children = browseTree[ALL_ROOT]?.map { item ->
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
@@ -277,7 +277,7 @@ class AbsDownloader : Plugin() {
finalDestinationFile.delete()
}
var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
downloadItem.downloadItemParts.add(downloadItemPart)
var dlRequest = downloadItemPart.getDownloadRequest()
@@ -294,7 +294,7 @@ class AbsDownloader : Plugin() {
if (finalDestinationFile.exists()) {
Log.d(tag, "Podcast cover already exists - not downloading cover again")
} else {
downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
dlRequest = downloadItemPart.getDownloadRequest()
@@ -10,9 +10,6 @@
android:translateY="-1.5294118">
<path
android:fillColor="@android:color/white"
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</group>
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

-3
View File
@@ -19,6 +19,3 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
include ':capacitor-storage'
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
include ':robingenz-capacitor-app-update'
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
+4 -1
View File
@@ -16,6 +16,7 @@ body {
min-height: calc(100% - 64px);
max-height: calc(100% - 64px);
}
#content.playerOpen {
height: calc(100% - 164px);
min-height: calc(100% - 164px);
@@ -46,6 +47,7 @@ body {
.box-shadow-book {
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
}
.shadow-height {
height: calc(100% - 4px);
}
@@ -53,6 +55,7 @@ body {
.bookshelfRow {
background-image: url(/wood_panels.jpg);
}
.bookshelfDivider {
background: rgb(149, 119, 90);
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
@@ -63,9 +66,9 @@ body {
Bookshelf Label
*/
.categoryPlacard {
background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg);
letter-spacing: 1px;
}
.shinyBlack {
background-color: #2d3436;
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
+2 -5
View File
@@ -7,7 +7,7 @@
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
<span class="material-icons text-3xl text-white">arrow_back</span>
</a>
<div v-if="user">
<div v-if="user && currentLibrary">
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
@@ -51,14 +51,11 @@ export default {
this.$store.commit('setCastAvailable', val)
}
},
socketConnected() {
return this.$store.state.socketConnected
},
currentLibrary() {
return this.$store.getters['libraries/getCurrentLibrary']
},
currentLibraryName() {
return this.currentLibrary ? this.currentLibrary.name : 'Main'
return this.currentLibrary ? this.currentLibrary.name : ''
},
currentLibraryIcon() {
return this.currentLibrary ? this.currentLibrary.icon : 'database'
+13 -6
View File
@@ -1,5 +1,5 @@
<template>
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
<div v-if="playbackSession" id="streamContainer" class="playerContainer fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
<div class="top-2 left-4 absolute cursor-pointer">
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
@@ -41,7 +41,7 @@
<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">
<div id="playerContent" class="playerContainer w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
<div class="flex items-center justify-between pointer-events-auto">
<span v-if="!isPodcast && !isLocalPlayMethod" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
@@ -79,7 +79,7 @@
<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>
<div class="flex pt-0.5">
<div id="timestamp-row" class="flex pt-0.5">
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
<div class="flex-grow" />
<p v-show="showFullscreen" class="text-sm truncate text-white text-opacity-75" style="max-width: 65%">{{ currentChapterTitle }}</p>
@@ -659,13 +659,15 @@ export default {
.bookCoverWrapper {
box-shadow: 3px -2px 5px #00000066;
}
#streamContainer {
box-shadow: 0px -8px 8px #11111155;
.playerContainer {
height: 100px;
}
.fullscreen #streamContainer {
.fullscreen .playerContainer {
height: 200px;
}
#playerContent {
box-shadow: 0px -8px 8px #11111155;
}
#playerTrack {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
@@ -676,6 +678,11 @@ export default {
margin-bottom: 18px;
}
.ios-player #timestamp-row {
padding-left: 16px;
padding-right: 16px;
}
.cover-wrapper {
bottom: 44px;
left: 12px;
-3
View File
@@ -74,9 +74,6 @@ export default {
username() {
return this.user ? this.user.username : ''
},
socketConnected() {
return this.$store.state.socketConnected
},
navItems() {
var items = [
{
+28 -9
View File
@@ -3,7 +3,7 @@
<template v-for="shelf in totalShelves">
<div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
<div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" />
<div v-else class="flex border-t border-white border-opacity-10 my-3 py-1"/>
<div v-else class="flex border-t border-white border-opacity-10" />
</div>
</template>
@@ -25,11 +25,12 @@ export default {
mixins: [bookshelfCardsHelpers],
data() {
return {
routeFullPath: null,
entitiesPerShelf: 2,
bookshelfHeight: 0,
bookshelfWidth: 0,
bookshelfMarginLeft: 0,
shelvesPerPage: 0,
entitiesPerShelf: 2,
currentPage: 0,
booksPerFetch: 20,
initialized: false,
@@ -72,6 +73,7 @@ export default {
return this.page
},
hasFilter() {
if (this.page === 'series' || this.page === 'collections') return false
return this.filterBy !== 'all'
},
orderBy() {
@@ -83,14 +85,11 @@ export default {
filterBy() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
},
coverAspectRatio() {
return this.$store.getters['getServerSetting']('coverAspectRatio')
},
isCoverSquareAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
return this.bookCoverAspectRatio === 1
},
bookCoverAspectRatio() {
return this.isCoverSquareAspectRatio ? 1 : 1.6
return this.$store.getters['getBookCoverAspectRatio']
},
bookWidth() {
var coverSize = 100
@@ -119,7 +118,7 @@ export default {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
shelfHeight() {
if (this.showBookshelfListView) return this.entityHeight
if (this.showBookshelfListView) return this.entityHeight + 16
return this.entityHeight + 40
},
totalEntityCardWidth() {
@@ -300,7 +299,6 @@ export default {
this.bookshelfHeight = clientHeight
this.bookshelfWidth = clientWidth
this.entitiesPerShelf = this.showBookshelfListView ? 1 : Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth)
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
@@ -320,6 +318,15 @@ export default {
await this.loadPage(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.mountEntites(0, lastBookIndex)
// Set last scroll position for this bookshelf page
if (this.$store.state.lastBookshelfScrollData[this.page] && window['bookshelf-wrapper']) {
const { path, scrollTop } = this.$store.state.lastBookshelfScrollData[this.page]
if (path === this.routeFullPath) {
// Exact path match with query so use scroll position
window['bookshelf-wrapper'].scrollTop = scrollTop
}
}
},
scroll(e) {
if (!e || !e.target) return
@@ -362,6 +369,8 @@ export default {
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
window.history.replaceState({ path: newurl }, '', newurl)
this.routeFullPath = window.location.pathname + (window.location.search || '') // Update for saving scroll position
return true
}
@@ -456,12 +465,22 @@ export default {
this.$socket.$off('items_added', this.libraryItemsAdded)
}
},
updated() {
this.routeFullPath = window.location.pathname + (window.location.search || '')
},
mounted() {
this.routeFullPath = window.location.pathname + (window.location.search || '')
this.init()
this.initListeners()
},
beforeDestroy() {
this.removeListeners()
// Set bookshelf scroll position for specific bookshelf page and query
if (window['bookshelf-wrapper']) {
this.$store.commit('setLastBookshelfScrollData', { scrollTop: window['bookshelf-wrapper'].scrollTop || 0, path: this.routeFullPath, name: this.page })
}
}
}
</script>
+28 -1
View File
@@ -353,7 +353,34 @@ export default {
this.isSelectionMode = val
if (!val) this.selected = false
},
setEntity(libraryItem) {
setEntity(_libraryItem) {
var libraryItem = _libraryItem
// this code block is only necessary when showing a selected series with sequence #
// it will update the selected series so we get realtime updates for series sequence changes
if (this.series) {
// i know.. but the libraryItem passed to this func cannot be modified so we need to create a copy
libraryItem = {
..._libraryItem,
media: {
..._libraryItem.media,
metadata: {
..._libraryItem.media.metadata
}
}
}
var mediaMetadata = libraryItem.media.metadata
if (mediaMetadata.series) {
var newSeries = mediaMetadata.series.find((se) => se.id === this.series.id)
if (newSeries) {
// update selected series
libraryItem.media.metadata.series = newSeries
this.libraryItem = libraryItem
return
}
}
}
this.libraryItem = libraryItem
},
setLocalLibraryItem(localLibraryItem) {
+4 -4
View File
@@ -54,16 +54,16 @@ export default {
updatedAt() {
return this._author.updatedAt
},
serverAddres() {
serverAddress() {
return this.$store.getters['user/getServerAddress']
},
imgSrc() {
if (!this.imagePath || !this.serverAddres) return null
if (process.env.NODE_ENV !== 'production') {
if (!this.imagePath || !this.serverAddress) return null
if (process.env.NODE_ENV !== 'production' && this.serverAddress.startsWith('http://192.168')) {
// Testing
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
}
return `${this.serverAddres}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
return `${this.serverAddress}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
}
},
methods: {
+2 -2
View File
@@ -5,7 +5,7 @@
<div class="absolute cover-bg" ref="coverBg" />
</div>
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && !hasCover ? 'object-contain' : 'object-fill'" />
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && hasCover ? 'object-contain' : 'object-fill'" />
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
@@ -121,7 +121,7 @@ export default {
return this.media.coverPath || this.placeholderUrl
},
hasCover() {
return !!this.media.coverPath || this.localCover || this.downloadCover
return (!!this.media.coverPath && !this.isLocal) || this.localCover || this.downloadCover
},
sizeMultiplier() {
var baseSize = this.squareAspectRatio ? 128 : 96
-4
View File
@@ -83,11 +83,7 @@ export default {
this.$emit('input', val)
}
},
isConnected() {
return this.$store.state.socketConnected
},
canCreateBookmark() {
if (!this.isConnected) return false
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
}
},
+4
View File
@@ -47,6 +47,10 @@ export default {
text: 'Size',
value: 'size'
},
{
text: 'Duration',
value: 'media.duration'
},
{
text: 'File Birthtime',
value: 'birthtimeMs'
+11 -4
View File
@@ -27,7 +27,7 @@
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
<div v-if="!isIos">
<div v-if="!isIos && userCanDownload">
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else-if="!localEpisode" class="material-icons mx-1 mt-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
@@ -41,7 +41,7 @@
<script>
import { Dialog } from '@capacitor/dialog'
import { AbsDownloader } from '@/plugins/capacitor'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
export default {
props: {
@@ -69,6 +69,9 @@ export default {
mediaType() {
return 'podcast'
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
audioFile() {
return this.episode.audioFile
},
@@ -132,8 +135,12 @@ export default {
}
},
methods: {
selectFolder() {
this.$toast.error('Folder selector not implemented for podcasts yet')
async selectFolder() {
var folderObj = await AbsFileSystem.selectFolder({ mediaType: this.mediaType })
if (folderObj.error) {
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
}
return folderObj
},
downloadClick() {
if (this.downloadItem) return
+21 -5
View File
@@ -23,6 +23,10 @@
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B951282EE822008272D4 /* AbsDownloader.m */; };
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B953282EE87C008272D4 /* AbsDownloader.swift */; };
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B955282EE951008272D4 /* AbsFileSystem.m */; };
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
@@ -50,6 +54,10 @@
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
4D66B951282EE822008272D4 /* AbsDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDownloader.m; sourceTree = "<group>"; };
4D66B953282EE87C008272D4 /* AbsDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDownloader.swift; sourceTree = "<group>"; };
4D66B955282EE951008272D4 /* AbsFileSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsFileSystem.m; sourceTree = "<group>"; };
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = "<group>"; };
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -111,6 +119,10 @@
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
4D66B951282EE822008272D4 /* AbsDownloader.m */,
4D66B953282EE87C008272D4 /* AbsDownloader.swift */,
4D66B955282EE951008272D4 /* AbsFileSystem.m */,
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */,
);
path = plugins;
sourceTree = "<group>";
@@ -303,16 +315,20 @@
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */,
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -459,12 +475,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.44;
MARKETING_VERSION = 0.9.46;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -483,12 +499,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = "";
CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.44;
MARKETING_VERSION = 0.9.46;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+13
View File
@@ -0,0 +1,13 @@
//
// AbsDownloader.m
// App
//
// Created by advplyr on 5/13/22.
//
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(AbsDownloader, "AbsDownloader",
CAP_PLUGIN_METHOD(downloadLibraryItem, CAPPluginReturnPromise);
)
+71
View File
@@ -0,0 +1,71 @@
//
// AbsDownloader.swift
// App
//
// Created by advplyr on 5/13/22.
//
import Foundation
import Capacitor
@objc(AbsDownloader)
public class AbsDownloader: CAPPlugin {
@objc func downloadLibraryItem(_ call: CAPPluginCall) {
let libraryItemId = call.getString("libraryItemId")
let episodeId = call.getString("episodeId")
NSLog("Download library item \(libraryItemId ?? "N/A") episode \(episodeId ?? "")")
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId!, episodeId: episodeId) { libraryItem in
if (libraryItem == nil) {
NSLog("Library item not found")
call.resolve()
} else {
NSLog("Got library item \(libraryItem!)")
// TODO: break out in seperate functions
libraryItem!.media.tracks?.forEach { track in
NSLog("TRACK \(track.contentUrl!)")
// filename needs to be encoded otherwise would just use contentUrl
let filename = track.metadata?.filename ?? ""
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
let urlstr = "\(Store.serverConfig!.address)/s/item/\(libraryItemId!)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
let url = URL(string: urlstr)!
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let itemDirectory = documentsDirectory.appendingPathComponent("\(libraryItemId!)")
NSLog("ITEM DIR \(itemDirectory)")
// Create library item directory
do {
try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: false)
} catch {
NSLog("Failed to CREATE LI DIRECTORY \(error)")
}
// Output filename
let trackFilename = itemDirectory.appendingPathComponent("\(filename)")
let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, responseOrNil, errorOrNil in
guard let fileURL = urlOrNil else { return }
do {
NSLog("Download TMP file URL \(fileURL)")
let imageData = try Data(contentsOf:fileURL)
try imageData.write(to: trackFilename)
NSLog("Download written to \(trackFilename)")
} catch {
NSLog("FILE ERROR: \(error)")
}
}
downloadTask.resume()
}
call.resolve()
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
//
// AbsFileSystem.m
// App
//
// Created by advplyr on 5/13/22.
//
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(AbsFileSystem, "AbsFileSystem",
CAP_PLUGIN_METHOD(selectFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(checkFolderPermission, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(scanFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(removeFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(removeLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(scanLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(deleteItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(deleteTrackFromItem, CAPPluginReturnPromise);
)
+91
View File
@@ -0,0 +1,91 @@
//
// AbsFileSystem.swift
// App
//
// Created by advplyr on 5/13/22.
//
import Foundation
import Capacitor
@objc(AbsFileSystem)
public class AbsFileSystem: CAPPlugin {
@objc func selectFolder(_ call: CAPPluginCall) {
let mediaType = call.getString("mediaType")
// TODO: Implement
NSLog("Select Folder for media type \(mediaType ?? "UNSET")")
call.resolve()
}
@objc func checkFolderPermission(_ call: CAPPluginCall) {
let folderUrl = call.getString("folderUrl")
// TODO: Is this even necessary on iOS?
NSLog("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
call.resolve([
"value": true
])
}
@objc func scanFolder(_ call: CAPPluginCall) {
let folderId = call.getString("folderId")
let forceAudioProbe = call.getBool("forceAudioProbe", false)
// TODO: Implement
NSLog("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
call.resolve()
}
@objc func removeFolder(_ call: CAPPluginCall) {
let folderId = call.getString("folderId")
// TODO: Implement
NSLog("removeFolder \(folderId ?? "UNSET")")
call.resolve()
}
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
let localLibraryItemId = call.getString("localLibraryItemId")
// TODO: Implement
NSLog("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
call.resolve()
}
@objc func scanLocalLibraryItem(_ call: CAPPluginCall) {
let localLibraryItemId = call.getString("localLibraryItemId")
let forceAudioProbe = call.getBool("forceAudioProbe", false)
// TODO: Implement
NSLog("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
call.resolve()
}
@objc func deleteItem(_ call: CAPPluginCall) {
let localLibraryItemId = call.getString("localLibraryItemId")
let contentUrl = call.getString("contentUrl")
// TODO: Implement
NSLog("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
call.resolve()
}
@objc func deleteTrackFromItem(_ call: CAPPluginCall) {
let localLibraryItemId = call.getString("localLibraryItemId")
let trackLocalFileId = call.getString("trackLocalFileId")
let contentUrl = call.getString("contentUrl")
// TODO: Implement
NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET") url \(contentUrl ?? "UNSET")")
call.resolve()
}
}
-1
View File
@@ -15,7 +15,6 @@ def capacitor_pods
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'
end
target 'App' do
+14
View File
@@ -15,6 +15,7 @@ struct LibraryItem: Codable {
var folderId: String
var path: String
var relPath: String
var isFile: Bool
var mtimeMs: Int64
var ctimeMs: Int64
var birthtimeMs: Int64
@@ -27,6 +28,7 @@ struct LibraryItem: Codable {
var mediaType: String
var media: MediaType
var libraryFiles: [LibraryFile]
var userMediaProgress:MediaProgress?
}
struct MediaType: Codable {
var libraryItemId: String?
@@ -125,3 +127,15 @@ struct LibraryFile: Codable {
var ino: String
var metadata: FileMetadata
}
struct MediaProgress:Codable {
var id:String
var libraryItemId:String
var episodeId:String?
var duration:Double
var progress:Double
var currentTime:Double
var isFinished:Bool
var lastUpdate:Int64
var startedAt:Int64
var finishedAt:Int64?
}
+31
View File
@@ -60,6 +60,27 @@ class ApiClient {
}
}
}
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
if (Store.serverConfig == nil) {
NSLog("Server config not set")
callback?(nil)
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .get, encoding: JSONEncoding.default, headers: headers).responseDecodable(of: decodable) { response in
switch response.result {
case .success(let obj):
callback?(obj)
case .failure(let error):
NSLog("api request to \(endpoint) failed")
print(error)
}
}
}
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, forceTranscode:Bool, callback: @escaping (_ param: PlaybackSession) -> Void) {
var endpoint = "api/items/\(libraryItemId)/play"
@@ -83,4 +104,14 @@ class ApiClient {
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) {
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
}
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
if episodeId != nil {
endpoint += "&episodeId=\(episodeId!)"
}
ApiClient.getResource(endpoint: endpoint, decodable: LibraryItem.self) { obj in
callback(obj)
}
}
}
-33
View File
@@ -12,8 +12,6 @@
</template>
<script>
import { AppUpdate } from '@robingenz/capacitor-app-update'
export default {
data() {
return {
@@ -79,36 +77,6 @@ export default {
this.$refs.streamContainer.streamOpen(stream)
}
},
async clickUpdateToast() {
var immediateUpdateAllowed = this.$store.state.appUpdateInfo.immediateUpdateAllowed
if (immediateUpdateAllowed) {
await AppUpdate.performImmediateUpdate()
} else {
await AppUpdate.openAppStore()
}
},
async checkForUpdate() {
if (this.$platform == 'web') return
console.log('Checking for app update')
const result = await AppUpdate.getAppUpdateInfo()
if (!result) {
console.error('Invalid version check')
return
}
console.log('App Update Info', JSON.stringify(result))
this.$store.commit('setAppUpdateInfo', result)
if (result.updateAvailability === 2) {
setTimeout(() => {
this.$toast.info(`Update is available! Click to update.`, {
draggable: false,
hideProgressBar: false,
timeout: 20000,
closeButton: true,
onClick: this.clickUpdateToast
})
}, 5000)
}
},
async loadSavedSettings() {
var userSavedServerSettings = await this.$localStore.getServerSettings()
if (userSavedServerSettings) {
@@ -255,7 +223,6 @@ export default {
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalMediaProgress()
this.$store.dispatch('globals/loadLocalMediaProgress')
this.checkForUpdate()
this.loadSavedSettings()
this.hasMounted = true
}
+18766 -45
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,9 +1,9 @@
{
"name": "audiobookshelf-app",
"version": "0.9.44-beta",
"version": "0.9.46-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname localhost --port 1337",
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
@@ -23,7 +23,6 @@
"@capacitor/status-bar": "^1.0.8",
"@capacitor/storage": "^1.2.5",
"@nuxtjs/axios": "^5.13.6",
"@robingenz/capacitor-app-update": "^1.3.1",
"core-js": "^3.15.1",
"date-fns": "^2.25.0",
"epubjs": "^0.3.88",
-37
View File
@@ -19,19 +19,10 @@
</div>
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate">Update is available</ui-btn>
<ui-btn v-if="!isUpdateAvailable || immediateUpdateAllowed" class="w-full my-4" color="primary" @click="openAppStore">Open app store</ui-btn>
<p class="text-xs text-gray-400">UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}</p>
</div>
</template>
<script>
import { AppUpdate } from '@robingenz/capacitor-app-update'
import { AbsAudioPlayer } from '@/plugins/capacitor'
export default {
asyncData({ redirect, store }) {
if (!store.state.socketConnected) {
@@ -58,24 +49,6 @@ export default {
},
serverAddress() {
return this.serverConnectionConfig.address
},
appUpdateInfo() {
return this.$store.state.appUpdateInfo
},
availableVersion() {
return this.appUpdateInfo ? this.appUpdateInfo.availableVersion : null
},
currentVersion() {
return this.appUpdateInfo ? this.appUpdateInfo.currentVersion : null
},
immediateUpdateAllowed() {
return this.appUpdateInfo ? !!this.appUpdateInfo.immediateUpdateAllowed : false
},
updateAvailability() {
return this.appUpdateInfo ? this.appUpdateInfo.updateAvailability : null
},
isUpdateAvailable() {
return this.updateAvailability === 2
}
},
methods: {
@@ -85,16 +58,6 @@ export default {
})
this.$server.logout()
this.$router.push('/connect')
},
openAppStore() {
AppUpdate.openAppStore()
},
async clickUpdate() {
if (this.immediateUpdateAllowed) {
AppUpdate.performImmediateUpdate()
} else {
AppUpdate.openAppStore()
}
}
},
mounted() {}
+25 -11
View File
@@ -49,7 +49,7 @@
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn>
<ui-btn v-if="user && showPlay && !isIos && !hasLocal" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mr-2" :padding-x="2" @click="downloadClick">
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mr-2" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
</ui-btn>
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
@@ -117,6 +117,9 @@ export default {
isIos() {
return this.$platform === 'ios'
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
isLocal() {
return this.libraryItem.isLocal
},
@@ -235,6 +238,10 @@ export default {
showRead() {
return this.ebookFile && this.ebookFormat !== 'pdf'
},
showDownload() {
if (this.isIos) return false
return this.user && this.userCanDownload && this.showPlay && !this.hasLocal
},
ebookFile() {
return this.media.ebookFile
},
@@ -242,9 +249,6 @@ export default {
if (!this.ebookFile) return null
return this.ebookFile.ebookFormat
},
hasStoragePermission() {
return this.$store.state.hasStoragePermission
},
downloadItem() {
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
},
@@ -319,13 +323,17 @@ export default {
if (this.downloadItem) {
return
}
this.download()
},
async download(selectedLocalFolder = null) {
if (!this.numTracks) {
return
}
if (this.isIos) {
// no local folders on iOS
this.startDownload()
} else {
this.download()
}
},
async download(selectedLocalFolder = null) {
// Get the local folder to download to
var localFolder = selectedLocalFolder
if (!localFolder) {
@@ -363,9 +371,15 @@ export default {
this.startDownload(localFolder)
}
},
async startDownload(localFolder) {
console.log('Starting download to local folder', localFolder.name)
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id })
async startDownload(localFolder = null) {
const payload = {
libraryItemId: this.libraryItemId
}
if (localFolder) {
console.log('Starting download to local folder', localFolder.name)
payload.localFolderId = localFolder.id
}
var downloadRes = await AbsDownloader.downloadLibraryItem(payload)
if (downloadRes && downloadRes.error) {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download error', errorMsg)
-3
View File
@@ -1,8 +1,5 @@
import { Capacitor } from '@capacitor/core';
import { AbsDatabase } from './capacitor/AbsDatabase'
const isWeb = Capacitor.getPlatform() == 'web'
class DbService {
constructor() { }
+5 -5
View File
@@ -7,7 +7,6 @@ export const state = () => ({
playerIsPlaying: false,
isCasting: false,
isCastAvailable: false,
appUpdateInfo: null,
socketConnected: false,
networkConnected: false,
networkConnectionType: null,
@@ -17,7 +16,8 @@ export const state = () => ({
showReader: false,
showSideDrawer: false,
isNetworkListenerInit: false,
serverSettings: null
serverSettings: null,
lastBookshelfScrollData: {}
})
export const getters = {
@@ -55,6 +55,9 @@ export const actions = {
}
export const mutations = {
setLastBookshelfScrollData(state, { scrollTop, path, name }) {
state.lastBookshelfScrollData[name] = { scrollTop, path }
},
setPlayerItem(state, playbackSession) {
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
@@ -84,9 +87,6 @@ export const mutations = {
setIsFirstLoad(state, val) {
state.isFirstLoad = val
},
setAppUpdateInfo(state, info) {
state.appUpdateInfo = info
},
setSocketConnected(state, val) {
state.socketConnected = val
},
+3
View File
@@ -35,6 +35,9 @@ export const getters = {
},
getUserSetting: (state) => (key) => {
return state.settings ? state.settings[key] || null : null
},
getUserCanDownload: (state) => {
return state.user && state.user.permissions ? !!state.user.permissions.download : false
}
}