Add podcast pages, android download podcast, scan podcast folders

This commit is contained in:
advplyr 2022-04-10 20:31:47 -05:00
parent ef65b4c278
commit c94e57f55e
26 changed files with 789 additions and 397 deletions

View file

@ -99,15 +99,44 @@ class Podcast(
episodes?.add(newEpisode) episodes?.add(newEpisode)
} }
} }
var index = 1
episodes?.forEach {
it.index = index
index++
}
} }
@JsonIgnore @JsonIgnore
override fun addAudioTrack(audioTrack:AudioTrack) { override fun addAudioTrack(audioTrack:AudioTrack) {
var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack) var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack)
episodes?.add(newEpisode) episodes?.add(newEpisode)
var index = 1
episodes?.forEach {
it.index = index
index++
}
} }
@JsonIgnore @JsonIgnore
override fun removeAudioTrack(localFileId:String) { override fun removeAudioTrack(localFileId:String) {
episodes?.removeIf { it.audioTrack?.localFileId == localFileId } episodes?.removeIf { it.audioTrack?.localFileId == localFileId }
var index = 1
episodes?.forEach {
it.index = index
index++
}
}
@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)
episodes?.add(newEpisode)
var index = 1
episodes?.forEach {
it.index = index
index++
}
} }
} }

View file

@ -20,6 +20,10 @@ class FolderScanner(var ctx: Context) {
return "local_" + DeviceManager.getBase64Id(mediaItemId) return "local_" + DeviceManager.getBase64Id(mediaItemId)
} }
enum class ItemScanResult {
ADDED, REMOVED, UPDATED, UPTODATE
}
// TODO: CLEAN this monster! Divide into bite-size methods // TODO: CLEAN this monster! Divide into bite-size methods
fun scanForMediaItems(localFolder:LocalFolder, forceAudioProbe:Boolean):FolderScanResult? { fun scanForMediaItems(localFolder:LocalFolder, forceAudioProbe:Boolean):FolderScanResult? {
FFmpegKitConfig.enableLogCallback { log -> FFmpegKitConfig.enableLogCallback { log ->
@ -57,16 +61,33 @@ class FolderScanner(var ctx: Context) {
fileFound != null fileFound != null
} }
var localLibraryItems = mutableListOf<LocalLibraryItem>()
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) }
var result = scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)
if (result == ItemScanResult.REMOVED) mediaItemsRemoved++
else if (result == ItemScanResult.UPDATED) mediaItemsUpdated++
else if (result == ItemScanResult.ADDED) mediaItemsAdded++
else mediaItemsUpToDate++
}
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
return if (mediaItemsAdded > 0 || mediaItemsUpdated > 0 || mediaItemsRemoved > 0) {
var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
} else {
Log.d(tag, "No Media Items to save")
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf())
}
}
fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
var itemFolderName = itemFolder.name ?: "" var itemFolderName = itemFolder.name ?: ""
var itemId = getLocalLibraryItemId(itemFolder.id) var itemId = getLocalLibraryItemId(itemFolder.id)
var itemContentUrl = itemFolder.uri.toString()
var existingItem = existingLocalLibraryItems.find { emi -> emi.id == itemId }
var existingLocalFiles = existingItem?.localFiles ?: mutableListOf() var existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf() var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
var isNewOrUpdated = existingItem == null var isNewOrUpdated = existingItem == null
@ -79,7 +100,7 @@ class FolderScanner(var ctx: Context) {
var coverAbsolutePath:String? = null var coverAbsolutePath:String? = null
var filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) var filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
var isPodcast = localFolder.mediaType == "podcast"
var 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
@ -172,36 +193,23 @@ class FolderScanner(var ctx: Context) {
if (existingItem != null && audioTracks.isEmpty()) { if (existingItem != null && audioTracks.isEmpty()) {
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item") Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id) DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
mediaItemsRemoved++ return ItemScanResult.REMOVED
} else if (existingItem != null && !isNewOrUpdated) { } else if (existingItem != null && !isNewOrUpdated) {
Log.d(tag, "Local library item ${existingItem.media.metadata.title} has no updates") Log.d(tag, "Local library item ${existingItem.media.metadata.title} has no updates")
mediaItemsUpToDate++ return ItemScanResult.UPTODATE
} else if (existingItem != null) { } else if (existingItem != null) {
Log.d(tag, "Updating local library item ${existingItem.media.metadata.title}") Log.d(tag, "Updating local library item ${existingItem.media.metadata.title}")
mediaItemsUpdated++
existingItem.updateFromScan(audioTracks,localFiles) existingItem.updateFromScan(audioTracks,localFiles)
localLibraryItems.add(existingItem) DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
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")
mediaItemsAdded++
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 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() var localLibraryItem = localMediaItem.getLocalLibraryItem()
localLibraryItems.add(localLibraryItem) DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
} return ItemScanResult.ADDED
}
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
return if (localLibraryItems.isNotEmpty()) {
DeviceManager.dbManager.saveLocalLibraryItems(localLibraryItems)
var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
} else { } else {
Log.d(tag, "No Media Items to save") return ItemScanResult.UPTODATE
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf())
} }
} }
@ -236,9 +244,18 @@ class FolderScanner(var ctx: Context) {
var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}") Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
var localLibraryItem = LocalLibraryItem("local_${downloadItem.id}", downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.id) var localLibraryItem:LocalLibraryItem? = null
if (downloadItem.mediaType == "book") {
localLibraryItem = LocalLibraryItem("local_${downloadItem.libraryItemId}", downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
} else {
// Lookup or create podcast local library item
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem("local_${downloadItem.libraryItemId}")
if (localLibraryItem == null) {
Log.d(tag, "Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
localLibraryItem = LocalLibraryItem("local_${downloadItem.libraryItemId}", downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media, mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId)
}
}
var localFiles:MutableList<LocalFile> = mutableListOf()
var audioTracks:MutableList<AudioTrack> = mutableListOf() var audioTracks:MutableList<AudioTrack> = mutableListOf()
filesFound.forEach { docFile -> filesFound.forEach { docFile ->
@ -246,13 +263,15 @@ class FolderScanner(var ctx: Context) {
itemPart.filename == docFile.name itemPart.filename == docFile.name
} }
if (itemPart == null) { if (itemPart == null) {
if (downloadItem.mediaType == "book") { // for books every download item should be a file found
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 var audioTrackFromServer = itemPart.audioTrack
var localFileId = DeviceManager.getBase64Id(docFile.id) 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()) var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localFiles.add(localFile) localLibraryItem.localFiles.add(localFile)
// TODO: Make asynchronous // TODO: Make asynchronous
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
@ -263,13 +282,19 @@ class FolderScanner(var ctx: Context) {
// 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) 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)
audioTracks.add(track) audioTracks.add(track)
// Add podcast episodes to library
itemPart.episode?.let { podcastEpisode ->
var podcast = localLibraryItem.media as Podcast
podcast.addEpisode(track, podcastEpisode)
}
} else { // Cover image } else { // Cover image
var localFileId = DeviceManager.getBase64Id(docFile.id) 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()) var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localFiles.add(localFile)
localLibraryItem.coverAbsolutePath = localFile.absolutePath localLibraryItem.coverAbsolutePath = localFile.absolutePath
localLibraryItem.coverContentUrl = localFile.contentUrl localLibraryItem.coverContentUrl = localFile.contentUrl
localLibraryItem.localFiles.add(localFile)
} }
} }
@ -278,6 +303,8 @@ class FolderScanner(var ctx: Context) {
return null return null
} }
// For books sort audio tracks then set
if (downloadItem.mediaType == "book") {
audioTracks.sortBy { it.index } audioTracks.sortBy { it.index }
var indexCheck = 1 var indexCheck = 1
@ -292,7 +319,7 @@ class FolderScanner(var ctx: Context) {
} }
localLibraryItem.media.setAudioTracks(audioTracks) localLibraryItem.media.setAudioTracks(audioTracks)
localLibraryItem.localFiles = localFiles }
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem) DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)

View file

@ -12,6 +12,7 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.CastManager import com.audiobookshelf.app.player.CastManager
import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.* import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin

View file

@ -43,6 +43,7 @@ class AbsDownloader : Plugin() {
val localFolderName: String, val localFolderName: String,
val localFolderId: String, val localFolderId: String,
val audioTrack: AudioTrack?, val audioTrack: AudioTrack?,
val episode:PodcastEpisode?,
var completed:Boolean, var completed:Boolean,
@JsonIgnore val uri: Uri, @JsonIgnore val uri: Uri,
@JsonIgnore val destinationUri: Uri, @JsonIgnore val destinationUri: Uri,
@ -62,6 +63,8 @@ class AbsDownloader : Plugin() {
data class DownloadItem( data class DownloadItem(
val id: String, val id: String,
val libraryItemId:String,
val episodeId:String?,
val serverConnectionConfigId:String, val serverConnectionConfigId:String,
val serverAddress:String, val serverAddress:String,
val serverUserId:String, val serverUserId:String,
@ -96,20 +99,40 @@ class AbsDownloader : Plugin() {
@PluginMethod @PluginMethod
fun downloadLibraryItem(call: PluginCall) { fun downloadLibraryItem(call: PluginCall) {
var libraryItemId = call.data.getString("libraryItemId").toString() var libraryItemId = call.data.getString("libraryItemId").toString()
var episodeId = call.data.getString("episodeId").toString()
var localFolderId = call.data.getString("localFolderId").toString() var localFolderId = call.data.getString("localFolderId").toString()
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId") Log.d(tag, "Download library item $libraryItemId to folder $localFolderId")
if (downloadQueue.find { it.id == libraryItemId } != null) { var downloadId = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId"
Log.d(tag, "Download already started for this library item $libraryItemId") if (downloadQueue.find { it.id == downloadId } != null) {
return call.resolve(JSObject("{\"error\":\"Download already started for this library item\"}")) Log.d(tag, "Download already started for this media entity $downloadId")
return call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}"))
} }
apiHandler.getLibraryItem(libraryItemId) { libraryItem -> apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
Log.d(tag, "Got library item from server ${libraryItem.id}") Log.d(tag, "Got library item from server ${libraryItem.id}")
var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId) var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
if (localFolder != null) { if (localFolder != null) {
startLibraryItemDownload(libraryItem, localFolder)
if (!episodeId.isNullOrEmpty() && libraryItem.mediaType != "podcast") {
Log.e(tag, "Library item is not a podcast but episode was requested")
call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
} else if (!episodeId.isNullOrEmpty()) {
var podcast = libraryItem.media as Podcast
var episode = podcast.episodes?.find { podcastEpisode ->
podcastEpisode.id == episodeId
}
if (episode == null) {
call.resolve(JSObject("{\"error\":\"Invalid podcast episode not found\"}"))
} else {
startLibraryItemDownload(libraryItem, localFolder, episode)
call.resolve() call.resolve()
}
} else {
startLibraryItemDownload(libraryItem, localFolder, null)
call.resolve()
}
} else { } else {
call.resolve(JSObject("{\"error\":\"Local Folder Not Found\"}")) call.resolve(JSObject("{\"error\":\"Local Folder Not Found\"}"))
} }
@ -139,13 +162,13 @@ class AbsDownloader : Plugin() {
return fileString return fileString
} }
fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder) { fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
if (libraryItem.mediaType == "book") { if (libraryItem.mediaType == "book") {
var bookTitle = libraryItem.media.metadata.title var bookTitle = libraryItem.media.metadata.title
var tracks = libraryItem.media.getAudioTracks() var 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")
var itemFolderPath = localFolder.absolutePath + "/" + bookTitle var itemFolderPath = localFolder.absolutePath + "/" + bookTitle
var downloadItem = DownloadItem(libraryItem.id, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf()) var downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null,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 ->
@ -162,7 +185,7 @@ class AbsDownloader : Plugin() {
var destinationUri = Uri.fromFile(destinationFile) var destinationUri = Uri.fromFile(destinationFile)
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}") var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")
Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri") Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri")
var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, audioTrack, false, downloadUri, destinationUri, null, 0) var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, audioTrack, null, false, downloadUri, destinationUri, null, 0)
downloadItem.downloadItemParts.add(downloadItemPart) downloadItem.downloadItemParts.add(downloadItemPart)
@ -185,7 +208,7 @@ class AbsDownloader : Plugin() {
var destinationUri = Uri.fromFile(destinationFile) var destinationUri = Uri.fromFile(destinationFile)
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}") var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}")
var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, null, false, downloadUri, destinationUri, null, 0) var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, null,null, false, downloadUri, destinationUri, null, 0)
downloadItem.downloadItemParts.add(downloadItemPart) downloadItem.downloadItemParts.add(downloadItemPart)
@ -204,7 +227,57 @@ class AbsDownloader : Plugin() {
DeviceManager.dbManager.saveDownloadItem(downloadItem) DeviceManager.dbManager.saveDownloadItem(downloadItem)
} }
} else { } else {
// TODO: Download podcast episode(s) // Podcast episode download
var podcastTitle = libraryItem.media.metadata.title
var audioTrack = episode?.audioTrack
Log.d(tag, "Starting podcast episode download")
var itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
var downloadItemId = "${libraryItem.id}-${episode?.id}"
var downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, 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 ?: "")
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack?.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
var destinationFile = File("$itemFolderPath/$destinationFilename")
if (destinationFile.exists()) {
Log.d(tag, "Audio file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
var destinationUri = Uri.fromFile(destinationFile)
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")
Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri")
var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, podcastTitle, serverPath, localFolder.name, localFolder.id, audioTrack, episode,false, downloadUri, destinationUri, null, 0)
downloadItem.downloadItemParts.add(downloadItemPart)
var dlRequest = downloadItemPart.getDownloadRequest()
var downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
var serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg"
var destinationFilename = "cover.jpg"
var destinationFile = File("$itemFolderPath/$destinationFilename")
if (destinationFile.exists()) {
Log.d(tag, "Podcast cover already exists - not downloading cover again")
} else {
var destinationUri = Uri.fromFile(destinationFile)
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}")
var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, podcastTitle, serverPath, localFolder.name, localFolder.id, null,null, false, downloadUri, destinationUri, null, 0)
downloadItem.downloadItemParts.add(downloadItemPart)
var dlRequest = downloadItemPart.getDownloadRequest()
var downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
}
}
downloadQueue.add(downloadItem)
startWatchingDownloads(downloadItem)
DeviceManager.dbManager.saveDownloadItem(downloadItem)
} }
} }

View file

@ -105,11 +105,9 @@ export default {
}, },
data() { data() {
return { return {
// Main
playbackSession: null, playbackSession: null,
// Others
showChapterModal: false, showChapterModal: false,
showCastBtn: true, showCastBtn: false,
showFullscreen: false, showFullscreen: false,
totalDuration: 0, totalDuration: 0,
currentPlaybackRate: 1, currentPlaybackRate: 1,
@ -493,6 +491,7 @@ export default {
onPlayingUpdate(data) { onPlayingUpdate(data) {
console.log('onPlayingUpdate', JSON.stringify(data)) console.log('onPlayingUpdate', JSON.stringify(data))
this.isPaused = !data.value this.isPaused = !data.value
this.$store.commit('setPlayerPlaying', !this.isPaused)
if (!this.isPaused) { if (!this.isPaused) {
this.startPlayInterval() this.startPlayInterval()
} else { } else {
@ -519,6 +518,8 @@ export default {
console.log('onPlaybackSession received', JSON.stringify(playbackSession)) console.log('onPlaybackSession received', JSON.stringify(playbackSession))
this.playbackSession = playbackSession this.playbackSession = playbackSession
this.$store.commit('setPlayerItem', this.playbackSession)
// Set track width // Set track width
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.track) { if (this.$refs.track) {
@ -530,6 +531,7 @@ export default {
}, },
onPlaybackClosed() { onPlaybackClosed() {
console.log('Received onPlaybackClosed evt') console.log('Received onPlaybackClosed evt')
this.$store.commit('setPlayerItem', null)
this.showFullscreen = false this.showFullscreen = false
this.playbackSession = null this.playbackSession = null
}, },

View file

@ -166,9 +166,12 @@ export default {
this.$refs.audioPlayer.terminateStream() this.$refs.audioPlayer.terminateStream()
} }
}, },
async playLibraryItem(libraryItemId) { async playLibraryItem(payload) {
var libraryItemId = payload.libraryItemId
var episodeId = payload.episodeId
console.log('Called playLibraryItem', libraryItemId) console.log('Called playLibraryItem', libraryItemId)
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, playWhenReady: true }) AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true })
.then((data) => { .then((data) => {
console.log('Library item play response', JSON.stringify(data)) console.log('Library item play response', JSON.stringify(data))
}) })
@ -176,6 +179,11 @@ export default {
console.error('Failed', error) console.error('Failed', error)
}) })
}, },
pauseItem() {
if (this.$refs.audioPlayer && !this.$refs.audioPlayer.isPaused) {
this.$refs.audioPlayer.pause()
}
},
onLocalMediaProgressUpdate(localMediaProgress) { onLocalMediaProgressUpdate(localMediaProgress) {
console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress)) console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress))
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress) this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
@ -191,6 +199,7 @@ export default {
this.setListeners() this.setListeners()
this.$eventBus.$on('play-item', this.playLibraryItem) this.$eventBus.$on('play-item', this.playLibraryItem)
this.$eventBus.$on('pause-item', this.pauseItem)
this.$eventBus.$on('close-stream', this.closeStreamOnly) this.$eventBus.$on('close-stream', this.closeStreamOnly)
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated }) this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
}, },
@ -207,6 +216,7 @@ export default {
// this.$server.socket.off('stream_reset', this.streamReset) // this.$server.socket.off('stream_reset', this.streamReset)
// } // }
this.$eventBus.$off('play-item', this.playLibraryItem) this.$eventBus.$off('play-item', this.playLibraryItem)
this.$eventBus.$off('pause-item', this.pauseItem)
this.$eventBus.$off('close-stream', this.closeStreamOnly) this.$eventBus.$off('close-stream', this.closeStreamOnly)
this.$store.commit('user/removeSettingsListener', 'streamContainer') this.$store.commit('user/removeSettingsListener', 'streamContainer')
} }

View file

@ -424,7 +424,7 @@ export default {
}, },
play() { play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus var eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('play-item', this.libraryItemId) eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
}, },
destroy() { destroy() {
// destroy the vue listeners, etc // destroy the vue listeners, etc

View file

@ -397,7 +397,7 @@ export default {
}, },
play() { play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus var eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('play-item', this.libraryItemId) eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
}, },
destroy() { destroy() {
// destroy the vue listeners, etc // destroy the vue listeners, etc

View file

@ -261,10 +261,15 @@ export default {
} }
}, },
async setUserAndConnection(user, userDefaultLibraryId) { async setUserAndConnection(user, userDefaultLibraryId) {
if (user) { if (!user) return
console.log('Successfully logged in', JSON.stringify(user)) console.log('Successfully logged in', JSON.stringify(user))
if (userDefaultLibraryId) { // Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId()
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
this.$store.commit('libraries/setCurrentLibrary', lastLibraryId)
} else if (userDefaultLibraryId) {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
} }
@ -278,7 +283,6 @@ export default {
this.$socket.connect(this.serverConfig.address, this.serverConfig.token) this.$socket.connect(this.serverConfig.address, this.serverConfig.token)
this.$router.replace('/bookshelf') this.$router.replace('/bookshelf')
}
}, },
async authenticateToken() { async authenticateToken() {
if (!this.networkConnected) return if (!this.networkConnected) return

View file

@ -55,6 +55,7 @@ export default {
this.show = false this.show = false
await this.$store.dispatch('libraries/fetch', lib.id) await this.$store.dispatch('libraries/fetch', lib.id)
this.$eventBus.$emit('library-changed', lib.id) this.$eventBus.$emit('library-changed', lib.id)
this.$localStore.setLastLibraryId(lib.id)
} }
}, },
mounted() {} mounted() {}

View file

@ -65,7 +65,7 @@ export default {
return this.book.numTracks return this.book.numTracks
}, },
isStreaming() { isStreaming() {
return this.$store.getters['getAudiobookIdStreaming'] === this.book.id return this.$store.getters['getIsItemStreaming'](this.book.id)
}, },
showPlayBtn() { showPlayBtn() {
return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks

View file

@ -0,0 +1,183 @@
<template>
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10">
<div v-if="episode" class="flex items-center h-24">
<!-- <div class="w-12 min-w-12 max-w-16 h-full">
<div class="flex h-full items-center justify-center">
<span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span>
</div>
</div> -->
<div class="flex-grow px-2">
<p class="text-sm font-semibold">
{{ title }}
</p>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">
{{ description }}
</p>
<div class="flex items-center pt-2">
<div class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click="playClick">
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
</div>
<span class="material-icons px-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75' : ''" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
<p v-if="publishedAt" class="px-4 text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
</div>
</div>
</div>
<div v-if="!userIsFinished" class="absolute bottom-0 left-0 h-0.5 bg-warning" :style="{ width: itemProgressPercent * 100 + '%' }" />
</div>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import { AbsDownloader } from '@/plugins/capacitor'
export default {
props: {
libraryItemId: String,
isLocal: Boolean,
episode: {
type: Object,
default: () => {}
}
},
data() {
return {
isProcessingReadUpdate: false
}
},
computed: {
mediaType() {
return 'podcast'
},
audioFile() {
return this.episode.audioFile
},
title() {
return this.episode.title || ''
},
description() {
if (this.episode.subtitle) return this.episode.subtitle
var desc = this.episode.description || ''
return desc
},
duration() {
return this.$secondsToTimestamp(this.episode.duration)
},
isStreaming() {
return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.episode.id)
},
streamIsPlaying() {
return this.$store.state.playerIsPlaying && this.isStreaming
},
itemProgress() {
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.episode.id)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
},
itemProgressPercent() {
return this.itemProgress ? this.itemProgress.progress : 0
},
userIsFinished() {
return this.itemProgress ? !!this.itemProgress.isFinished : false
},
timeRemaining() {
if (this.streamIsPlaying) return 'Playing'
if (!this.itemProgressPercent) return this.$elapsedPretty(this.episode.duration)
if (this.userIsFinished) return 'Finished'
var remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime)
return `${this.$elapsedPretty(remaining)} left`
},
publishedAt() {
return this.episode.publishedAt
},
downloadItem() {
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId, this.episode.id)
}
},
methods: {
selectFolder() {
this.$toast.error('Folder selector not implemented for podcasts yet')
},
downloadClick() {
if (this.downloadItem) return
this.download()
},
async download(selectedLocalFolder = null) {
var localFolder = selectedLocalFolder
if (!localFolder) {
var localFolders = (await this.$db.getLocalFolders()) || []
console.log('Local folders loaded', localFolders.length)
var foldersWithMediaType = localFolders.filter((lf) => {
console.log('Checking local folder', lf.mediaType)
return lf.mediaType == this.mediaType
})
console.log('Folders with media type', this.mediaType, foldersWithMediaType.length)
if (!foldersWithMediaType.length) {
// No local folders or no local folders with this media type
localFolder = await this.selectFolder()
} else if (foldersWithMediaType.length == 1) {
console.log('Only 1 local folder with this media type - auto select it')
localFolder = foldersWithMediaType[0]
} else {
console.log('Multiple folders with media type')
// this.showSelectLocalFolder = true
return
}
if (!localFolder) {
return this.$toast.error('Invalid download folder')
}
}
console.log('Local folder', JSON.stringify(localFolder))
var startDownloadMessage = `Start download for "${this.title}" to folder ${localFolder.name}?`
const { value } = await Dialog.confirm({
title: 'Confirm',
message: startDownloadMessage
})
if (value) {
this.startDownload(localFolder)
}
},
async startDownload(localFolder) {
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id, episodeId: this.episode.id })
if (downloadRes && downloadRes.error) {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download error', errorMsg)
this.$toast.error(errorMsg)
}
},
playClick() {
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
} else {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId,
episodeId: this.episode.id
})
}
},
toggleFinished() {
var updatePayload = {
isFinished: !this.userIsFinished
}
this.isProcessingReadUpdate = true
this.$axios
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
.then(() => {
this.isProcessingReadUpdate = false
this.$toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
.catch((error) => {
console.error('Failed', error)
this.isProcessingReadUpdate = false
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
}
},
mounted() {}
}
</script>

View file

@ -0,0 +1,25 @@
<template>
<div class="w-full">
<template v-for="episode in episodes">
<tables-podcast-episode-row :episode="episode" :library-item-id="libraryItemId" :key="episode.id" />
</template>
</div>
</template>
<script>
export default {
props: {
libraryItemId: String,
episodes: {
type: Array,
default: () => []
}
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,57 @@
<template>
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
<div class="w-5 h-5 text-white relative">
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
</svg>
</div>
</button>
</template>
<script>
export default {
props: {
isRead: Boolean,
disabled: Boolean,
borderless: Boolean
},
data() {
return {}
},
computed: {},
methods: {
clickBtn(e) {
if (this.disabled) {
e.preventDefault()
return
}
this.$emit('click')
e.stopPropagation()
}
},
mounted() {}
}
</script>
<style>
button.icon-btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
button.icon-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
button.icon-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
</style>

View file

@ -62,12 +62,14 @@ export default {
var update = { var update = {
id: data.id, id: data.id,
libraryItemId: data.libraryItemId,
partsRemaining, partsRemaining,
partsCompleted, partsCompleted,
totalParts: downloadItemParts.length, totalParts: downloadItemParts.length,
itemProgress itemProgress
} }
data.itemProgress = itemProgress data.itemProgress = itemProgress
data.episodes = downloadItemParts.filter((dip) => dip.episode).map((dip) => dip.episode)
console.log('Saving item update download payload', JSON.stringify(update)) console.log('Saving item update download payload', JSON.stringify(update))
this.$set(this.itemDownloadingMap, update.id, update) this.$set(this.itemDownloadingMap, update.id, update)

View file

@ -13,7 +13,6 @@
<script> <script>
import { AppUpdate } from '@robingenz/capacitor-app-update' import { AppUpdate } from '@robingenz/capacitor-app-update'
import { AbsFileSystem } from '@/plugins/capacitor'
export default { export default {
data() { data() {
@ -56,7 +55,7 @@ export default {
}, },
computed: { computed: {
playerIsOpen() { playerIsOpen() {
return this.$store.getters['playerIsOpen'] return this.$store.state.playerLibraryItemId
}, },
routeName() { routeName() {
return this.$route.name return this.$route.name
@ -110,58 +109,6 @@ export default {
}, 5000) }, 5000)
} }
}, },
async searchFolder(downloadFolder) {
try {
var response = await AbsFileSystem.searchFolder({ folderUrl: downloadFolder.uri })
var searchResults = response
searchResults.folders = JSON.parse(searchResults.folders)
searchResults.files = JSON.parse(searchResults.files)
console.log('Search folders results length', searchResults.folders.length)
searchResults.folders = searchResults.folders.map((sr) => {
if (sr.files) {
sr.files = JSON.parse(sr.files)
}
return sr
})
return searchResults
} catch (error) {
console.error('Failed', error)
this.$toast.error('Failed to search downloads folder')
return {}
}
},
// async syncDownloads(downloads, downloadFolder) {
// console.log('Syncing downloads ' + downloads.length)
// var mediaScanResults = await this.searchFolder(downloadFolder)
// this.$store.commit('downloads/setMediaScanResults', mediaScanResults)
// // Filter out media folders without any audio files
// var mediaFolders = mediaScanResults.folders.filter((sr) => {
// if (!sr.files) return false
// var audioFiles = sr.files.filter((mf) => !!mf.isAudio)
// return audioFiles.length
// })
// downloads.forEach((download) => {
// var mediaFolder = mediaFolders.find((mf) => mf.name === download.folderName)
// if (mediaFolder) {
// console.log('Found download ' + download.folderName)
// if (download.isMissing) {
// download.isMissing = false
// this.$store.commit('downloads/addUpdateDownload', download)
// }
// } else {
// console.error('Download not found ' + download.folderName)
// if (!download.isMissing) {
// download.isMissing = true
// this.$store.commit('downloads/addUpdateDownload', download)
// }
// }
// })
// },
async loadSavedSettings() { async loadSavedSettings() {
var userSavedServerSettings = await this.$localStore.getServerSettings() var userSavedServerSettings = await this.$localStore.getServerSettings()
if (userSavedServerSettings) { if (userSavedServerSettings) {
@ -209,7 +156,12 @@ export default {
} }
const { user, userDefaultLibraryId } = authRes const { user, userDefaultLibraryId } = authRes
if (userDefaultLibraryId) {
// Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId()
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
this.$store.commit('libraries/setCurrentLibrary', lastLibraryId)
} else if (userDefaultLibraryId) {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
} }
var serverConnectionConfig = await this.$db.setServerConnectionConfig(serverConfig) var serverConnectionConfig = await this.$db.setServerConnectionConfig(serverConfig)
@ -247,7 +199,7 @@ export default {
} }
this.inittingLibraries = true this.inittingLibraries = true
await this.$store.dispatch('libraries/load') await this.$store.dispatch('libraries/load')
console.log(`[default] initLibraries loaded`) console.log(`[default] initLibraries loaded ${this.currentLibraryId}`)
this.$eventBus.$emit('library-changed') this.$eventBus.$emit('library-changed')
this.$store.dispatch('libraries/fetch', this.currentLibraryId) this.$store.dispatch('libraries/fetch', this.currentLibraryId)
this.inittingLibraries = false this.inittingLibraries = false
@ -261,7 +213,7 @@ export default {
console.log('[default] Calling syncLocalMediaProgress') console.log('[default] Calling syncLocalMediaProgress')
var response = await this.$db.syncLocalMediaProgressWithServer() var response = await this.$db.syncLocalMediaProgressWithServer()
if (!response) { if (!response) {
this.$toast.error('Failed to sync local media with server') if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server')
return return
} }
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
@ -276,20 +228,12 @@ export default {
} }
}, },
userUpdated(user) { userUpdated(user) {
console.log('User updated', user)
if (this.user && this.user.id == user.id) { if (this.user && this.user.id == user.id) {
this.$store.commit('user/setUser', user) this.$store.commit('user/setUser', user)
} }
} }
}, },
async mounted() { async mounted() {
// this.$server.on('logout', this.userLoggedOut)
// this.$server.on('connected', this.connected)
// this.$server.on('connectionFailed', this.socketConnectionFailed)
// this.$server.on('initialStream', this.initialStream)
// this.$server.on('show_error_toast', this.showErrorToast)
// this.$server.on('show_success_toast', this.showSuccessToast)
this.$socket.on('connection-update', this.socketConnectionUpdate) this.$socket.on('connection-update', this.socketConnectionUpdate)
this.$socket.on('initialized', this.socketInit) this.$socket.on('initialized', this.socketInit)
this.$socket.on('user_updated', this.userUpdated) this.$socket.on('user_updated', this.userUpdated)

View file

@ -75,7 +75,7 @@ export default {
}) })
}, },
streaming() { streaming() {
return !!this.playableBooks.find((b) => b.id === this.$store.getters['getAudiobookIdStreaming']) return !!this.playableBooks.find((b) => this.$store.getters['getIsItemStreaming'](b.id))
}, },
showPlayButton() { showPlayButton() {
return this.playableBooks.length return this.playableBooks.length
@ -88,7 +88,7 @@ export default {
return !prog || !prog.isFinished return !prog || !prog.isFinished
}) })
if (nextBookNotRead) { if (nextBookNotRead) {
this.$eventBus.$emit('play-item', nextBookNotRead.id) this.$eventBus.$emit('play-item', { libraryItemId: nextBookNotRead.id })
} }
} }
}, },

View file

@ -45,6 +45,9 @@ export default {
} }
}, },
mounted() { mounted() {
// Reset data on logouts
this.$store.commit('libraries/reset')
this.$store.commit('setIsFirstLoad', true)
this.init() this.init()
} }
} }

View file

@ -4,7 +4,7 @@
<div class="w-32"> <div class="w-32">
<div class="relative"> <div class="relative">
<covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div>
</div> </div>
<div class="flex my-4"> <div class="flex my-4">
<p v-if="numTracks" class="text-sm">{{ numTracks }} Tracks</p> <p v-if="numTracks" class="text-sm">{{ numTracks }} Tracks</p>
@ -19,7 +19,7 @@
<span class="px-4">{{ $bytesPretty(size) }}</span> <span class="px-4">{{ $bytesPretty(size) }}</span>
</p> </p>
<div v-if="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>
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p> <p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick"> <div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
@ -61,6 +61,8 @@
<p>{{ description }}</p> <p>{{ description }}</p>
</div> </div>
<tables-podcast-episodes-table v-if="isPodcast" :library-item-id="libraryItemId" :episodes="episodes" />
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" /> <modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
</div> </div>
</template> </template>
@ -133,6 +135,9 @@ export default {
mediaType() { mediaType() {
return this.libraryItem.mediaType return this.libraryItem.mediaType
}, },
isPodcast() {
return this.mediaType == 'podcast'
},
media() { media() {
return this.libraryItem.media || {} return this.libraryItem.media || {}
}, },
@ -143,6 +148,7 @@ export default {
return this.mediaMetadata.title return this.mediaMetadata.title
}, },
author() { author() {
if (this.isPodcast) return this.mediaMetadata.author
return this.mediaMetadata.authorName return this.mediaMetadata.authorName
}, },
description() { description() {
@ -185,10 +191,10 @@ export default {
return this.userItemProgress ? this.userItemProgress.finishedAt : 0 return this.userItemProgress ? this.userItemProgress.finishedAt : 0
}, },
isStreaming() { isStreaming() {
return this.$store.getters['isAudiobookStreaming'](this.libraryItemId) return this.isPlaying && !this.$store.state.playerIsLocal
}, },
isPlaying() { isPlaying() {
return this.$store.getters['isAudiobookPlaying'](this.libraryItemId) return this.$store.getters['getIsItemStreaming'](this.libraryItemId)
}, },
numTracks() { numTracks() {
if (!this.media.tracks) return 0 if (!this.media.tracks) return 0
@ -219,8 +225,8 @@ export default {
downloadItem() { downloadItem() {
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId) return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
}, },
downloadItems() { episodes() {
return this.$store.state.globals.downloadItems || [] return this.media.episodes || []
} }
}, },
methods: { methods: {
@ -229,8 +235,8 @@ export default {
}, },
playClick() { playClick() {
// Todo: Allow playing local or streaming // Todo: Allow playing local or streaming
if (this.hasLocal) return this.$eventBus.$emit('play-item', this.localLibraryItem.id) if (this.hasLocal) return this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id })
this.$eventBus.$emit('play-item', this.libraryItem.id) this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
}, },
async clearProgressClick() { async clearProgressClick() {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({

View file

@ -23,7 +23,7 @@
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="text-sm">{{ mediaItem.media.metadata.title }}</p> <p class="text-sm">{{ mediaItem.media.metadata.title }}</p>
<p v-if="mediaItem.mediaType == 'book'" class="text-xs text-gray-300">{{ mediaItem.media.tracks.length }} Track{{ mediaItem.media.tracks.length == 1 ? '' : 's' }}</p> <p v-if="mediaItem.mediaType == 'book'" class="text-xs text-gray-300">{{ mediaItem.media.tracks.length }} Track{{ mediaItem.media.tracks.length == 1 ? '' : 's' }}</p>
<p v-else-if="mediaItem.mediaType == 'podcast'" class="text-xs text-gray-300">{{ mediaItem.media.episodes.length }} Episode{{ mediaItem.media.tracks.length == 1 ? '' : 's' }}</p> <p v-else-if="mediaItem.mediaType == 'podcast'" class="text-xs text-gray-300">{{ mediaItem.media.episodes.length }} Episode{{ mediaItem.media.episodes.length == 1 ? '' : 's' }}</p>
</div> </div>
<div class="w-12 h-12 flex items-center justify-center"> <div class="w-12 h-12 flex items-center justify-center">
<span class="material-icons text-xl text-gray-300">arrow_right</span> <span class="material-icons text-xl text-gray-300">arrow_right</span>
@ -78,7 +78,7 @@ export default {
text: 'Remove', text: 'Remove',
value: 'remove' value: 'remove'
} }
].filter((i) => !i.value == 'rescan' || this.localLibraryItems.length) // Filter out rescan if there are no local library items ].filter((i) => i.value != 'rescan' || this.localLibraryItems.length) // Filter out rescan if there are no local library items
} }
}, },
methods: { methods: {
@ -110,7 +110,7 @@ export default {
} }
}, },
play(mediaItem) { play(mediaItem) {
this.$eventBus.$emit('play-item', mediaItem.id) this.$eventBus.$emit('play-item', { libraryItemId: mediaItem.id })
}, },
async scanFolder(forceAudioProbe = false) { async scanFolder(forceAudioProbe = false) {
this.isScanning = true this.isScanning = true
@ -150,11 +150,13 @@ export default {
var items = (await this.$db.getLocalLibraryItemsInFolder(this.folderId)) || [] var items = (await this.$db.getLocalLibraryItemsInFolder(this.folderId)) || []
console.log('Init folder', this.folderId, items) console.log('Init folder', this.folderId, items)
this.localLibraryItems = items.map((lmi) => { this.localLibraryItems = items.map((lmi) => {
console.log('Local library item', JSON.stringify(lmi))
return { return {
...lmi, ...lmi,
coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null
} }
}) })
if (this.shouldScan) { if (this.shouldScan) {
this.scanFolder() this.scanFolder()
} }

View file

@ -1,32 +1,33 @@
<template> <template>
<div class="w-full h-full py-6 px-4"> <div class="w-full h-full py-6 px-2">
<div v-if="localLibraryItem" class="w-full h-full"> <div v-if="localLibraryItem" class="w-full h-full">
<div class="flex items-center mb-2"> <div class="px-2 flex items-center mb-2">
<p class="text-base font-book font-semibold">{{ mediaMetadata.title }}</p> <p class="text-base font-book font-semibold">{{ mediaMetadata.title }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<button v-if="audioTracks.length" class="shadow-sm text-accent flex items-center justify-center rounded-full mx-2" @click.stop="play"> <button v-if="audioTracks.length && !isPodcast" class="shadow-sm text-accent flex items-center justify-center rounded-full mx-2" @click.stop="play">
<span class="material-icons" style="font-size: 2rem">play_arrow</span> <span class="material-icons" style="font-size: 2rem">play_arrow</span>
</button> </button>
<span class="material-icons" @click="showItemDialog">more_vert</span> <span class="material-icons" @click="showItemDialog">more_vert</span>
</div> </div>
<p class="text-sm mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p> <p class="px-2 text-sm mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
<p class="mb-4 text-xs text-gray-400">{{ libraryItemId ? 'Linked to item on server ' + liServerAddress : 'Not linked to server item' }}</p> <p class="px-2 mb-4 text-xs text-gray-400">{{ libraryItemId ? 'Linked to item on server ' + liServerAddress : 'Not linked to server item' }}</p>
<div v-if="isScanning" class="w-full text-center p-4"> <div v-if="isScanning" class="w-full text-center p-4">
<p>Scanning...</p> <p>Scanning...</p>
</div> </div>
<div v-else class="w-full media-item-container overflow-y-auto"> <div v-else class="w-full media-item-container overflow-y-auto">
<div v-if="!isPodcast" class="w-full">
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p> <p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
<template v-for="track in audioTracks"> <template v-for="track in audioTracks">
<div :key="track.localFileId" class="flex items-center my-1"> <div :key="track.localFileId" class="flex items-center my-1">
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px"> <div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px">
<p class="font-mono font-bold text-xl">{{ track.index }}</p> <p class="font-mono font-bold text-xl">{{ track.index }}</p>
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="text-sm">{{ track.title }}</p> <p class="text-xs">{{ track.title }}</p>
</div> </div>
<div class="w-20 text-center text-gray-300" style="min-width: 80px"> <div class="w-20 text-center text-gray-300" style="min-width: 80px">
<p class="text-xs">{{ track.mimeType }}</p> <p class="text-xs">{{ track.mimeType }}</p>
@ -37,6 +38,27 @@
</div> </div>
</div> </div>
</template> </template>
</div>
<div v-else class="w-full">
<p class="text-base mb-2">Episodes ({{ audioTracks.length }})</p>
<template v-for="episode in audioTracks">
<div :key="episode.id" class="flex items-center my-1">
<div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px">
<p class="font-mono font-bold text-xl">{{ episode.index }}</p>
</div>
<div class="flex-grow px-2">
<p class="text-xs">{{ episode.title }}</p>
</div>
<div class="w-20 text-center text-gray-300" style="min-width: 80px">
<p class="text-xs">{{ episode.audioTrack.mimeType }}</p>
<p class="text-sm">{{ $elapsedPretty(episode.audioTrack.duration) }}</p>
</div>
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
<span class="material-icons" @click="showTrackDialog(episode)">more_vert</span>
</div>
</div>
</template>
</div>
<p v-if="otherFiles.length" class="text-lg mb-2 pt-8">Other Files</p> <p v-if="otherFiles.length" class="text-lg mb-2 pt-8">Other Files</p>
<template v-for="file in otherFiles"> <template v-for="file in otherFiles">
@ -56,7 +78,7 @@
</template> </template>
</div> </div>
</div> </div>
<div v-else class="w-full h-full"> <div v-else class="px-2 w-full h-full">
<p class="text-lg text-center px-8">{{ failed ? 'Failed to get local library item ' + localLibraryItemId : 'Loading..' }}</p> <p class="text-lg text-center px-8">{{ failed ? 'Failed to get local library item ' + localLibraryItemId : 'Loading..' }}</p>
</div> </div>
@ -84,7 +106,8 @@ export default {
folder: null, folder: null,
isScanning: false, isScanning: false,
showDialog: false, showDialog: false,
selectedAudioTrack: null selectedAudioTrack: null,
selectedEpisode: null
} }
}, },
computed: { computed: {
@ -100,6 +123,7 @@ export default {
return [] return []
} }
return this.localFiles.filter((lf) => { return this.localFiles.filter((lf) => {
if (this.isPodcast) return !this.audioTracks.find((episode) => episode.audioTrack.localFileId == lf.id)
return !this.audioTracks.find((at) => at.localFileId == lf.id) return !this.audioTracks.find((at) => at.localFileId == lf.id)
}) })
}, },
@ -109,6 +133,9 @@ export default {
mediaType() { mediaType() {
return this.localLibraryItem ? this.localLibraryItem.mediaType : null return this.localLibraryItem ? this.localLibraryItem.mediaType : null
}, },
isPodcast() {
return this.mediaType == 'podcast'
},
libraryItemId() { libraryItemId() {
return this.localLibraryItem ? this.localLibraryItem.libraryItemId : null return this.localLibraryItem ? this.localLibraryItem.libraryItemId : null
}, },
@ -165,11 +192,17 @@ export default {
this.showDialog = true this.showDialog = true
}, },
showTrackDialog(track) { showTrackDialog(track) {
if (this.isPodcast) {
this.selectedAudioTrack = null
this.selectedEpisode = track
} else {
this.selectedEpisode = null
this.selectedAudioTrack = track this.selectedAudioTrack = track
}
this.showDialog = true this.showDialog = true
}, },
play() { play() {
this.$eventBus.$emit('play-item', this.localLibraryItemId) this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItemId })
}, },
getCapImageSrc(contentUrl) { getCapImageSrc(contentUrl) {
return Capacitor.convertFileSrc(contentUrl) return Capacitor.convertFileSrc(contentUrl)
@ -185,13 +218,34 @@ export default {
} else if (action == 'delete') { } else if (action == 'delete') {
this.deleteItem() this.deleteItem()
} else if (action == 'track-delete') { } else if (action == 'track-delete') {
this.deleteTrack() if (this.isPodcast) this.deleteEpisode()
else this.deleteTrack()
} }
this.showDialog = false this.showDialog = false
}, },
getLocalFileForTrack(localFileId) { getLocalFileForTrack(localFileId) {
return this.localFiles.find((lf) => lf.id == localFileId) return this.localFiles.find((lf) => lf.id == localFileId)
}, },
async deleteEpisode() {
if (!this.selectedEpisode) return
var localFile = this.getLocalFileForTrack(this.selectedEpisode.audioTrack.localFileId)
if (!localFile) {
this.$toast.error('Audio track does not have matching local file..')
return
}
var trackPath = localFile ? localFile.basePath : this.selectedEpisode.title
const { value } = await Dialog.confirm({
title: 'Confirm',
message: `Warning! This will delete the audio file "${trackPath}" from your file system. Are you sure?`
})
if (value) {
var res = await AbsFileSystem.deleteTrackFromItem({ id: this.localLibraryItem.id, trackLocalFileId: localFile.id, trackContentUrl: this.selectedEpisode.audioTrack.contentUrl })
if (res && res.id) {
this.$toast.success('Deleted track successfully')
this.localLibraryItem = res
} else this.$toast.error('Failed to delete')
}
},
async deleteTrack() { async deleteTrack() {
if (!this.selectedAudioTrack) { if (!this.selectedAudioTrack) {
return return

View file

@ -63,7 +63,6 @@ class LocalStorage {
async setBookshelfListView(useIt) { async setBookshelfListView(useIt) {
try { try {
await Storage.set({ key: 'bookshelfListView', value: useIt ? '1' : '0' }) await Storage.set({ key: 'bookshelfListView', value: useIt ? '1' : '0' })
this.getBookshelfListView()
} catch (error) { } catch (error) {
console.error('[LocalStorage] Failed to set bookshelf list view', error) console.error('[LocalStorage] Failed to set bookshelf list view', error)
} }
@ -78,6 +77,25 @@ class LocalStorage {
return false return false
} }
} }
async setLastLibraryId(libraryId) {
try {
await Storage.set({ key: 'lastLibraryId', value: libraryId })
console.log('[LocalStorage] Set Last Library Id', libraryId)
} catch (error) {
console.error('[LocalStorage] Failed to set current library', error)
}
}
async getLastLibraryId() {
try {
var obj = await Storage.get({ key: 'lastLibraryId' }) || {}
return obj.value || null
} catch (error) {
console.error('[LocalStorage] Failed to get last library id', error)
return false
}
}
} }

View file

@ -1,4 +1,3 @@
import Vue from 'vue'
import { io } from 'socket.io-client' import { io } from 'socket.io-client'
import EventEmitter from 'events' import EventEmitter from 'events'
@ -58,6 +57,7 @@ class ServerSocket extends EventEmitter {
this.connected = true this.connected = true
this.$store.commit('setSocketConnected', true) this.$store.commit('setSocketConnected', true)
this.emit('connection-update', true) this.emit('connection-update', true)
this.socket.emit('auth', this.token) // Required to connect a user with their socket
} }
onDisconnect(reason) { onDisconnect(reason) {

View file

@ -6,8 +6,11 @@ export const state = () => ({
}) })
export const getters = { export const getters = {
getDownloadItem: state => libraryItemId => { getDownloadItem: state => (libraryItemId, episodeId = null) => {
return state.itemDownloads.find(i => i.id == libraryItemId) return state.itemDownloads.find(i => {
if (episodeId && !i.episodes.some(e => e.id == episodeId)) return false
return i.id == libraryItemId
})
}, },
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => { getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
if (!libraryItem) return placeholder if (!libraryItem) return placeholder

View file

@ -1,16 +1,14 @@
import Vue from 'vue'
import { Network } from '@capacitor/network' import { Network } from '@capacitor/network'
export const state = () => ({ export const state = () => ({
streamAudiobook: null, playerLibraryItemId: null,
playingDownload: null, playerEpisodeId: null,
playOnLoad: false, playerIsLocal: false,
serverUrl: null, playerIsPlaying: false,
appUpdateInfo: null, appUpdateInfo: null,
socketConnected: false, socketConnected: false,
networkConnected: false, networkConnected: false,
networkConnectionType: null, networkConnectionType: null,
streamListener: null,
isFirstLoad: true, isFirstLoad: true,
hasStoragePermission: false, hasStoragePermission: false,
selectedBook: null, selectedBook: null,
@ -22,16 +20,13 @@ export const state = () => ({
export const getters = { export const getters = {
playerIsOpen: (state) => { playerIsOpen: (state) => {
return state.streamAudiobook || state.playingDownload return state.streamAudiobook
}, },
isAudiobookStreaming: (state) => id => { getIsItemStreaming: state => libraryItemId => {
return (state.streamAudiobook && state.streamAudiobook.id === id) return state.playerLibraryItemId == libraryItemId
}, },
isAudiobookPlaying: (state) => id => { getIsEpisodeStreaming: state => (libraryItemId, episodeId) => {
return (state.playingDownload && state.playingDownload.id === id) || (state.streamAudiobook && state.streamAudiobook.id === id) return state.playerLibraryItemId == libraryItemId && state.playerEpisodeId == episodeId
},
getAudiobookIdStreaming: state => {
return state.streamAudiobook ? state.streamAudiobook.id : null
}, },
getServerSetting: state => key => { getServerSetting: state => key => {
if (!state.serverSettings) return null if (!state.serverSettings) return null
@ -61,6 +56,14 @@ export const actions = {
} }
export const mutations = { export const mutations = {
setPlayerItem(state, playbackSession) {
state.playerLibraryItemId = playbackSession ? playbackSession.libraryItemId || null : null
state.playerEpisodeId = playbackSession ? playbackSession.episodeId || null : null
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
},
setPlayerPlaying(state, val) {
state.playerIsPlaying = val
},
setHasStoragePermission(state, val) { setHasStoragePermission(state, val) {
state.hasStoragePermission = val state.hasStoragePermission = val
}, },
@ -70,36 +73,6 @@ export const mutations = {
setAppUpdateInfo(state, info) { setAppUpdateInfo(state, info) {
state.appUpdateInfo = info state.appUpdateInfo = info
}, },
closeStream(state, audiobookId) {
if (state.streamAudiobook && state.streamAudiobook.id !== audiobookId) {
return
}
state.streamAudiobook = null
},
setPlayOnLoad(state, val) {
state.playOnLoad = val
},
setStreamAudiobook(state, audiobook) {
if (audiobook) {
state.playingDownload = null
}
Vue.set(state, 'streamAudiobook', audiobook)
if (state.streamListener) {
state.streamListener('stream', audiobook)
}
},
setPlayingDownload(state, download) {
if (download) {
state.streamAudiobook = null
}
Vue.set(state, 'playingDownload', download)
if (state.streamListener) {
state.streamListener('download', download)
}
},
setServerUrl(state, url) {
state.serverUrl = url
},
setSocketConnected(state, val) { setSocketConnected(state, val) {
state.socketConnected = val state.socketConnected = val
}, },

View file

@ -1,11 +1,8 @@
export const state = () => ({ export const state = () => ({
libraries: [], libraries: [],
lastLoad: 0, lastLoad: 0,
listeners: [],
currentLibraryId: '', currentLibraryId: '',
showModal: false, showModal: false,
folders: [],
folderLastUpdate: 0,
issues: 0, issues: 0,
filterData: null filterData: null
}) })
@ -66,13 +63,13 @@ export const actions = {
return this.$axios return this.$axios
.$get(`/api/libraries`) .$get(`/api/libraries`)
.then((data) => { .then((data) => {
// Set current library // Set current library if not already set or was not returned in results
if (data.length) { if (data.length && (!state.currentLibraryId || !data.find(li => li.id == state.currentLibraryId))) {
commit('setCurrentLibrary', data[0].id) commit('setCurrentLibrary', data[0].id)
} }
commit('set', data) commit('set', data)
commit('setLastLoad') commit('setLastLoad', Date.now())
return true return true
}) })
.catch((error) => { .catch((error) => {
@ -85,27 +82,21 @@ export const actions = {
} }
export const mutations = { export const mutations = {
setFolders(state, folders) {
state.folders = folders
},
setFoldersLastUpdate(state) {
state.folderLastUpdate = Date.now()
},
setShowModal(state, val) { setShowModal(state, val) {
state.showModal = val state.showModal = val
}, },
setLastLoad(state) { setLastLoad(state, val) {
state.lastLoad = Date.now() state.lastLoad = val
},
reset(state) {
state.lastLoad = 0
state.libraries = []
}, },
setCurrentLibrary(state, val) { setCurrentLibrary(state, val) {
state.currentLibraryId = val state.currentLibraryId = val
}, },
set(state, libraries) { set(state, libraries) {
console.log('set libraries', libraries)
state.libraries = libraries state.libraries = libraries
state.listeners.forEach((listener) => {
listener.meth()
})
}, },
addUpdate(state, library) { addUpdate(state, library) {
var index = state.libraries.findIndex(a => a.id === library.id) var index = state.libraries.findIndex(a => a.id === library.id)
@ -114,25 +105,9 @@ export const mutations = {
} else { } else {
state.libraries.push(library) state.libraries.push(library)
} }
state.listeners.forEach((listener) => {
listener.meth()
})
}, },
remove(state, library) { remove(state, library) {
state.libraries = state.libraries.filter(a => a.id !== library.id) state.libraries = state.libraries.filter(a => a.id !== library.id)
state.listeners.forEach((listener) => {
listener.meth()
})
},
addListener(state, listener) {
var index = state.listeners.findIndex(l => l.id === listener.id)
if (index >= 0) state.listeners.splice(index, 1, listener)
else state.listeners.push(listener)
},
removeListener(state, listenerId) {
state.listeners = state.listeners.filter(l => l.id !== listenerId)
}, },
setLibraryIssues(state, val) { setLibraryIssues(state, val) {
state.issues = val state.issues = val