mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-08 12:58:38 +02:00
Add podcast pages, android download podcast, scan podcast folders
This commit is contained in:
@@ -105,11 +105,9 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Main
|
||||
playbackSession: null,
|
||||
// Others
|
||||
showChapterModal: false,
|
||||
showCastBtn: true,
|
||||
showCastBtn: false,
|
||||
showFullscreen: false,
|
||||
totalDuration: 0,
|
||||
currentPlaybackRate: 1,
|
||||
@@ -493,6 +491,7 @@ export default {
|
||||
onPlayingUpdate(data) {
|
||||
console.log('onPlayingUpdate', JSON.stringify(data))
|
||||
this.isPaused = !data.value
|
||||
this.$store.commit('setPlayerPlaying', !this.isPaused)
|
||||
if (!this.isPaused) {
|
||||
this.startPlayInterval()
|
||||
} else {
|
||||
@@ -519,6 +518,8 @@ export default {
|
||||
console.log('onPlaybackSession received', JSON.stringify(playbackSession))
|
||||
this.playbackSession = playbackSession
|
||||
|
||||
this.$store.commit('setPlayerItem', this.playbackSession)
|
||||
|
||||
// Set track width
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.track) {
|
||||
@@ -530,6 +531,7 @@ export default {
|
||||
},
|
||||
onPlaybackClosed() {
|
||||
console.log('Received onPlaybackClosed evt')
|
||||
this.$store.commit('setPlayerItem', null)
|
||||
this.showFullscreen = false
|
||||
this.playbackSession = null
|
||||
},
|
||||
|
||||
@@ -166,9 +166,12 @@ export default {
|
||||
this.$refs.audioPlayer.terminateStream()
|
||||
}
|
||||
},
|
||||
async playLibraryItem(libraryItemId) {
|
||||
async playLibraryItem(payload) {
|
||||
var libraryItemId = payload.libraryItemId
|
||||
var episodeId = payload.episodeId
|
||||
|
||||
console.log('Called playLibraryItem', libraryItemId)
|
||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, playWhenReady: true })
|
||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true })
|
||||
.then((data) => {
|
||||
console.log('Library item play response', JSON.stringify(data))
|
||||
})
|
||||
@@ -176,6 +179,11 @@ export default {
|
||||
console.error('Failed', error)
|
||||
})
|
||||
},
|
||||
pauseItem() {
|
||||
if (this.$refs.audioPlayer && !this.$refs.audioPlayer.isPaused) {
|
||||
this.$refs.audioPlayer.pause()
|
||||
}
|
||||
},
|
||||
onLocalMediaProgressUpdate(localMediaProgress) {
|
||||
console.log('Got local media progress update', localMediaProgress.progress, JSON.stringify(localMediaProgress))
|
||||
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
|
||||
@@ -191,6 +199,7 @@ export default {
|
||||
|
||||
this.setListeners()
|
||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
||||
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.$eventBus.$off('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ export default {
|
||||
},
|
||||
play() {
|
||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||
eventBus.$emit('play-item', this.libraryItemId)
|
||||
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
|
||||
@@ -397,7 +397,7 @@ export default {
|
||||
},
|
||||
play() {
|
||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||
eventBus.$emit('play-item', this.libraryItemId)
|
||||
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
|
||||
@@ -261,24 +261,28 @@ export default {
|
||||
}
|
||||
},
|
||||
async setUserAndConnection(user, userDefaultLibraryId) {
|
||||
if (user) {
|
||||
console.log('Successfully logged in', JSON.stringify(user))
|
||||
if (!user) return
|
||||
|
||||
if (userDefaultLibraryId) {
|
||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||
}
|
||||
console.log('Successfully logged in', JSON.stringify(user))
|
||||
|
||||
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')
|
||||
// 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.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() {
|
||||
if (!this.networkConnected) return
|
||||
|
||||
@@ -55,6 +55,7 @@ export default {
|
||||
this.show = false
|
||||
await this.$store.dispatch('libraries/fetch', lib.id)
|
||||
this.$eventBus.$emit('library-changed', lib.id)
|
||||
this.$localStore.setLastLibraryId(lib.id)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
return this.book.numTracks
|
||||
},
|
||||
isStreaming() {
|
||||
return this.$store.getters['getAudiobookIdStreaming'] === this.book.id
|
||||
return this.$store.getters['getIsItemStreaming'](this.book.id)
|
||||
},
|
||||
showPlayBtn() {
|
||||
return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 = {
|
||||
id: data.id,
|
||||
libraryItemId: data.libraryItemId,
|
||||
partsRemaining,
|
||||
partsCompleted,
|
||||
totalParts: downloadItemParts.length,
|
||||
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))
|
||||
this.$set(this.itemDownloadingMap, update.id, update)
|
||||
|
||||
Reference in New Issue
Block a user