Compare commits

...
Author SHA1 Message Date
advplyr 15b7fbce47 iOS version bump 0.9.43 2022-04-28 18:06:13 -05:00
advplyr e03f878865 iOS fix: Logging out when player is open crashing because server config is nil, added nil check in Api requests 2022-04-28 18:05:33 -05:00
advplyr cffa7f5344 Web and android version bump 0.9.43-beta 2022-04-28 17:44:44 -05:00
advplyr 8411428241 Fix:Auto rewind and jump forward/jump backward on multi-tracks #143 2022-04-28 17:19:02 -05:00
advplyr be885009ad Change:persist last search term #142 2022-04-26 17:05:49 -05:00
advplyr d64dd63ea4 Fix:Close playback when logging out 2022-04-26 16:46:29 -05:00
advplyrandGitHub 72f10e5f42 Merge pull request #148 from selfhost-alt/fix-local-scanning-android
Handle audio probes that don't return any tags
2022-04-26 16:13:04 -05:00
Selfhost Alt 511386e80a Handle audio probes that don't return any tags 2022-04-26 10:32:14 -07:00
advplyr b219756cb7 Fix:Save local media progress when downloading library item by requesting user media progress with item from server (will require server update) #145 2022-04-25 19:01:20 -05:00
advplyr 31c753ffcc Fix:Downloading cover art query strings to set token and jpeg format #144 2022-04-25 17:30:59 -05:00
advplyr 2dd822e04d Fix: Sleep timer for multi-track playback sessions - use duration of all tracks stored in playback session instead of duration from exoplayer #140 2022-04-25 17:22:12 -05:00
advplyrandGitHub d18972e2f3 Merge pull request #132 from benonymity/DataClasses
New Data Classes
2022-04-25 07:27:22 -05:00
benonymity 81ca757c77 Book-Podcast combination, optional fixes 2022-04-25 00:15:44 -04:00
BenandGitHub 7189588c2b Merge branch 'advplyr:master' into DataClasses 2022-04-24 22:47:48 -04:00
advplyr 843f6ca606 Update home page for displaying podcast episode shelves 2022-04-24 14:15:37 -05:00
advplyr 52fd8ac5e8 Fix crash when probing audio files by updating start/end chapter data type to Long #128, remove unused files and generic database save/load methods 2022-04-24 13:28:14 -05:00
benonymity 4d0d1eb88f New Data Classes 2022-04-22 20:31:45 -04:00
38 changed files with 596 additions and 287 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 71
versionName "0.9.42-beta"
versionCode 72
versionName "0.9.43-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -2,11 +2,6 @@ package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.jsonschema.JsonSerializableSchema
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeStream(
@@ -27,15 +22,15 @@ data class AudioProbeChapterTags(
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeChapter(
val id:Int,
val start:Int,
val end:Int,
val start:Long,
val end:Long,
val tags:AudioProbeChapterTags?
) {
@JsonIgnore
fun getBookChapter():BookChapter {
var startS = start / 1000.0
var endS = end / 1000.0
var title = tags?.title ?: "Chapter $id"
val startS = start / 1000.0
val endS = end / 1000.0
val title = tags?.title ?: "Chapter $id"
return BookChapter(id, startS, endS, title)
}
}
@@ -57,7 +52,7 @@ data class AudioProbeFormat(
val duration:Double,
val size:Long,
val bit_rate:Double,
val tags:AudioProbeFormatTags
val tags:AudioProbeFormatTags?
)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -68,8 +63,8 @@ class AudioProbeResult (
val duration get() = format.duration
val size get() = format.size
val title get() = format.tags.title ?: format.filename.split("/").last()
val artist get() = format.tags.artist ?: ""
val title get() = format.tags?.title ?: format.filename.split("/").last()
val artist get() = format.tags?.artist ?: ""
@JsonIgnore
fun getBookChapters(): List<BookChapter> {
@@ -25,7 +25,8 @@ data class LibraryItem(
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var libraryFiles:MutableList<LibraryFile>?
var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads)
) : LibraryItemWrapper() {
@get:JsonIgnore
val title get() = media.metadata.title
@@ -85,7 +86,7 @@ class Podcast(
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
var tracks = episodes?.map { it.audioTrack }
val tracks = episodes?.map { it.audioTrack }
return tracks?.filterNotNull() ?: mutableListOf()
}
@JsonIgnore
@@ -98,7 +99,7 @@ class Podcast(
// Add new episodes
audioTracks.forEach { at ->
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
var 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_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
episodes?.add(newEpisode)
}
}
@@ -111,7 +112,7 @@ class Podcast(
}
@JsonIgnore
override fun addAudioTrack(audioTrack:AudioTrack) {
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
episodes?.add(newEpisode)
var index = 1
@@ -131,8 +132,8 @@ class Podcast(
}
}
@JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode) {
var newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
val newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
episodes?.add(newEpisode)
var index = 1
@@ -140,6 +141,7 @@ class Podcast(
it.index = index
index++
}
return newEpisode
}
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
@@ -355,3 +357,17 @@ data class BookChapter(
var end:Double,
var title:String?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgress(
var id:String,
var libraryItemId:String,
var episodeId:String?,
var duration:Double, // seconds
var progress:Double, // 0 to 1
var currentTime:Double,
var isFinished:Boolean,
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?
)
@@ -17,9 +17,9 @@ class DbManager {
}
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
val localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
Paper.book("localLibraryItems").allKeys.forEach {
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
val localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
localLibraryItems.add(localLibraryItem)
}
@@ -28,7 +28,7 @@ class DbManager {
}
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> {
var localLibraryItems = getLocalLibraryItems()
val localLibraryItems = getLocalLibraryItems()
return localLibraryItems.filter {
it.folderId == folderId
}
@@ -65,7 +65,7 @@ class DbManager {
}
fun getAllLocalFolders():List<LocalFolder> {
var localFolders:MutableList<LocalFolder> = mutableListOf()
val localFolders:MutableList<LocalFolder> = mutableListOf()
Paper.book("localFolders").allKeys.forEach { localFolderId ->
Paper.book("localFolders").read<LocalFolder>(localFolderId)?.let {
localFolders.add(it)
@@ -75,7 +75,7 @@ class DbManager {
}
fun removeLocalFolder(folderId:String) {
var localLibraryItems = getLocalLibraryItemsInFolder(folderId)
val localLibraryItems = getLocalLibraryItemsInFolder(folderId)
localLibraryItems.forEach {
Paper.book("localLibraryItems").delete(it.id)
}
@@ -91,7 +91,7 @@ class DbManager {
}
fun getDownloadItems():List<AbsDownloader.DownloadItem> {
var downloadItems:MutableList<AbsDownloader.DownloadItem> = mutableListOf()
val downloadItems:MutableList<AbsDownloader.DownloadItem> = mutableListOf()
Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
Paper.book("downloadItems").read<AbsDownloader.DownloadItem>(downloadItemId)?.let {
downloadItems.add(it)
@@ -108,7 +108,7 @@ class DbManager {
return Paper.book("localMediaProgress").read(localMediaProgressId)
}
fun getAllLocalMediaProgress():List<LocalMediaProgress> {
var mediaProgress:MutableList<LocalMediaProgress> = mutableListOf()
val mediaProgress:MutableList<LocalMediaProgress> = mutableListOf()
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let {
mediaProgress.add(it)
@@ -126,14 +126,14 @@ class DbManager {
// Make sure all local file ids still exist
fun cleanLocalLibraryItems() {
var localLibraryItems = getLocalLibraryItems()
val localLibraryItems = getLocalLibraryItems()
localLibraryItems.forEach { lli ->
var hasUpates = false
// Check local files
lli.localFiles = lli.localFiles.filter { localFile ->
var file = File(localFile.absolutePath)
val file = File(localFile.absolutePath)
if (!file.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}")
hasUpates = true
@@ -143,7 +143,7 @@ class DbManager {
// Check audio tracks and episodes
if (lli.isPodcast) {
var podcast = lli.media as Podcast
val podcast = lli.media as Podcast
podcast.episodes = podcast.episodes?.filter { ep ->
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
@@ -152,7 +152,7 @@ class DbManager {
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
} as MutableList<PodcastEpisode>
} else {
var book = lli.media as Book
val book = lli.media as Book
book.tracks = book.tracks?.filter { track ->
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}")
@@ -164,7 +164,7 @@ class DbManager {
// Check cover still there
lli.coverAbsolutePath?.let {
var coverFile = File(it)
val coverFile = File(it)
if (!coverFile.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
@@ -183,10 +183,10 @@ class DbManager {
// Remove any local media progress where the local media item is not found
fun cleanLocalMediaProgress() {
var localMediaProgress = getAllLocalMediaProgress()
var localLibraryItems = getLocalLibraryItems()
val localMediaProgress = getAllLocalMediaProgress()
val localLibraryItems = getLocalLibraryItems()
localMediaProgress.forEach {
var matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
if (matchingLLI == null) {
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
Paper.book("localMediaProgress").delete(it.id)
@@ -195,8 +195,8 @@ class DbManager {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress has no episode id - removing")
Paper.book("localMediaProgress").delete(it.id)
} else {
var podcast = matchingLLI.media as Podcast
var matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId }
val podcast = matchingLLI.media as Podcast
val matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId }
if (matchingLEp == null) {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing")
Paper.book("localMediaProgress").delete(it.id)
@@ -212,15 +212,4 @@ class DbManager {
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
return Paper.book("localPlaybackSession").read(playbackSessionId)
}
fun saveObject(db:String, key:String, value:JSONObject) {
Log.d(tag, "Saving Object $key ${value.toString()}")
Paper.book(db).write(key, value)
}
fun loadObject(db:String, key:String):JSONObject? {
var json: JSONObject? = Paper.book(db).read(key)
Log.d(tag, "Loaded Object $key $json")
return json
}
}
@@ -51,8 +51,8 @@ data class LocalLibraryItem(
@JsonIgnore
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
media.setAudioTracks(audioTracks)
localFiles = _localFiles
media.setAudioTracks(audioTracks)
if (coverContentUrl != null) {
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
@@ -66,24 +66,25 @@ data class LocalLibraryItem(
@JsonIgnore
fun getPlaybackSession(episode:PodcastEpisode?):PlaybackSession {
var localEpisodeId = episode?.id
var sessionId = "play_local_${UUID.randomUUID()}"
val localEpisodeId = episode?.id
val sessionId = "play_local_${UUID.randomUUID()}"
// Get current progress for local media
val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
var currentTime = mediaProgress?.currentTime ?: 0.0
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
val currentTime = mediaProgress?.currentTime ?: 0.0
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
var mediaMetadata = media.metadata
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
val mediaMetadata = media.metadata
val chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
var authorName = mediaMetadata.getAuthorDisplayName()
val authorName = mediaMetadata.getAuthorDisplayName()
if (episode != null) { // Get podcast episode audio track
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
}
var dateNow = System.currentTimeMillis()
val dateNow = System.currentTimeMillis()
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
}
@@ -57,6 +57,8 @@ class PlaybackSession(
@get:JsonIgnore
val currentTimeMs get() = (currentTime * 1000L).toLong()
@get:JsonIgnore
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
@get:JsonIgnore
val localLibraryItemId get() = localLibraryItem?.id ?: ""
@get:JsonIgnore
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
@@ -19,6 +19,7 @@ class FolderScanner(var ctx: Context) {
private val tag = "FolderScanner"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
data class DownloadItemScanResult(val localLibraryItem:LocalLibraryItem, var localMediaProgress:LocalMediaProgress?)
private fun getLocalLibraryItemId(mediaItemId:String):String {
return "local_" + DeviceManager.getBase64Id(mediaItemId)
@@ -36,7 +37,7 @@ class FolderScanner(var ctx: Context) {
}
}
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localFolder.contentUrl))
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localFolder.contentUrl))
if (df == null) {
Log.e(tag, "Folder Doc File Invalid $localFolder.contentUrl")
@@ -49,7 +50,7 @@ class FolderScanner(var ctx: Context) {
var mediaItemsUpToDate = 0
// Search for files in media item folder
var foldersFound = df.search(false, DocumentFileType.FOLDER)
val foldersFound = df.search(false, DocumentFileType.FOLDER)
// Match folders found with local library items already saved in db
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
@@ -57,7 +58,7 @@ class FolderScanner(var ctx: Context) {
// Remove existing items no longer there
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
Log.d(tag, "scanForMediaItems Checking Existing LLI ${lli.id}")
var fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
val fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
if (fileFound == null) {
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
DeviceManager.dbManager.removeLocalLibraryItem(lli.id)
@@ -68,9 +69,9 @@ class FolderScanner(var ctx: Context) {
foldersFound.forEach { itemFolder ->
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
var existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
var result = scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)
val result = scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)
if (result == ItemScanResult.REMOVED) mediaItemsRemoved++
else if (result == ItemScanResult.UPDATED) mediaItemsUpdated++
@@ -90,25 +91,23 @@ class FolderScanner(var ctx: Context) {
}
fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
var itemFolderName = itemFolder.name ?: ""
var itemId = getLocalLibraryItemId(itemFolder.id)
val itemFolderName = itemFolder.name ?: ""
val itemId = getLocalLibraryItemId(itemFolder.id)
var existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
val existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
val existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
var isNewOrUpdated = existingItem == null
var audioTracks = mutableListOf<AudioTrack>()
var localFiles = mutableListOf<LocalFile>()
val audioTracks = mutableListOf<AudioTrack>()
val localFiles = mutableListOf<LocalFile>()
var index = 1
var startOffset = 0.0
var coverContentUrl:String? = null
var coverAbsolutePath:String? = null
// itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio"))
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
var filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
var existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
}
if (existingLocalFilesRemoved.isNotEmpty()) {
@@ -117,14 +116,14 @@ class FolderScanner(var ctx: Context) {
}
filesInFolder.forEach { file ->
var mimeType = file.mimeType ?: ""
var filename = file.name ?: ""
var isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
val mimeType = file.mimeType ?: ""
val filename = file.name ?: ""
val isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
var localFileId = DeviceManager.getBase64Id(file.id)
val localFileId = DeviceManager.getBase64Id(file.id)
var localFile = LocalFile(localFileId,filename,file.uri.toString(),file.getBasePath(ctx), file.getAbsolutePath(ctx),file.getSimplePath(ctx),mimeType,file.length())
val localFile = LocalFile(localFileId,filename,file.uri.toString(),file.getBasePath(ctx), file.getAbsolutePath(ctx),file.getSimplePath(ctx),mimeType,file.length())
localFiles.add(localFile)
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
@@ -132,7 +131,7 @@ class FolderScanner(var ctx: Context) {
if (isAudio) {
var audioTrackToAdd:AudioTrack? = null
var existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
if (existingAudioTrack != null) { // Update existing audio track
if (existingAudioTrack.index != index) {
Log.d(tag, "scanLibraryItemFolder Updating Audio track index from ${existingAudioTrack.index} to $index")
@@ -150,7 +149,7 @@ class FolderScanner(var ctx: Context) {
Log.d(tag, "scanLibraryItemFolder Scanning Audio File Path ${localFile.absolutePath} | ForceAudioProbe=${forceAudioProbe}")
// TODO: Make asynchronous
var audioProbeResult = probeAudioFile(localFile.absolutePath)
val audioProbeResult = probeAudioFile(localFile.absolutePath)
if (existingAudioTrack != null) {
// Update audio probe data on existing audio track
@@ -172,7 +171,7 @@ class FolderScanner(var ctx: Context) {
index++
audioTracks.add(audioTrackToAdd)
} else {
var existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
if (existingLocalFile == null) {
Log.d(tag, "scanLibraryItemFolder new local file found ${localFile.absolutePath}")
@@ -209,8 +208,8 @@ class FolderScanner(var ctx: Context) {
return ItemScanResult.UPDATED
} else if (audioTracks.isNotEmpty()) {
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
var localLibraryItem = localMediaItem.getLocalLibraryItem()
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
val localLibraryItem = localMediaItem.getLocalLibraryItem()
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
return ItemScanResult.ADDED
} else {
@@ -219,9 +218,9 @@ class FolderScanner(var ctx: Context) {
}
// Scan item after download and create local library item
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):LocalLibraryItem? {
var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
var foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):DownloadItemScanResult? {
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
val foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
var itemFolderId = ""
var itemFolderUrl = ""
@@ -240,20 +239,21 @@ class FolderScanner(var ctx: Context) {
Log.d(tag, "scanDownloadItem failed to find media folder")
return null
}
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
if (df == null) {
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
return null
}
var localLibraryItemId = getLocalLibraryItemId(itemFolderId)
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
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, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
var localEpisodeId:String? = null
var localLibraryItem:LocalLibraryItem? = null
if (downloadItem.mediaType == "book") {
localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
@@ -266,10 +266,10 @@ class FolderScanner(var ctx: Context) {
}
}
var audioTracks:MutableList<AudioTrack> = mutableListOf()
val audioTracks:MutableList<AudioTrack> = mutableListOf()
filesFound.forEach { docFile ->
var itemPart = downloadItem.downloadItemParts.find { itemPart ->
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
itemPart.filename == docFile.name
}
if (itemPart == null) {
@@ -277,31 +277,32 @@ class FolderScanner(var ctx: Context) {
Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}")
}
} else if (itemPart.audioTrack != null) { // Is audio track
var audioTrackFromServer = itemPart.audioTrack
val audioTrackFromServer = itemPart.audioTrack
Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}")
var localFileId = DeviceManager.getBase64Id(docFile.id)
var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.localFiles.add(localFile)
// TODO: Make asynchronous
var audioProbeResult = probeAudioFile(localFile.absolutePath)
val audioProbeResult = probeAudioFile(localFile.absolutePath)
// Create new audio track
var 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 ?: -1, audioTrackFromServer?.startOffset ?: 0.0, 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}")
// Add podcast episodes to library
itemPart.episode?.let { podcastEpisode ->
var podcast = localLibraryItem.media as Podcast
podcast.addEpisode(track, podcastEpisode)
val podcast = localLibraryItem.media as Podcast
var 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}")
}
} else { // Cover image
var localFileId = DeviceManager.getBase64Id(docFile.id)
var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.coverAbsolutePath = localFile.absolutePath
localLibraryItem.coverContentUrl = localFile.contentUrl
@@ -332,9 +333,36 @@ class FolderScanner(var ctx: Context) {
localLibraryItem.media.setAudioTracks(audioTracks)
}
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null)
// If library item had media progress then make local media progress and save
downloadItem.userMediaProgress?.let { mediaProgress ->
val localMediaProgressId = if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
val newLocalMediaProgress = LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = localEpisodeId,
duration = mediaProgress.duration,
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = false,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
serverConnectionConfigId = downloadItem.serverConnectionConfigId,
serverAddress = downloadItem.serverAddress,
serverUserId = downloadItem.serverUserId,
libraryItemId = downloadItem.libraryItemId,
episodeId = downloadItem.episodeId)
Log.d(tag, "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}")
DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress)
downloadItemScanResult.localMediaProgress = newLocalMediaProgress
}
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
return localLibraryItem
return downloadItemScanResult
}
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
@@ -66,19 +66,26 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
Log.d(tag, "EVENT IS PLAYING CHANGED ${playerNotificationService.getMediaPlayer()}")
if (player.isPlaying) {
Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0) {
if (onSeekBack) onSeekBack = false
else {
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
var backTime = calcPauseSeekBackTime()
if (backTime > 0) {
if (backTime >= playerNotificationService.mPlayer.currentPosition) backTime = playerNotificationService.mPlayer.currentPosition - 500
if (backTime >= playerNotificationService.getCurrentTime()) backTime = playerNotificationService.getCurrentTime() - 500
Log.d(tag, "SeekBackTime $backTime")
onSeekBack = true
playerNotificationService.seekBackward(backTime)
} else {
Log.d(tag, "SeekBackTime: back time is 0")
}
}
}
} else lastPauseTime = System.currentTimeMillis()
} else {
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
lastPauseTime = System.currentTimeMillis()
}
// Start/stop progress sync interval
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
@@ -96,12 +103,10 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
if (lastPauseTime <= 0) return 0
var time: Long = System.currentTimeMillis() - lastPauseTime
var seekback: Long
if (time < 60000) seekback = 0
else if (time < 120000) seekback = 10000
else if (time < 300000) seekback = 15000
else if (time < 1800000) seekback = 20000
else if (time < 3600000) seekback = 25000
else seekback = 29500
if (time < 3000) seekback = 0
else if (time < 300000) seekback = 10000 // 3s to 5m = jump back 10s
else if (time < 1800000) seekback = 20000 // 5m to 30m = jump back 20s
else seekback = 29500 // 30m and up = jump back 30s
return seekback
}
}
@@ -454,7 +454,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
fun getDuration() : Long {
return currentPlayer.duration
return currentPlaybackSession?.totalDurationMs ?: 0L
}
fun getCurrentBookTitle() : String? {
@@ -509,11 +509,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
fun seekForward(amount: Long) {
currentPlayer.seekTo(currentPlayer.currentPosition + amount)
seekPlayer(getCurrentTime() + amount)
// currentPlayer.seekTo(currentPlayer.currentPosition + amount)
}
fun seekBackward(amount: Long) {
currentPlayer.seekTo(currentPlayer.currentPosition - amount)
seekPlayer(getCurrentTime() - amount)
// currentPlayer.seekTo(currentPlayer.currentPosition - amount)
}
fun setPlaybackSpeed(speed: Float) {
@@ -288,38 +288,4 @@ class AbsDatabase : Plugin() {
call.resolve()
}
}
//
// Generic Webview calls to db
//
@PluginMethod
fun saveFromWebview(call: PluginCall) {
var db = call.getString("db", "").toString()
var key = call.getString("key", "").toString()
var value = call.getObject("value")
GlobalScope.launch(Dispatchers.IO) {
if (db == "" || key == "" || value == null) {
Log.d(tag, "saveFromWebview Invalid key/value")
} else {
var json = value as JSONObject
DeviceManager.dbManager.saveObject(db, key, json)
}
call.resolve()
}
}
@PluginMethod
fun loadFromWebview(call:PluginCall) {
var db = call.getString("db", "").toString()
var key = call.getString("key", "").toString()
if (db == "" || key == "") {
Log.d(tag, "loadFromWebview Invalid Key")
call.resolve()
return
}
var json = DeviceManager.dbManager.loadObject(db, key)
var jsobj = JSObject.fromJSONObject(json)
call.resolve(jsobj)
}
}
@@ -63,7 +63,10 @@ class AbsDownloader : Plugin() {
fun make(filename:String, destinationFile:File, finalDestinationFile:File, itemTitle:String, serverPath:String, localFolder:LocalFolder, audioTrack:AudioTrack?, episode:PodcastEpisode?) :DownloadItemPart {
val destinationUri = Uri.fromFile(destinationFile)
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
val downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg
val downloadUri = Uri.parse(downloadUrl)
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
return DownloadItemPart(
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
@@ -102,6 +105,7 @@ class AbsDownloader : Plugin() {
val id: String,
val libraryItemId:String,
val episodeId:String?,
val userMediaProgress:MediaProgress?,
val serverConnectionConfigId:String,
val serverAddress:String,
val serverUserId:String,
@@ -138,7 +142,7 @@ class AbsDownloader : Plugin() {
return call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}"))
}
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
apiHandler.getLibraryItemWithProgress(libraryItemId, episodeId) { libraryItem ->
Log.d(tag, "Got library item from server ${libraryItem.id}")
val localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
@@ -188,7 +192,7 @@ class AbsDownloader : Plugin() {
val tracks = libraryItem.media.getAudioTracks()
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
val itemFolderPath = localFolder.absolutePath + "/" + bookTitle
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf())
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf())
// Create download item part for each audio track
tracks.forEach { audioTrack ->
@@ -215,7 +219,7 @@ class AbsDownloader : Plugin() {
if (downloadItem.downloadItemParts.isNotEmpty()) {
// Add cover download item
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
val serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg"
val serverPath = "/api/items/${libraryItem.id}/cover"
val destinationFilename = "cover.jpg"
val destinationFile = File("$tempFolderPath/$destinationFilename")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
@@ -245,7 +249,7 @@ class AbsDownloader : Plugin() {
Log.d(tag, "Starting podcast episode download")
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
val downloadItemId = "${libraryItem.id}-${episode?.id}"
val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, libraryItem.media, mutableListOf())
val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, libraryItem.userMediaProgress, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, libraryItem.media, mutableListOf())
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack?.relPath ?: "")}"
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
@@ -266,7 +270,7 @@ class AbsDownloader : Plugin() {
downloadItemPart.downloadId = downloadId
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg"
serverPath = "/api/items/${libraryItem.id}/cover"
destinationFilename = "cover.jpg"
destinationFile = File("$tempFolderPath/$destinationFilename")
@@ -290,7 +294,7 @@ class AbsDownloader : Plugin() {
}
}
fun startWatchingDownloads(downloadItem: DownloadItem) {
private fun startWatchingDownloads(downloadItem: DownloadItem) {
GlobalScope.launch(Dispatchers.IO) {
while (downloadItem.downloadItemParts.find { !it.moved && !it.failed } != null) { // While some item is not completed
val numPartsBefore = downloadItem.downloadItemParts.size
@@ -306,23 +310,27 @@ class AbsDownloader : Plugin() {
delay(500)
}
val localLibraryItem = folderScanner.scanDownloadItem(downloadItem)
val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem)
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
downloadQueue.remove(downloadItem)
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${localLibraryItem?.id} | Items remaining in Queue ${downloadQueue.size}")
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id} | Items remaining in Queue ${downloadQueue.size}")
val jsobj = JSObject()
jsobj.put("libraryItemId", downloadItem.id)
jsobj.put("localFolderId", downloadItem.localFolder.id)
if (localLibraryItem != null) {
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
}
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
notifyListeners("onItemDownloadComplete", jsobj)
}
}
fun checkDownloads(downloadItem: DownloadItem) {
private fun checkDownloads(downloadItem: DownloadItem) {
val itemParts = downloadItem.downloadItemParts.map { it }
for (downloadItemPart in itemParts) {
if (downloadItemPart.downloadId != null) {
@@ -88,13 +88,13 @@ class ApiHandler(var ctx:Context) {
response.use {
if (!it.isSuccessful) throw IOException("Unexpected code $response")
var bodyString = it.body!!.string()
val bodyString = it.body!!.string()
if (bodyString == "OK") {
cb(JSObject())
} else {
var jsonObj = JSObject()
if (bodyString.startsWith("[")) {
var array = JSArray(bodyString)
val array = JSArray(bodyString)
jsonObj.put("value", array)
} else {
jsonObj = JSObject(bodyString)
@@ -111,7 +111,7 @@ class ApiHandler(var ctx:Context) {
getRequest("/api/libraries") {
val libraries = mutableListOf<Library>()
if (it.has("value")) {
var array = it.getJSONArray("value")
val array = it.getJSONArray("value")
for (i in 0 until array.length()) {
val library = mapper.readValue<Library>(array.get(i).toString())
libraries.add(library)
@@ -128,11 +128,20 @@ class ApiHandler(var ctx:Context) {
}
}
fun getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, cb: (LibraryItem) -> Unit) {
var requestUrl = "/api/items/$libraryItemId?expanded=1&include=progress"
if (!episodeId.isNullOrEmpty()) requestUrl += "&episode=$episodeId"
getRequest(requestUrl) {
val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
cb(libraryItem)
}
}
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
getRequest("/api/libraries/$libraryId/items?limit=100&minified=1") {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
var array = it.getJSONArray("results")
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
items.add(item)
@@ -146,11 +155,11 @@ class ApiHandler(var ctx:Context) {
getRequest("/api/libraries/$libraryId/personalized") {
val items = mutableListOf<LibraryCategory>()
if (it.has("value")) {
var array = it.getJSONArray("value")
val array = it.getJSONArray("value")
for (i in 0 until array.length()) {
var jsobj = array.get(i) as JSONObject
val jsobj = array.get(i) as JSONObject
var type = jsobj.get("type").toString()
val type = jsobj.get("type").toString()
// Only support for podcast and book in android auto
if (type == "podcast" || type == "book") {
jsobj.put("isLocal", false)
@@ -164,7 +173,7 @@ class ApiHandler(var ctx:Context) {
}
fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
var payload = JSObject()
val payload = JSObject()
payload.put("mediaPlayer", mediaPlayer)
// Only if direct play fails do we force transcode
@@ -181,7 +190,7 @@ class ApiHandler(var ctx:Context) {
}
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: () -> Unit) {
var payload = JSObject(jacksonMapper.writeValueAsString(syncData))
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
postRequest("/api/session/$sessionId/sync", payload) {
cb()
@@ -189,7 +198,7 @@ class ApiHandler(var ctx:Context) {
}
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) {
var payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
postRequest("/api/session/local", payload) {
cb()
@@ -204,15 +213,15 @@ class ApiHandler(var ctx:Context) {
}
// Get all local media progress connected to items on the current connected server
var localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter {
val localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter {
it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id
}
var localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0)
val localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0)
if (localMediaProgress.isNotEmpty()) {
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
var payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
postRequest("/api/me/sync-local-progress", payload) {
Log.d(tag, "Media Progress Sync payload $payload - response ${it.toString()}")
@@ -242,7 +251,7 @@ class ApiHandler(var ctx:Context) {
fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) {
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
var endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
patchRequest(endpoint,updatePayload) {
Log.d(tag, "updateMediaProgress patched progress")
cb()
+2 -2
View File
@@ -18,8 +18,8 @@
<widgets-download-progress-indicator />
<!-- Must be connected to a server to cast, only supports media items on server -->
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer">
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.75rem" @click="castClick">cast</span>
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5">
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.6rem" @click="castClick">cast</span>
</div>
<nuxt-link v-if="user" class="h-7 mx-2" to="/search">
+5
View File
@@ -634,6 +634,11 @@ export default {
this.$nextTick(this.init)
},
beforeDestroy() {
if (this.playbackSession) {
console.log('[AudioPlayer] Before destroy closing playback')
this.closePlayback()
}
this.forceCloseDropdownMenu()
document.body.removeEventListener('touchstart', this.touchstart)
document.body.removeEventListener('touchend', this.touchend)
+5
View File
@@ -108,6 +108,11 @@ export default {
text: 'Local Media',
to: '/localMedia/folders'
})
// items.push({
// icon: 'settings',
// text: 'Settings',
// to: '/settings'
// })
}
return items
},
+1
View File
@@ -3,6 +3,7 @@
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }">
<template v-for="(entity, index) in entities">
<cards-lazy-book-card v-if="type === 'book' || type === 'podcast'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
<cards-lazy-book-card v-if="type === 'episode'" :key="entity.recentEpisode.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
<cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
<cards-author-card v-else-if="type === 'authors'" :key="entity.id" :width="bookWidth / 1.25" :height="bookWidth" :author="entity" :size-multiplier="1" class="mx-2" />
</template>
+57 -31
View File
@@ -34,8 +34,13 @@
</div>
</div>
<!-- Play/pause button for podcast episode -->
<div v-if="recentEpisode" class="absolute z-10 top-0 left-0 bottom-0 right-0 m-auto flex items-center justify-center w-12 h-12 rounded-full bg-white bg-opacity-70">
<span class="material-icons text-6xl text-black text-opacity-80">{{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }}</span>
</div>
<!-- No progress shown for collapsed series in library -->
<div v-if="!collapsedSeries && !isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<div v-if="!collapsedSeries && (!isPodcast || recentEpisode)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
@@ -46,13 +51,18 @@
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
</div>
<!-- Volume number -->
<!-- Series sequence -->
<div v-if="seriesSequence && showSequence && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
</div>
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
</div>
<!-- Podcast Num Episodes -->
<div v-if="numEpisodes && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<div v-else-if="numEpisodes && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
</div>
</div>
@@ -196,6 +206,17 @@ export default {
seriesSequence() {
return this.series ? this.series.sequence : null
},
recentEpisode() {
// Only added to item when getting currently listening podcasts
return this._libraryItem.recentEpisode
},
recentEpisodeNumber() {
if (!this.recentEpisode) return null
if (this.recentEpisode.episode) {
return this.recentEpisode.episode.replace(/^#/, '')
}
return this.recentEpisode.index
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
return this._libraryItem.collapsedSeries
@@ -222,7 +243,14 @@ export default {
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
return null
},
episodeProgress() {
// Only used on home page currently listening podcast shelf
if (!this.recentEpisode) return null
if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.recentEpisode.id)
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
},
userProgress() {
if (this.episodeProgress) return this.episodeProgress
if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
@@ -233,19 +261,28 @@ export default {
return this.userProgress ? !!this.userProgress.isFinished : false
},
showError() {
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
return this.numMissingParts || this.isMissing || this.isInvalid
},
playerIsLocal() {
return !!this.$store.state.playerIsLocal
},
localLibraryItemId() {
if (this.isLocal) return this.libraryItemId
return this.localLibraryItem ? this.localLibraryItem.id : null
},
isStreaming() {
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
if (this.isPodcast) {
if (this.playerIsLocal) {
// Check is streaming local version of this episode
return false // episode cards not implemented for local yet
}
return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.recentEpisode.id)
} else {
return false // not yet necessary for books
}
},
showReadButton() {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
isMissing() {
return this._libraryItem.isMissing
@@ -253,24 +290,9 @@ export default {
isInvalid() {
return this._libraryItem.isInvalid
},
hasMissingParts() {
return this._libraryItem.hasMissingParts
},
hasInvalidParts() {
return this._libraryItem.hasInvalidParts
},
errorText() {
if (this.isMissing) return 'Item directory is missing!'
else if (this.isInvalid) return 'Item has no media files'
var txt = ''
if (this.hasMissingParts) {
txt = `${this.hasMissingParts} missing parts.`
}
if (this.hasInvalidParts) {
if (this.hasMissingParts) txt += ' '
txt += `${this.hasInvalidParts} invalid parts.`
}
return txt || 'Unknown Error'
numMissingParts() {
if (this.isPodcast) return 0
return this.media.numMissingParts
},
overlayWrapperClasslist() {
var classes = []
@@ -343,6 +365,10 @@ export default {
e.stopPropagation()
e.preventDefault()
this.selectBtnClick()
} else if (this.recentEpisode) {
var eventBus = this.$eventBus || this.$nuxt.$eventBus
if (this.streamIsPlaying) eventBus.$emit('pause-item')
else eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id })
} else {
var router = this.$router || this.$nuxt.$router
if (router) {
+1 -1
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>
+48
View File
@@ -0,0 +1,48 @@
<template>
<div>
<div class="border rounded-full border-gray-400 flex items-center cursor-pointer w-10 justify-start" :class="className" @click.stop="clickToggle">
<span class="rounded-full border w-5 h-5 border-gray-100 shadow transform transition-transform duration-100" :class="switchClassName"></span>
</div>
</div>
</template>
<script>
export default {
props: {
value: Boolean,
onColor: {
type: String,
default: 'success'
},
offColor: {
type: String,
default: 'primary'
},
disabled: Boolean
},
computed: {
toggleValue: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
className() {
if (this.disabled) return this.toggleValue ? `bg-${this.onColor} cursor-not-allowed` : `bg-${this.offColor} cursor-not-allowed`
return this.toggleValue ? `bg-${this.onColor}` : `bg-${this.offColor}`
},
switchClassName() {
var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white'
return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor
}
},
methods: {
clickToggle() {
if (this.disabled) return
this.toggleValue = !this.toggleValue
}
}
}
</script>
@@ -97,6 +97,11 @@ export default {
this.$eventBus.$emit('new-local-library-item', data.localLibraryItem)
}
if (data.localMediaProgress) {
console.log('onItemDownloadComplete updating local media progress', data.localMediaProgress.id)
this.$store.commit('globals/updateLocalMediaProgress', data.localMediaProgress)
}
this.$store.commit('globals/removeItemDownload', data.libraryItemId)
}
},
+8 -4
View File
@@ -30,6 +30,7 @@
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D0677428106D0C00B8F875 /* DataClasses.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -60,6 +61,7 @@
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
C4D0677428106D0C00B8F875 /* DataClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataClasses.swift; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -118,6 +120,7 @@
children = (
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */,
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */,
C4D0677428106D0C00B8F875 /* DataClasses.swift */,
3A90295E280968E700E1D427 /* PlaybackReport.swift */,
);
path = models;
@@ -309,6 +312,7 @@
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -455,12 +459,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
CURRENT_PROJECT_VERSION = 6;
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.42;
MARKETING_VERSION = 0.9.43;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.development;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -479,12 +483,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
CURRENT_PROJECT_VERSION = 6;
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.42;
MARKETING_VERSION = 0.9.43;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+127
View File
@@ -0,0 +1,127 @@
//
// DataClasses.swift
// App
//
// Created by Benonymity on 4/20/22.
//
import Foundation
import CoreMedia
struct LibraryItem: Codable {
var id: String
var ino:String
var libraryId: String
var folderId: String
var path: String
var relPath: String
var mtimeMs: Int64
var ctimeMs: Int64
var birthtimeMs: Int64
var addedAt: Int64
var updatedAt: Int64
var lastScan: Int64?
var scanVersion: String?
var isMissing: Bool
var isInvalid: Bool
var mediaType: String
var media: MediaType
var libraryFiles: [LibraryFile]
}
struct MediaType: Codable {
var libraryItemId: String?
var metadata: Metadata
var coverPath: String?
var tags: [String]?
var audioFiles: [AudioTrack]?
var chapters: [Chapter]?
var tracks: [AudioTrack]?
var size: Int64?
var duration: Double?
var episodes: [PodcastEpisode]?
var autoDownloadEpisodes: Bool?
}
struct Metadata: Codable {
var title: String
var subtitle: String?
var authors: [Author]?
var narrators: [String]?
var genres: [String]
var publishedYear: String?
var publishedDate: String?
var publisher: String?
var description: String?
var isbn: String?
var asin: String?
var language: String?
var explicit: Bool
var authorName: String?
var authorNameLF: String?
var narratorName: String?
var seriesName: String?
var feedUrl: String?
}
struct PodcastEpisode: Codable {
var id: String
var index: Int
var episode: String?
var episodeType: String?
var title: String
var subtitle: String?
var description: String?
var audioFile: AudioFile?
var audioTrack: AudioTrack?
var duration: Double
var size: Int64
// var serverEpisodeId: String?
}
struct AudioFile: Codable {
var index: Int
var ino: String
var metadata: FileMetadata
}
struct Author: Codable {
var id: String
var name: String
var coverPath: String?
}
struct Chapter: Codable {
var id: Int
var start: Double
var end: Double
var title: String?
}
struct AudioTrack: Codable {
var index: Int?
var startOffset: Double?
var duration: Double
var title: String?
var contentUrl: String?
var mimeType: String
var metadata: FileMetadata?
// var isLocal: Bool
// var localFileId: String?
// var audioProbeResult: AudioProbeResult? Needed for local playback
var serverIndex: Int?
}
struct FileMetadata: Codable {
var filename: String
var ext: String
var path: String
var relPath: String
}
struct Library: Codable {
var id: String
var name: String
var folders: [Folder]
var icon: String
var mediaType: String
}
struct Folder: Codable {
var id: String
var fullPath: String
}
struct LibraryFile: Codable {
var ino: String
var metadata: FileMetadata
}
+2 -27
View File
@@ -6,7 +6,7 @@
//
import Foundation
struct PlaybackSession: Decodable, Encodable {
var id: String
var userId: String?
@@ -25,33 +25,8 @@ struct PlaybackSession: Decodable, Encodable {
var timeListening: Double
var audioTracks: [AudioTrack]
var currentTime: Double
// var libraryItem: LibraryItem?
var libraryItem: LibraryItem
// var localLibraryItem: LocalLibraryItem?
var serverConnectionConfigId: String?
var serverAddress: String?
}
struct Chapter: Decodable, Encodable {
var id: Int
var start: Double
var end: Double
var title: String?
}
struct AudioTrack: Decodable, Encodable {
var index: Int?
var startOffset: Double
var duration: Double
var title: String
var contentUrl: String
var mimeType: String
var metadata: FileMetadata?
// var isLocal: Bool
// var localFileId: String?
// var audioProbeResult: AudioProbeResult? Needed for local playback
var serverIndex: Int?
}
struct FileMetadata: Decodable, Encodable {
var filename: String
var ext: String
var path: String
var relPath: String
}
+7 -3
View File
@@ -40,7 +40,7 @@ class AudioPlayer: NSObject {
if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" {
NSLog("The player only support HLS streams right now")
self.activeAudioTrack = AudioTrack(index: 0, startOffset: -1, duration: -1, title: "", contentUrl: "", mimeType: "")
self.activeAudioTrack = AudioTrack(index: 0, startOffset: -1, duration: -1, title: "", contentUrl: nil, mimeType: "", metadata: nil, serverIndex: 0)
super.init()
return
@@ -67,6 +67,8 @@ class AudioPlayer: NSObject {
destroy()
}
public func destroy() {
// Pause is not synchronous causing this error on below lines:
// AVAudioSession_iOS.mm:1206 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session
pause()
audioPlayer.replaceCurrentItem(with: nil)
@@ -77,9 +79,11 @@ class AudioPlayer: NSObject {
print(error)
}
// Throws error Possibly related to the error above
// DispatchQueue.main.sync {
UIApplication.shared.endReceivingRemoteControlEvents()
// UIApplication.shared.endReceivingRemoteControlEvents()
// }
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
}
@@ -168,7 +172,7 @@ class AudioPlayer: NSObject {
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl)")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
}
private func initAudioSession() {
do {
+12
View File
@@ -10,6 +10,11 @@ import Alamofire
class ApiClient {
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: String], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
if (Store.serverConfig == nil) {
NSLog("Server config not set")
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
@@ -25,6 +30,12 @@ class ApiClient {
}
}
public static func postResource(endpoint: String, parameters: [String: String], callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) {
NSLog("Server config not set")
callback?(false)
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
@@ -43,6 +54,7 @@ class ApiClient {
}
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, callback: @escaping (_ param: PlaybackSession) -> Void) {
var endpoint = "api/items/\(libraryItemId)/play"
if episodeId != nil {
endpoint += "/\(episodeId!)"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.42-beta",
"version": "0.9.43-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname localhost --port 1337",
-16
View File
@@ -1,16 +0,0 @@
<template>
<div class="w-full h-full">
<p class="text-xl text-center py-8">Under Construction...</p>
</div>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+2 -1
View File
@@ -13,8 +13,9 @@
<p class="text-sm text-gray-400">by {{ author }}</p>
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
{{ $elapsedPretty(duration) }}
<span class="px-4">{{ $bytesPretty(size) }}</span>
<span v-if="!isLocal" class="px-4">{{ $bytesPretty(size) }}</span>
</p>
<p v-if="numTracks" class="text-gray-300 text-sm my-1">{{ numTracks }} Tracks</p>
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
+14 -1
View File
@@ -76,7 +76,11 @@ export default {
},
methods: {
async runSearch(value) {
if (this.isFetching && this.lastSearch === value) return
this.lastSearch = value
this.$store.commit('globals/setLastSearch', value)
if (!this.lastSearch) {
this.bookResults = []
this.podcastResults = []
@@ -89,6 +93,10 @@ export default {
console.error('Search error', error)
return []
})
if (value !== this.lastSearch) {
console.log(`runSearch: New search was made for ${this.lastSearch} - results are from ${value}`)
return
}
console.log('RESULTS', results)
this.isFetching = false
@@ -113,7 +121,12 @@ export default {
}
},
mounted() {
this.$nextTick(this.setFocus())
if (this.$store.state.globals.lastSearch) {
this.search = this.$store.state.globals.lastSearch
this.runSearch(this.search)
} else {
this.$nextTick(this.setFocus())
}
}
}
</script>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div class="w-full h-full p-8">
<div class="flex items-center py-3" @click="toggleDisableAutoRewind">
<div class="w-10 flex justify-center">
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
</div>
<p class="pl-4">Disable Auto Rewind</p>
</div>
<div class="flex items-center py-3" @click="toggleJumpBackwards">
<div class="w-10 flex justify-center">
<span class="material-icons text-4xl">{{ currentJumpBackwardsTimeIcon }}</span>
</div>
<p class="pl-4">Jump backwards time</p>
</div>
<div class="flex items-center py-3" @click="toggleJumpForwards">
<div class="w-10 flex justify-center">
<span class="material-icons text-4xl">{{ currentJumpForwardsTimeIcon }}</span>
</div>
<p class="pl-4">Jump forwards time</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
settings: {
disableAutoRewind: false,
jumpForwardsTime: 10000,
jumpBackwardsTime: 10000
},
jumpForwardsItems: [
{
icon: 'forward_5',
value: 5000
},
{
icon: 'forward_10',
value: 10000
},
{
icon: 'forward_30',
value: 30000
}
],
jumpBackwardsItems: [
{
icon: 'replay_5',
value: 5000
},
{
icon: 'replay_10',
value: 10000
},
{
icon: 'replay_30',
value: 30000
}
]
}
},
computed: {
currentJumpForwardsTimeIcon() {
return this.jumpForwardsItems[this.currentJumpForwardsTimeIndex].icon
},
currentJumpForwardsTimeIndex() {
return this.jumpForwardsItems.findIndex((jfi) => jfi.value === this.settings.jumpForwardsTime)
},
currentJumpBackwardsTimeIcon() {
return this.jumpBackwardsItems[this.currentJumpBackwardsTimeIndex].icon
},
currentJumpBackwardsTimeIndex() {
return this.jumpBackwardsItems.findIndex((jfi) => jfi.value === this.settings.jumpBackwardsTime)
}
},
methods: {
toggleDisableAutoRewind() {
this.settings.disableAutoRewind = !this.settings.disableAutoRewind
this.saveSettings()
},
toggleJumpForwards() {
var next = (this.currentJumpForwardsTimeIndex + 1) % 3
this.settings.jumpForwardsTime = this.jumpForwardsItems[next].value
this.saveSettings()
},
toggleJumpBackwards() {
var next = (this.currentJumpBackwardsTimeIndex + 4) % 3
console.log('next', next)
if (next > 2) return
this.settings.jumpBackwardsTime = this.jumpBackwardsItems[next].value
this.saveSettings()
},
saveSettings() {
// TODO: Save settings
}
},
mounted() {
// TODO: Load settings
}
}
</script>
-22
View File
@@ -6,28 +6,6 @@ const isWeb = Capacitor.getPlatform() == 'web'
class DbService {
constructor() { }
// Please dont use this, it is not implemented in ios (maybe key: primary value: any ?)
save(db, key, value) {
if (isWeb) return
return AbsDatabase.saveFromWebview({ db, key, value }).then(() => {
console.log('Saved data', db, key, JSON.stringify(value))
}).catch((error) => {
console.error('Failed to save data', error)
})
}
// Please dont use this, it is not implemented in ios
load(db, key) {
if (isWeb) return null
return AbsDatabase.loadFromWebview({ db, key }).then((data) => {
console.log('Loaded data', db, key, JSON.stringify(data))
return data
}).catch((error) => {
console.error('Failed to load', error)
return null
})
}
getDeviceData() {
return AbsDatabase.getDeviceData().then((data) => {
console.log('Loaded device data', JSON.stringify(data))
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

+5 -1
View File
@@ -2,7 +2,8 @@ export const state = () => ({
itemDownloads: [],
bookshelfListView: false,
series: null,
localMediaProgress: []
localMediaProgress: [],
lastSearch: null
})
export const getters = {
@@ -82,5 +83,8 @@ export const mutations = {
},
removeLocalMediaProgress(state, id) {
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.id != id)
},
setLastSearch(state, val) {
state.lastSearch = val
}
}
-3
View File
@@ -21,9 +21,6 @@ export const state = () => ({
})
export const getters = {
playerIsOpen: (state) => {
return state.streamAudiobook
},
getIsItemStreaming: state => libraryItemId => {
return state.playerLibraryItemId == libraryItemId
},