mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-29 14:28:34 +02:00
Merge branch 'advplyr:master' into MarkAsRead
This commit is contained in:
commit
2064cd8380
19 changed files with 314 additions and 119 deletions
|
@ -52,7 +52,7 @@ data class AudioProbeFormat(
|
||||||
val duration:Double,
|
val duration:Double,
|
||||||
val size:Long,
|
val size:Long,
|
||||||
val bit_rate:Double,
|
val bit_rate:Double,
|
||||||
val tags:AudioProbeFormatTags
|
val tags:AudioProbeFormatTags?
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@ -63,8 +63,8 @@ class AudioProbeResult (
|
||||||
|
|
||||||
val duration get() = format.duration
|
val duration get() = format.duration
|
||||||
val size get() = format.size
|
val size get() = format.size
|
||||||
val title get() = format.tags.title ?: format.filename.split("/").last()
|
val title get() = format.tags?.title ?: format.filename.split("/").last()
|
||||||
val artist get() = format.tags.artist ?: ""
|
val artist get() = format.tags?.artist ?: ""
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getBookChapters(): List<BookChapter> {
|
fun getBookChapters(): List<BookChapter> {
|
||||||
|
|
|
@ -25,7 +25,8 @@ data class LibraryItem(
|
||||||
var isInvalid:Boolean,
|
var isInvalid:Boolean,
|
||||||
var mediaType:String,
|
var mediaType:String,
|
||||||
var media:MediaType,
|
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() {
|
) : LibraryItemWrapper() {
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val title get() = media.metadata.title
|
val title get() = media.metadata.title
|
||||||
|
@ -131,7 +132,7 @@ class Podcast(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode) {
|
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)
|
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)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
|
@ -140,6 +141,7 @@ class Podcast(
|
||||||
it.index = index
|
it.index = index
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
|
return newEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||||
|
@ -355,3 +357,17 @@ data class BookChapter(
|
||||||
var end:Double,
|
var end:Double,
|
||||||
var title:String?
|
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?
|
||||||
|
)
|
||||||
|
|
|
@ -51,8 +51,8 @@ data class LocalLibraryItem(
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
|
fun updateFromScan(audioTracks:MutableList<AudioTrack>, _localFiles:MutableList<LocalFile>) {
|
||||||
media.setAudioTracks(audioTracks)
|
|
||||||
localFiles = _localFiles
|
localFiles = _localFiles
|
||||||
|
media.setAudioTracks(audioTracks)
|
||||||
|
|
||||||
if (coverContentUrl != null) {
|
if (coverContentUrl != null) {
|
||||||
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
|
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
|
||||||
|
@ -66,24 +66,25 @@ data class LocalLibraryItem(
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getPlaybackSession(episode:PodcastEpisode?):PlaybackSession {
|
fun getPlaybackSession(episode:PodcastEpisode?):PlaybackSession {
|
||||||
var localEpisodeId = episode?.id
|
val localEpisodeId = episode?.id
|
||||||
var sessionId = "play_local_${UUID.randomUUID()}"
|
val sessionId = "play_local_${UUID.randomUUID()}"
|
||||||
|
|
||||||
|
// Get current progress for local media
|
||||||
val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
|
val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
|
||||||
var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
|
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
|
||||||
var currentTime = mediaProgress?.currentTime ?: 0.0
|
val currentTime = mediaProgress?.currentTime ?: 0.0
|
||||||
|
|
||||||
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
|
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
|
||||||
var mediaMetadata = media.metadata
|
val mediaMetadata = media.metadata
|
||||||
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
val chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
||||||
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
||||||
var authorName = mediaMetadata.getAuthorDisplayName()
|
val authorName = mediaMetadata.getAuthorDisplayName()
|
||||||
if (episode != null) { // Get podcast episode audio track
|
if (episode != null) { // Get podcast episode audio track
|
||||||
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
|
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
|
||||||
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
|
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")
|
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
|
@get:JsonIgnore
|
||||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
|
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
||||||
|
@get:JsonIgnore
|
||||||
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
|
|
|
@ -19,6 +19,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
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 {
|
private fun getLocalLibraryItemId(mediaItemId:String):String {
|
||||||
return "local_" + DeviceManager.getBase64Id(mediaItemId)
|
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) {
|
if (df == null) {
|
||||||
Log.e(tag, "Folder Doc File Invalid $localFolder.contentUrl")
|
Log.e(tag, "Folder Doc File Invalid $localFolder.contentUrl")
|
||||||
|
@ -49,7 +50,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
var mediaItemsUpToDate = 0
|
var mediaItemsUpToDate = 0
|
||||||
|
|
||||||
// Search for files in media item folder
|
// 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
|
// Match folders found with local library items already saved in db
|
||||||
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
||||||
|
@ -57,7 +58,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
// Remove existing items no longer there
|
// Remove existing items no longer there
|
||||||
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
|
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
|
||||||
Log.d(tag, "scanForMediaItems Checking Existing LLI ${lli.id}")
|
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) {
|
if (fileFound == null) {
|
||||||
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
|
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(lli.id)
|
DeviceManager.dbManager.removeLocalLibraryItem(lli.id)
|
||||||
|
@ -68,9 +69,9 @@ class FolderScanner(var ctx: Context) {
|
||||||
|
|
||||||
foldersFound.forEach { itemFolder ->
|
foldersFound.forEach { itemFolder ->
|
||||||
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
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++
|
if (result == ItemScanResult.REMOVED) mediaItemsRemoved++
|
||||||
else if (result == ItemScanResult.UPDATED) mediaItemsUpdated++
|
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 {
|
fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
||||||
var itemFolderName = itemFolder.name ?: ""
|
val itemFolderName = itemFolder.name ?: ""
|
||||||
var itemId = getLocalLibraryItemId(itemFolder.id)
|
val itemId = getLocalLibraryItemId(itemFolder.id)
|
||||||
|
|
||||||
var existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
|
val existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
|
||||||
var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
|
val existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
|
||||||
var isNewOrUpdated = existingItem == null
|
var isNewOrUpdated = existingItem == null
|
||||||
|
|
||||||
var audioTracks = mutableListOf<AudioTrack>()
|
val audioTracks = mutableListOf<AudioTrack>()
|
||||||
var localFiles = mutableListOf<LocalFile>()
|
val localFiles = mutableListOf<LocalFile>()
|
||||||
var index = 1
|
var index = 1
|
||||||
var startOffset = 0.0
|
var startOffset = 0.0
|
||||||
var coverContentUrl:String? = null
|
var coverContentUrl:String? = null
|
||||||
var coverAbsolutePath: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"))
|
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
|
|
||||||
var existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
}
|
}
|
||||||
if (existingLocalFilesRemoved.isNotEmpty()) {
|
if (existingLocalFilesRemoved.isNotEmpty()) {
|
||||||
|
@ -117,14 +116,14 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
filesInFolder.forEach { file ->
|
filesInFolder.forEach { file ->
|
||||||
var mimeType = file.mimeType ?: ""
|
val mimeType = file.mimeType ?: ""
|
||||||
var filename = file.name ?: ""
|
val filename = file.name ?: ""
|
||||||
var isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
|
val isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
|
||||||
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
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)
|
localFiles.add(localFile)
|
||||||
|
|
||||||
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
||||||
|
@ -132,7 +131,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
var audioTrackToAdd:AudioTrack? = null
|
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 != null) { // Update existing audio track
|
||||||
if (existingAudioTrack.index != index) {
|
if (existingAudioTrack.index != index) {
|
||||||
Log.d(tag, "scanLibraryItemFolder Updating Audio track index from ${existingAudioTrack.index} to $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}")
|
Log.d(tag, "scanLibraryItemFolder Scanning Audio File Path ${localFile.absolutePath} | ForceAudioProbe=${forceAudioProbe}")
|
||||||
|
|
||||||
// TODO: Make asynchronous
|
// TODO: Make asynchronous
|
||||||
var audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
if (existingAudioTrack != null) {
|
if (existingAudioTrack != null) {
|
||||||
// Update audio probe data on existing audio track
|
// Update audio probe data on existing audio track
|
||||||
|
@ -172,7 +171,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
index++
|
index++
|
||||||
audioTracks.add(audioTrackToAdd)
|
audioTracks.add(audioTrackToAdd)
|
||||||
} else {
|
} else {
|
||||||
var existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
||||||
|
|
||||||
if (existingLocalFile == null) {
|
if (existingLocalFile == null) {
|
||||||
Log.d(tag, "scanLibraryItemFolder new local file found ${localFile.absolutePath}")
|
Log.d(tag, "scanLibraryItemFolder new local file found ${localFile.absolutePath}")
|
||||||
|
@ -209,8 +208,8 @@ class FolderScanner(var ctx: Context) {
|
||||||
return ItemScanResult.UPDATED
|
return ItemScanResult.UPDATED
|
||||||
} else if (audioTracks.isNotEmpty()) {
|
} else if (audioTracks.isNotEmpty()) {
|
||||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
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)
|
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)
|
||||||
var localLibraryItem = localMediaItem.getLocalLibraryItem()
|
val localLibraryItem = localMediaItem.getLocalLibraryItem()
|
||||||
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
return ItemScanResult.ADDED
|
return ItemScanResult.ADDED
|
||||||
} else {
|
} else {
|
||||||
|
@ -219,9 +218,9 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan item after download and create local library item
|
// Scan item after download and create local library item
|
||||||
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):LocalLibraryItem? {
|
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):DownloadItemScanResult? {
|
||||||
var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
||||||
var foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
|
val foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
|
||||||
|
|
||||||
var itemFolderId = ""
|
var itemFolderId = ""
|
||||||
var itemFolderUrl = ""
|
var itemFolderUrl = ""
|
||||||
|
@ -240,20 +239,21 @@ class FolderScanner(var ctx: Context) {
|
||||||
Log.d(tag, "scanDownloadItem failed to find media folder")
|
Log.d(tag, "scanDownloadItem failed to find media folder")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
|
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
|
||||||
|
|
||||||
if (df == null) {
|
if (df == null) {
|
||||||
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
|
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
|
||||||
return null
|
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")
|
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
||||||
|
|
||||||
// Search for files in media item folder
|
// 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}")
|
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
||||||
|
|
||||||
|
var localEpisodeId:String? = null
|
||||||
var localLibraryItem:LocalLibraryItem? = null
|
var localLibraryItem:LocalLibraryItem? = null
|
||||||
if (downloadItem.mediaType == "book") {
|
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)
|
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 ->
|
filesFound.forEach { docFile ->
|
||||||
var itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
||||||
itemPart.filename == docFile.name
|
itemPart.filename == docFile.name
|
||||||
}
|
}
|
||||||
if (itemPart == null) {
|
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}")
|
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
|
} 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}")
|
Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}")
|
||||||
|
|
||||||
var localFileId = DeviceManager.getBase64Id(docFile.id)
|
val 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 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)
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
|
||||||
// TODO: Make asynchronous
|
// TODO: Make asynchronous
|
||||||
var audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
// Create new audio track
|
// 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)
|
audioTracks.add(track)
|
||||||
|
|
||||||
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
||||||
|
|
||||||
// Add podcast episodes to library
|
// Add podcast episodes to library
|
||||||
itemPart.episode?.let { podcastEpisode ->
|
itemPart.episode?.let { podcastEpisode ->
|
||||||
var podcast = localLibraryItem.media as Podcast
|
val podcast = localLibraryItem.media as Podcast
|
||||||
podcast.addEpisode(track, podcastEpisode)
|
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}")
|
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
||||||
}
|
}
|
||||||
} else { // Cover image
|
} else { // Cover image
|
||||||
var localFileId = DeviceManager.getBase64Id(docFile.id)
|
val 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 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.coverAbsolutePath = localFile.absolutePath
|
||||||
localLibraryItem.coverContentUrl = localFile.contentUrl
|
localLibraryItem.coverContentUrl = localFile.contentUrl
|
||||||
|
@ -332,9 +333,36 @@ class FolderScanner(var ctx: Context) {
|
||||||
localLibraryItem.media.setAudioTracks(audioTracks)
|
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)
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
|
|
||||||
return localLibraryItem
|
return downloadItemScanResult
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
||||||
|
|
|
@ -454,7 +454,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDuration() : Long {
|
fun getDuration() : Long {
|
||||||
return currentPlayer.duration
|
return currentPlaybackSession?.totalDurationMs ?: 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentBookTitle() : String? {
|
fun getCurrentBookTitle() : String? {
|
||||||
|
|
|
@ -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 {
|
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 destinationUri = Uri.fromFile(destinationFile)
|
||||||
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
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")
|
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
||||||
return DownloadItemPart(
|
return DownloadItemPart(
|
||||||
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
|
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
|
||||||
|
@ -102,6 +105,7 @@ class AbsDownloader : Plugin() {
|
||||||
val id: String,
|
val id: String,
|
||||||
val libraryItemId:String,
|
val libraryItemId:String,
|
||||||
val episodeId:String?,
|
val episodeId:String?,
|
||||||
|
val userMediaProgress:MediaProgress?,
|
||||||
val serverConnectionConfigId:String,
|
val serverConnectionConfigId:String,
|
||||||
val serverAddress:String,
|
val serverAddress:String,
|
||||||
val serverUserId:String,
|
val serverUserId:String,
|
||||||
|
@ -138,7 +142,7 @@ class AbsDownloader : Plugin() {
|
||||||
return call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}"))
|
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}")
|
Log.d(tag, "Got library item from server ${libraryItem.id}")
|
||||||
|
|
||||||
val localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
|
val localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
|
||||||
|
@ -188,7 +192,7 @@ class AbsDownloader : Plugin() {
|
||||||
val tracks = libraryItem.media.getAudioTracks()
|
val tracks = libraryItem.media.getAudioTracks()
|
||||||
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
||||||
val itemFolderPath = localFolder.absolutePath + "/" + bookTitle
|
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
|
// Create download item part for each audio track
|
||||||
tracks.forEach { audioTrack ->
|
tracks.forEach { audioTrack ->
|
||||||
|
@ -215,7 +219,7 @@ class AbsDownloader : Plugin() {
|
||||||
if (downloadItem.downloadItemParts.isNotEmpty()) {
|
if (downloadItem.downloadItemParts.isNotEmpty()) {
|
||||||
// Add cover download item
|
// Add cover download item
|
||||||
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
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 destinationFilename = "cover.jpg"
|
||||||
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
||||||
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
|
@ -245,7 +249,7 @@ class AbsDownloader : Plugin() {
|
||||||
Log.d(tag, "Starting podcast episode download")
|
Log.d(tag, "Starting podcast episode download")
|
||||||
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
|
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
|
||||||
val downloadItemId = "${libraryItem.id}-${episode?.id}"
|
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 serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack?.relPath ?: "")}"
|
||||||
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
|
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
|
||||||
|
@ -266,7 +270,7 @@ class AbsDownloader : Plugin() {
|
||||||
downloadItemPart.downloadId = downloadId
|
downloadItemPart.downloadId = downloadId
|
||||||
|
|
||||||
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
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"
|
destinationFilename = "cover.jpg"
|
||||||
|
|
||||||
destinationFile = File("$tempFolderPath/$destinationFilename")
|
destinationFile = File("$tempFolderPath/$destinationFilename")
|
||||||
|
@ -290,7 +294,7 @@ class AbsDownloader : Plugin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startWatchingDownloads(downloadItem: DownloadItem) {
|
private fun startWatchingDownloads(downloadItem: DownloadItem) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
while (downloadItem.downloadItemParts.find { !it.moved && !it.failed } != null) { // While some item is not completed
|
while (downloadItem.downloadItemParts.find { !it.moved && !it.failed } != null) { // While some item is not completed
|
||||||
val numPartsBefore = downloadItem.downloadItemParts.size
|
val numPartsBefore = downloadItem.downloadItemParts.size
|
||||||
|
@ -306,18 +310,22 @@ class AbsDownloader : Plugin() {
|
||||||
delay(500)
|
delay(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
val localLibraryItem = folderScanner.scanDownloadItem(downloadItem)
|
val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem)
|
||||||
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
||||||
downloadQueue.remove(downloadItem)
|
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()
|
val jsobj = JSObject()
|
||||||
jsobj.put("libraryItemId", downloadItem.id)
|
jsobj.put("libraryItemId", downloadItem.id)
|
||||||
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
||||||
if (localLibraryItem != null) {
|
|
||||||
|
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
|
||||||
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||||
}
|
}
|
||||||
|
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
|
||||||
|
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||||
|
}
|
||||||
notifyListeners("onItemDownloadComplete", jsobj)
|
notifyListeners("onItemDownloadComplete", jsobj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,13 +88,13 @@ class ApiHandler(var ctx:Context) {
|
||||||
response.use {
|
response.use {
|
||||||
if (!it.isSuccessful) throw IOException("Unexpected code $response")
|
if (!it.isSuccessful) throw IOException("Unexpected code $response")
|
||||||
|
|
||||||
var bodyString = it.body!!.string()
|
val bodyString = it.body!!.string()
|
||||||
if (bodyString == "OK") {
|
if (bodyString == "OK") {
|
||||||
cb(JSObject())
|
cb(JSObject())
|
||||||
} else {
|
} else {
|
||||||
var jsonObj = JSObject()
|
var jsonObj = JSObject()
|
||||||
if (bodyString.startsWith("[")) {
|
if (bodyString.startsWith("[")) {
|
||||||
var array = JSArray(bodyString)
|
val array = JSArray(bodyString)
|
||||||
jsonObj.put("value", array)
|
jsonObj.put("value", array)
|
||||||
} else {
|
} else {
|
||||||
jsonObj = JSObject(bodyString)
|
jsonObj = JSObject(bodyString)
|
||||||
|
@ -111,7 +111,7 @@ class ApiHandler(var ctx:Context) {
|
||||||
getRequest("/api/libraries") {
|
getRequest("/api/libraries") {
|
||||||
val libraries = mutableListOf<Library>()
|
val libraries = mutableListOf<Library>()
|
||||||
if (it.has("value")) {
|
if (it.has("value")) {
|
||||||
var array = it.getJSONArray("value")
|
val array = it.getJSONArray("value")
|
||||||
for (i in 0 until array.length()) {
|
for (i in 0 until array.length()) {
|
||||||
val library = mapper.readValue<Library>(array.get(i).toString())
|
val library = mapper.readValue<Library>(array.get(i).toString())
|
||||||
libraries.add(library)
|
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) {
|
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||||
getRequest("/api/libraries/$libraryId/items?limit=100&minified=1") {
|
getRequest("/api/libraries/$libraryId/items?limit=100&minified=1") {
|
||||||
val items = mutableListOf<LibraryItem>()
|
val items = mutableListOf<LibraryItem>()
|
||||||
if (it.has("results")) {
|
if (it.has("results")) {
|
||||||
var array = it.getJSONArray("results")
|
val array = it.getJSONArray("results")
|
||||||
for (i in 0 until array.length()) {
|
for (i in 0 until array.length()) {
|
||||||
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
|
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
|
||||||
items.add(item)
|
items.add(item)
|
||||||
|
@ -146,11 +155,11 @@ class ApiHandler(var ctx:Context) {
|
||||||
getRequest("/api/libraries/$libraryId/personalized") {
|
getRequest("/api/libraries/$libraryId/personalized") {
|
||||||
val items = mutableListOf<LibraryCategory>()
|
val items = mutableListOf<LibraryCategory>()
|
||||||
if (it.has("value")) {
|
if (it.has("value")) {
|
||||||
var array = it.getJSONArray("value")
|
val array = it.getJSONArray("value")
|
||||||
for (i in 0 until array.length()) {
|
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
|
// Only support for podcast and book in android auto
|
||||||
if (type == "podcast" || type == "book") {
|
if (type == "podcast" || type == "book") {
|
||||||
jsobj.put("isLocal", false)
|
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) {
|
fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
||||||
var payload = JSObject()
|
val payload = JSObject()
|
||||||
payload.put("mediaPlayer", mediaPlayer)
|
payload.put("mediaPlayer", mediaPlayer)
|
||||||
|
|
||||||
// Only if direct play fails do we force transcode
|
// 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) {
|
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) {
|
postRequest("/api/session/$sessionId/sync", payload) {
|
||||||
cb()
|
cb()
|
||||||
|
@ -189,7 +198,7 @@ class ApiHandler(var ctx:Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) {
|
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) {
|
||||||
var payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
||||||
|
|
||||||
postRequest("/api/session/local", payload) {
|
postRequest("/api/session/local", payload) {
|
||||||
cb()
|
cb()
|
||||||
|
@ -204,15 +213,15 @@ class ApiHandler(var ctx:Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all local media progress connected to items on the current connected server
|
// 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
|
it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
var localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0)
|
val localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0)
|
||||||
|
|
||||||
if (localMediaProgress.isNotEmpty()) {
|
if (localMediaProgress.isNotEmpty()) {
|
||||||
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
|
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) {
|
postRequest("/api/me/sync-local-progress", payload) {
|
||||||
Log.d(tag, "Media Progress Sync payload $payload - response ${it.toString()}")
|
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) {
|
fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) {
|
||||||
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
|
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) {
|
patchRequest(endpoint,updatePayload) {
|
||||||
Log.d(tag, "updateMediaProgress patched progress")
|
Log.d(tag, "updateMediaProgress patched progress")
|
||||||
cb()
|
cb()
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
<widgets-download-progress-indicator />
|
<widgets-download-progress-indicator />
|
||||||
|
|
||||||
<!-- Must be connected to a server to cast, only supports media items on server -->
|
<!-- Must be connected to a server to cast, only supports media items on server -->
|
||||||
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer">
|
<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.75rem" @click="castClick">cast</span>
|
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.6rem" @click="castClick">cast</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="user" class="h-7 mx-2" to="/search">
|
<nuxt-link v-if="user" class="h-7 mx-2" to="/search">
|
||||||
|
|
|
@ -634,6 +634,11 @@ export default {
|
||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
if (this.playbackSession) {
|
||||||
|
console.log('[AudioPlayer] Before destroy closing playback')
|
||||||
|
this.closePlayback()
|
||||||
|
}
|
||||||
|
|
||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
document.body.removeEventListener('touchstart', this.touchstart)
|
document.body.removeEventListener('touchstart', this.touchstart)
|
||||||
document.body.removeEventListener('touchend', this.touchend)
|
document.body.removeEventListener('touchend', this.touchend)
|
||||||
|
|
|
@ -97,6 +97,11 @@ export default {
|
||||||
this.$eventBus.$emit('new-local-library-item', data.localLibraryItem)
|
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)
|
this.$store.commit('globals/removeItemDownload', data.libraryItemId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -118,6 +120,7 @@
|
||||||
children = (
|
children = (
|
||||||
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */,
|
3AD4FCE828043FD7006DB301 /* ServerConnectionConfig.swift */,
|
||||||
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */,
|
3ABF580828059BAE005DFBE5 /* PlaybackSession.swift */,
|
||||||
|
C4D0677428106D0C00B8F875 /* DataClasses.swift */,
|
||||||
3A90295E280968E700E1D427 /* PlaybackReport.swift */,
|
3A90295E280968E700E1D427 /* PlaybackReport.swift */,
|
||||||
);
|
);
|
||||||
path = models;
|
path = models;
|
||||||
|
@ -309,6 +312,7 @@
|
||||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
||||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
||||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
||||||
|
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
|
||||||
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
127
ios/App/Shared/models/DataClasses.swift
Normal file
127
ios/App/Shared/models/DataClasses.swift
Normal 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
|
||||||
|
}
|
|
@ -25,33 +25,8 @@ struct PlaybackSession: Decodable, Encodable {
|
||||||
var timeListening: Double
|
var timeListening: Double
|
||||||
var audioTracks: [AudioTrack]
|
var audioTracks: [AudioTrack]
|
||||||
var currentTime: Double
|
var currentTime: Double
|
||||||
// var libraryItem: LibraryItem?
|
var libraryItem: LibraryItem
|
||||||
// var localLibraryItem: LocalLibraryItem?
|
// var localLibraryItem: LocalLibraryItem?
|
||||||
var serverConnectionConfigId: String?
|
var serverConnectionConfigId: String?
|
||||||
var serverAddress: 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ class AudioPlayer: NSObject {
|
||||||
|
|
||||||
if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" {
|
if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" {
|
||||||
NSLog("The player only support HLS streams right now")
|
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()
|
super.init()
|
||||||
return
|
return
|
||||||
|
@ -168,7 +168,7 @@ class AudioPlayer: NSObject {
|
||||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
"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() {
|
private func initAudioSession() {
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -13,8 +13,9 @@
|
||||||
<p class="text-sm text-gray-400">by {{ author }}</p>
|
<p class="text-sm text-gray-400">by {{ author }}</p>
|
||||||
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
||||||
{{ $elapsedPretty(duration) }}
|
{{ $elapsedPretty(duration) }}
|
||||||
<span class="px-4">{{ $bytesPretty(size) }}</span>
|
<span v-if="!isLocal" class="px-4">{{ $bytesPretty(size) }}</span>
|
||||||
</p>
|
</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' : ''">
|
<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>
|
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
|
|
|
@ -76,7 +76,11 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async runSearch(value) {
|
async runSearch(value) {
|
||||||
|
if (this.isFetching && this.lastSearch === value) return
|
||||||
|
|
||||||
this.lastSearch = value
|
this.lastSearch = value
|
||||||
|
this.$store.commit('globals/setLastSearch', value)
|
||||||
|
|
||||||
if (!this.lastSearch) {
|
if (!this.lastSearch) {
|
||||||
this.bookResults = []
|
this.bookResults = []
|
||||||
this.podcastResults = []
|
this.podcastResults = []
|
||||||
|
@ -89,6 +93,10 @@ export default {
|
||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
if (value !== this.lastSearch) {
|
||||||
|
console.log(`runSearch: New search was made for ${this.lastSearch} - results are from ${value}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
console.log('RESULTS', results)
|
console.log('RESULTS', results)
|
||||||
|
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
|
@ -113,7 +121,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
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>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,8 @@ export const state = () => ({
|
||||||
itemDownloads: [],
|
itemDownloads: [],
|
||||||
bookshelfListView: false,
|
bookshelfListView: false,
|
||||||
series: null,
|
series: null,
|
||||||
localMediaProgress: []
|
localMediaProgress: [],
|
||||||
|
lastSearch: null
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -82,5 +83,8 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
removeLocalMediaProgress(state, id) {
|
removeLocalMediaProgress(state, id) {
|
||||||
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.id != id)
|
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.id != id)
|
||||||
|
},
|
||||||
|
setLastSearch(state, val) {
|
||||||
|
state.lastSearch = val
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,9 +21,6 @@ export const state = () => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
playerIsOpen: (state) => {
|
|
||||||
return state.streamAudiobook
|
|
||||||
},
|
|
||||||
getIsItemStreaming: state => libraryItemId => {
|
getIsItemStreaming: state => libraryItemId => {
|
||||||
return state.playerLibraryItemId == libraryItemId
|
return state.playerLibraryItemId == libraryItemId
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue