mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-06-26 07:08:45 +02:00
Add podcast pages, android download podcast, scan podcast folders
This commit is contained in:
parent
ef65b4c278
commit
c94e57f55e
26 changed files with 789 additions and 397 deletions
|
@ -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++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,146 +61,21 @@ 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 itemFolderName = itemFolder.name ?: ""
|
var result = scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)
|
||||||
var itemId = getLocalLibraryItemId(itemFolder.id)
|
|
||||||
var itemContentUrl = itemFolder.uri.toString()
|
|
||||||
|
|
||||||
var existingItem = existingLocalLibraryItems.find { emi -> emi.id == itemId }
|
if (result == ItemScanResult.REMOVED) mediaItemsRemoved++
|
||||||
var existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
|
else if (result == ItemScanResult.UPDATED) mediaItemsUpdated++
|
||||||
var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
|
else if (result == ItemScanResult.ADDED) mediaItemsAdded++
|
||||||
var isNewOrUpdated = existingItem == null
|
else mediaItemsUpToDate++
|
||||||
|
|
||||||
var audioTracks = mutableListOf<AudioTrack>()
|
|
||||||
var localFiles = mutableListOf<LocalFile>()
|
|
||||||
var index = 1
|
|
||||||
var startOffset = 0.0
|
|
||||||
var coverContentUrl:String? = null
|
|
||||||
var coverAbsolutePath:String? = null
|
|
||||||
|
|
||||||
var filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
|
||||||
|
|
||||||
|
|
||||||
var existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
|
||||||
}
|
|
||||||
if (existingLocalFilesRemoved.isNotEmpty()) {
|
|
||||||
Log.d(tag, "${existingLocalFilesRemoved.size} Local files were removed from local media item ${existingItem?.media?.metadata?.title}")
|
|
||||||
isNewOrUpdated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
filesInFolder.forEach { file ->
|
|
||||||
var mimeType = file?.mimeType ?: ""
|
|
||||||
var filename = file?.name ?: ""
|
|
||||||
var isAudio = mimeType.startsWith("audio")
|
|
||||||
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
|
||||||
|
|
||||||
var 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())
|
|
||||||
localFiles.add(localFile)
|
|
||||||
|
|
||||||
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
|
||||||
|
|
||||||
if (isAudio) {
|
|
||||||
var audioTrackToAdd:AudioTrack? = null
|
|
||||||
|
|
||||||
var existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
|
||||||
if (existingAudioTrack != null) { // Update existing audio track
|
|
||||||
if (existingAudioTrack.index != index) {
|
|
||||||
Log.d(tag, "Updating Audio track index from ${existingAudioTrack.index} to $index")
|
|
||||||
existingAudioTrack.index = index
|
|
||||||
isNewOrUpdated = true
|
|
||||||
}
|
|
||||||
if (existingAudioTrack.startOffset != startOffset) {
|
|
||||||
Log.d(tag, "Updating Audio track startOffset ${existingAudioTrack.startOffset} to $startOffset")
|
|
||||||
existingAudioTrack.startOffset = startOffset
|
|
||||||
isNewOrUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingAudioTrack == null || forceAudioProbe) {
|
|
||||||
Log.d(tag, "Scanning Audio File Path ${localFile.absolutePath}")
|
|
||||||
|
|
||||||
// TODO: Make asynchronous
|
|
||||||
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
|
||||||
|
|
||||||
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
|
||||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
|
||||||
|
|
||||||
if (existingAudioTrack != null) {
|
|
||||||
// Update audio probe data on existing audio track
|
|
||||||
existingAudioTrack.audioProbeResult = audioProbeResult
|
|
||||||
audioTrackToAdd = existingAudioTrack
|
|
||||||
} else {
|
|
||||||
// Create new audio track
|
|
||||||
var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, null, true, localFileId, audioProbeResult, null)
|
|
||||||
audioTrackToAdd = track
|
|
||||||
}
|
|
||||||
|
|
||||||
startOffset += audioProbeResult.duration
|
|
||||||
isNewOrUpdated = true
|
|
||||||
} else {
|
|
||||||
audioTrackToAdd = existingAudioTrack
|
|
||||||
}
|
|
||||||
|
|
||||||
startOffset += audioTrackToAdd.duration
|
|
||||||
index++
|
|
||||||
audioTracks.add(audioTrackToAdd)
|
|
||||||
} else {
|
|
||||||
var existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
|
||||||
|
|
||||||
if (existingLocalFile == null) {
|
|
||||||
isNewOrUpdated = true
|
|
||||||
}
|
|
||||||
if (existingItem != null && existingItem.coverContentUrl == null) {
|
|
||||||
// Existing media item did not have a cover - cover found on scan
|
|
||||||
isNewOrUpdated = true
|
|
||||||
existingItem.coverAbsolutePath = localFile.absolutePath
|
|
||||||
existingItem.coverContentUrl = localFile.contentUrl
|
|
||||||
existingItem.media.coverPath = localFile.absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// First image file use as cover path
|
|
||||||
if (coverContentUrl == null) {
|
|
||||||
coverContentUrl = localFile.contentUrl
|
|
||||||
coverAbsolutePath = localFile.absolutePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingItem != null && audioTracks.isEmpty()) {
|
|
||||||
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
|
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
|
|
||||||
mediaItemsRemoved++
|
|
||||||
} else if (existingItem != null && !isNewOrUpdated) {
|
|
||||||
Log.d(tag, "Local library item ${existingItem.media.metadata.title} has no updates")
|
|
||||||
mediaItemsUpToDate++
|
|
||||||
} else if (existingItem != null) {
|
|
||||||
Log.d(tag, "Updating local library item ${existingItem.media.metadata.title}")
|
|
||||||
mediaItemsUpdated++
|
|
||||||
|
|
||||||
existingItem.updateFromScan(audioTracks,localFiles)
|
|
||||||
localLibraryItems.add(existingItem)
|
|
||||||
} else if (audioTracks.isNotEmpty()) {
|
|
||||||
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 localLibraryItem = localMediaItem.getLocalLibraryItem()
|
|
||||||
localLibraryItems.add(localLibraryItem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
||||||
|
|
||||||
return if (localLibraryItems.isNotEmpty()) {
|
return if (mediaItemsAdded > 0 || mediaItemsUpdated > 0 || mediaItemsRemoved > 0) {
|
||||||
DeviceManager.dbManager.saveLocalLibraryItems(localLibraryItems)
|
|
||||||
|
|
||||||
var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
|
var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
|
||||||
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
|
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
|
||||||
} else {
|
} else {
|
||||||
|
@ -205,6 +84,135 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
||||||
|
var itemFolderName = itemFolder.name ?: ""
|
||||||
|
var itemId = getLocalLibraryItemId(itemFolder.id)
|
||||||
|
|
||||||
|
var existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
|
||||||
|
var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
|
||||||
|
var isNewOrUpdated = existingItem == null
|
||||||
|
|
||||||
|
var audioTracks = mutableListOf<AudioTrack>()
|
||||||
|
var localFiles = mutableListOf<LocalFile>()
|
||||||
|
var index = 1
|
||||||
|
var startOffset = 0.0
|
||||||
|
var coverContentUrl:String? = null
|
||||||
|
var coverAbsolutePath:String? = null
|
||||||
|
|
||||||
|
var filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||||
|
var isPodcast = localFolder.mediaType == "podcast"
|
||||||
|
|
||||||
|
var existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
|
}
|
||||||
|
if (existingLocalFilesRemoved.isNotEmpty()) {
|
||||||
|
Log.d(tag, "${existingLocalFilesRemoved.size} Local files were removed from local media item ${existingItem?.media?.metadata?.title}")
|
||||||
|
isNewOrUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
filesInFolder.forEach { file ->
|
||||||
|
var mimeType = file?.mimeType ?: ""
|
||||||
|
var filename = file?.name ?: ""
|
||||||
|
var isAudio = mimeType.startsWith("audio")
|
||||||
|
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
||||||
|
|
||||||
|
var 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())
|
||||||
|
localFiles.add(localFile)
|
||||||
|
|
||||||
|
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
||||||
|
|
||||||
|
if (isAudio) {
|
||||||
|
var audioTrackToAdd:AudioTrack? = null
|
||||||
|
|
||||||
|
var existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
||||||
|
if (existingAudioTrack != null) { // Update existing audio track
|
||||||
|
if (existingAudioTrack.index != index) {
|
||||||
|
Log.d(tag, "Updating Audio track index from ${existingAudioTrack.index} to $index")
|
||||||
|
existingAudioTrack.index = index
|
||||||
|
isNewOrUpdated = true
|
||||||
|
}
|
||||||
|
if (existingAudioTrack.startOffset != startOffset) {
|
||||||
|
Log.d(tag, "Updating Audio track startOffset ${existingAudioTrack.startOffset} to $startOffset")
|
||||||
|
existingAudioTrack.startOffset = startOffset
|
||||||
|
isNewOrUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingAudioTrack == null || forceAudioProbe) {
|
||||||
|
Log.d(tag, "Scanning Audio File Path ${localFile.absolutePath}")
|
||||||
|
|
||||||
|
// TODO: Make asynchronous
|
||||||
|
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||||
|
|
||||||
|
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
||||||
|
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||||
|
|
||||||
|
if (existingAudioTrack != null) {
|
||||||
|
// Update audio probe data on existing audio track
|
||||||
|
existingAudioTrack.audioProbeResult = audioProbeResult
|
||||||
|
audioTrackToAdd = existingAudioTrack
|
||||||
|
} else {
|
||||||
|
// Create new audio track
|
||||||
|
var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, null, true, localFileId, audioProbeResult, null)
|
||||||
|
audioTrackToAdd = track
|
||||||
|
}
|
||||||
|
|
||||||
|
startOffset += audioProbeResult.duration
|
||||||
|
isNewOrUpdated = true
|
||||||
|
} else {
|
||||||
|
audioTrackToAdd = existingAudioTrack
|
||||||
|
}
|
||||||
|
|
||||||
|
startOffset += audioTrackToAdd.duration
|
||||||
|
index++
|
||||||
|
audioTracks.add(audioTrackToAdd)
|
||||||
|
} else {
|
||||||
|
var existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
||||||
|
|
||||||
|
if (existingLocalFile == null) {
|
||||||
|
isNewOrUpdated = true
|
||||||
|
}
|
||||||
|
if (existingItem != null && existingItem.coverContentUrl == null) {
|
||||||
|
// Existing media item did not have a cover - cover found on scan
|
||||||
|
isNewOrUpdated = true
|
||||||
|
existingItem.coverAbsolutePath = localFile.absolutePath
|
||||||
|
existingItem.coverContentUrl = localFile.contentUrl
|
||||||
|
existingItem.media.coverPath = localFile.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// First image file use as cover path
|
||||||
|
if (coverContentUrl == null) {
|
||||||
|
coverContentUrl = localFile.contentUrl
|
||||||
|
coverAbsolutePath = localFile.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingItem != null && audioTracks.isEmpty()) {
|
||||||
|
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
|
||||||
|
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
|
||||||
|
return ItemScanResult.REMOVED
|
||||||
|
} else if (existingItem != null && !isNewOrUpdated) {
|
||||||
|
Log.d(tag, "Local library item ${existingItem.media.metadata.title} has no updates")
|
||||||
|
return ItemScanResult.UPTODATE
|
||||||
|
} else if (existingItem != null) {
|
||||||
|
Log.d(tag, "Updating local library item ${existingItem.media.metadata.title}")
|
||||||
|
existingItem.updateFromScan(audioTracks,localFiles)
|
||||||
|
DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
|
||||||
|
return ItemScanResult.UPDATED
|
||||||
|
} else if (audioTracks.isNotEmpty()) {
|
||||||
|
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
||||||
|
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
|
||||||
|
var localLibraryItem = localMediaItem.getLocalLibraryItem()
|
||||||
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
|
return ItemScanResult.ADDED
|
||||||
|
} else {
|
||||||
|
return ItemScanResult.UPTODATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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):LocalLibraryItem? {
|
||||||
var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
var folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
||||||
|
@ -236,63 +244,82 @@ 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") {
|
||||||
var localFiles:MutableList<LocalFile> = mutableListOf()
|
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 audioTracks:MutableList<AudioTrack> = mutableListOf()
|
} else {
|
||||||
|
// Lookup or create podcast local library item
|
||||||
filesFound.forEach { docFile ->
|
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem("local_${downloadItem.libraryItemId}")
|
||||||
var itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
if (localLibraryItem == null) {
|
||||||
itemPart.filename == docFile.name
|
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)
|
||||||
if (itemPart == null) {
|
|
||||||
Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}")
|
|
||||||
} else if (itemPart.audioTrack != null) { // Is audio track
|
|
||||||
var audioTrackFromServer = itemPart.audioTrack
|
|
||||||
|
|
||||||
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())
|
|
||||||
localFiles.add(localFile)
|
|
||||||
|
|
||||||
// TODO: Make asynchronous
|
|
||||||
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
|
||||||
|
|
||||||
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
|
||||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
audioTracks.add(track)
|
|
||||||
} else { // Cover image
|
|
||||||
var localFileId = DeviceManager.getBase64Id(docFile.id)
|
|
||||||
var localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
|
||||||
localFiles.add(localFile)
|
|
||||||
|
|
||||||
localLibraryItem.coverAbsolutePath = localFile.absolutePath
|
|
||||||
localLibraryItem.coverContentUrl = localFile.contentUrl
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var audioTracks:MutableList<AudioTrack> = mutableListOf()
|
||||||
|
|
||||||
|
filesFound.forEach { docFile ->
|
||||||
|
var itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
||||||
|
itemPart.filename == docFile.name
|
||||||
|
}
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
} else if (itemPart.audioTrack != null) { // Is audio track
|
||||||
|
var audioTrackFromServer = itemPart.audioTrack
|
||||||
|
|
||||||
|
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())
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
|
||||||
|
// TODO: Make asynchronous
|
||||||
|
var session = FFprobeKit.execute("-i \"${localFile.absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||||
|
|
||||||
|
val audioProbeResult = jacksonObjectMapper().readValue<AudioProbeResult>(session.output)
|
||||||
|
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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
|
||||||
|
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())
|
||||||
|
|
||||||
|
localLibraryItem.coverAbsolutePath = localFile.absolutePath
|
||||||
|
localLibraryItem.coverContentUrl = localFile.contentUrl
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (audioTracks.isEmpty()) {
|
if (audioTracks.isEmpty()) {
|
||||||
Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}")
|
Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
audioTracks.sortBy { it.index }
|
// For books sort audio tracks then set
|
||||||
|
if (downloadItem.mediaType == "book") {
|
||||||
|
audioTracks.sortBy { it.index }
|
||||||
|
|
||||||
var indexCheck = 1
|
var indexCheck = 1
|
||||||
var startOffset = 0.0
|
var startOffset = 0.0
|
||||||
audioTracks.forEach { audioTrack ->
|
audioTracks.forEach { audioTrack ->
|
||||||
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
|
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
|
||||||
audioTrack.index = indexCheck
|
audioTrack.index = indexCheck
|
||||||
audioTrack.startOffset = startOffset
|
audioTrack.startOffset = startOffset
|
||||||
|
}
|
||||||
|
indexCheck++
|
||||||
|
startOffset += audioTrack.duration
|
||||||
}
|
}
|
||||||
indexCheck++
|
|
||||||
startOffset += audioTrack.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
localLibraryItem.media.setAudioTracks(audioTracks)
|
localLibraryItem.media.setAudioTracks(audioTracks)
|
||||||
localLibraryItem.localFiles = localFiles
|
}
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
call.resolve()
|
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()
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -261,24 +261,28 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setUserAndConnection(user, userDefaultLibraryId) {
|
async setUserAndConnection(user, userDefaultLibraryId) {
|
||||||
if (user) {
|
if (!user) return
|
||||||
console.log('Successfully logged in', JSON.stringify(user))
|
|
||||||
|
|
||||||
if (userDefaultLibraryId) {
|
console.log('Successfully logged in', JSON.stringify(user))
|
||||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.serverConfig.userId = user.id
|
// Set library - Use last library if set and available fallback to default user library
|
||||||
this.serverConfig.token = user.token
|
var lastLibraryId = await this.$localStore.getLastLibraryId()
|
||||||
|
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
|
||||||
var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig)
|
this.$store.commit('libraries/setCurrentLibrary', lastLibraryId)
|
||||||
|
} else if (userDefaultLibraryId) {
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||||
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
|
|
||||||
|
|
||||||
this.$socket.connect(this.serverConfig.address, this.serverConfig.token)
|
|
||||||
this.$router.replace('/bookshelf')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.serverConfig.userId = user.id
|
||||||
|
this.serverConfig.token = user.token
|
||||||
|
|
||||||
|
var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig)
|
||||||
|
|
||||||
|
this.$store.commit('user/setUser', user)
|
||||||
|
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
|
||||||
|
|
||||||
|
this.$socket.connect(this.serverConfig.address, this.serverConfig.token)
|
||||||
|
this.$router.replace('/bookshelf')
|
||||||
},
|
},
|
||||||
async authenticateToken() {
|
async authenticateToken() {
|
||||||
if (!this.networkConnected) return
|
if (!this.networkConnected) return
|
||||||
|
|
|
@ -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() {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
183
components/tables/podcast/EpisodeRow.vue
Normal file
183
components/tables/podcast/EpisodeRow.vue
Normal 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>
|
25
components/tables/podcast/EpisodesTable.vue
Normal file
25
components/tables/podcast/EpisodesTable.vue
Normal 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>
|
57
components/ui/ReadIconBtn.vue
Normal file
57
components/ui/ReadIconBtn.vue
Normal 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>
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,64 @@
|
||||||
<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">
|
||||||
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
<div v-if="!isPodcast" class="w-full">
|
||||||
<template v-for="track in audioTracks">
|
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p>
|
||||||
<div :key="track.localFileId" class="flex items-center my-1">
|
<template v-for="track in audioTracks">
|
||||||
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
|
<div :key="track.localFileId" class="flex items-center my-1">
|
||||||
<p class="font-mono font-bold text-xl">{{ track.index }}</p>
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2">
|
||||||
|
<p class="text-xs">{{ track.title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-20 text-center text-gray-300" style="min-width: 80px">
|
||||||
|
<p class="text-xs">{{ track.mimeType }}</p>
|
||||||
|
<p class="text-sm">{{ $elapsedPretty(track.duration) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
|
||||||
|
<span class="material-icons" @click="showTrackDialog(track)">more_vert</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2">
|
</template>
|
||||||
<p class="text-sm">{{ track.title }}</p>
|
</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>
|
</div>
|
||||||
<div class="w-20 text-center text-gray-300" style="min-width: 80px">
|
</template>
|
||||||
<p class="text-xs">{{ track.mimeType }}</p>
|
</div>
|
||||||
<p class="text-sm">{{ $elapsedPretty(track.duration) }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-12 h-12 flex items-center justify-center" style="min-width: 48px">
|
|
||||||
<span class="material-icons" @click="showTrackDialog(track)">more_vert</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<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) {
|
||||||
this.selectedAudioTrack = track
|
if (this.isPodcast) {
|
||||||
|
this.selectedAudioTrack = null
|
||||||
|
this.selectedEpisode = track
|
||||||
|
} else {
|
||||||
|
this.selectedEpisode = null
|
||||||
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue