diff --git a/Dockerfile b/Dockerfile index db7642bb..5936ee6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,4 +29,4 @@ HEALTHCHECK \ --timeout=3s \ --start-period=10s \ CMD curl -f http://127.0.0.1/healthcheck || exit 1 -CMD ["npm", "start"] +CMD ["node", "index.js"] diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 124c332d..d512bc1b 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -64,12 +64,22 @@ - -

{{ route.title }}

+ +

{{ route.title }}

diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index bf58e341..25350d24 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -86,6 +86,14 @@
+ + file_download + +

{{ $strings.ButtonDownloadQueue }}

+ +
+ + warning @@ -149,6 +157,9 @@ export default { isMusicLibrary() { return this.currentLibraryMediaType === 'music' }, + isPodcastDownloadQueuePage() { + return this.$route.name === 'library-library-podcast-download-queue' + }, isPodcastSearchPage() { return this.$route.name === 'library-library-podcast-search' }, @@ -212,4 +223,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index 95e29554..27e79d8f 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -11,12 +11,15 @@
person -

{{ podcastAuthor }}

-

{{ musicArtists }}

-

- {{ author.name }} -

-

{{ $strings.LabelUnknown }}

+
+
{{ podcastAuthor }}
+
{{ musicArtists }}
+
+ {{ author.name }} +
+
{{ $strings.LabelUnknown }}
+ +
@@ -129,6 +132,9 @@ export default { isMusic() { return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false }, + isExplicit() { + return this.mediaMetadata.explicit || false + }, mediaMetadata() { return this.media.metadata || {} }, @@ -474,4 +480,4 @@ export default { #streamContainer { box-shadow: 0px -6px 8px #1111113f; } - \ No newline at end of file + diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index d458664f..dd782d30 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -28,7 +28,11 @@
-

{{ book.title }}

+

+
+ {{ book.title }} +
+

by {{ book.author }}

{{ book.genres.join(', ') }}

{{ book.trackCount }} Episodes

@@ -78,4 +82,4 @@ export default { this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null } } - \ No newline at end of file + diff --git a/client/components/cards/ItemTaskRunningCard.vue b/client/components/cards/ItemTaskRunningCard.vue new file mode 100644 index 00000000..f7ee4095 --- /dev/null +++ b/client/components/cards/ItemTaskRunningCard.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 38bbb76a..d54ab791 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -7,9 +7,12 @@
-

- {{ displayTitle }} -

+
+
+ {{ displayTitle }} + +
+

{{ displayLineTwo || ' ' }}

{{ displaySortLine }}

@@ -102,8 +105,10 @@
-
-

Episode #{{ recentEpisodeNumber }}

+
+

+ Episode #{{ recentEpisodeNumber }} +

@@ -193,6 +198,9 @@ export default { isMusic() { return this.mediaType === 'music' }, + isExplicit() { + return this.mediaMetadata.explicit || false + }, placeholderUrl() { const config = this.$config || this.$nuxt.$config return `${config.routerBasePath}/book_placeholder.jpg` @@ -236,7 +244,7 @@ export default { if (this.recentEpisode.episode) { return this.recentEpisode.episode.replace(/^#/, '') } - return this.recentEpisode.index + return '' }, collapsedSeries() { // Only added to item object when collapseSeries is enabled @@ -734,7 +742,7 @@ export default { episodeId: this.recentEpisode.id, title: this.recentEpisode.title, subtitle: this.mediaMetadata.title, - caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: this.recentEpisode.audioFile.duration || null, coverPath: this.media.coverPath || null } @@ -858,7 +866,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.mediaMetadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.media.coverPath || null }) diff --git a/client/components/modals/BookmarksModal.vue b/client/components/modals/BookmarksModal.vue index de76c4c1..1bf3748d 100644 --- a/client/components/modals/BookmarksModal.vue +++ b/client/components/modals/BookmarksModal.vue @@ -73,6 +73,12 @@ export default { }, canCreateBookmark() { return !this.bookmarks.find((bm) => bm.time === this.currentTime) + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -111,7 +117,7 @@ export default { }, submitCreateBookmark() { if (!this.newBookmarkTitle) { - this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm') + this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat) } var bookmark = { title: this.newBookmarkTitle, @@ -134,4 +140,4 @@ export default { } } } - \ No newline at end of file + diff --git a/client/components/modals/ListeningSessionModal.vue b/client/components/modals/ListeningSessionModal.vue index fb435107..79e66cc7 100644 --- a/client/components/modals/ListeningSessionModal.vue +++ b/client/components/modals/ListeningSessionModal.vue @@ -19,13 +19,13 @@
{{ $strings.LabelStartedAt }}
- {{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }} + {{ $formatDatetime(_session.startedAt, dateFormat, timeFormat) }}
{{ $strings.LabelUpdatedAt }}
- {{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }} + {{ $formatDatetime(_session.updatedAt, dateFormat, timeFormat) }}
@@ -151,6 +151,12 @@ export default { else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream' else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local' return 'Unknown' + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -186,4 +192,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 6749c730..6cc1e1ba 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -164,6 +164,13 @@

{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}

+
+ +
+ +

{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? 'Explicit (checked)' : 'Not Explicit (unchecked)' }}

+
+
{{ $strings.ButtonSubmit }} @@ -327,6 +334,7 @@ export default { res.itunesPageUrl = res.pageUrl || null res.itunesId = res.id || null res.author = res.artistName || null + res.explicit = res.explicit || false return res }) } diff --git a/client/components/modals/item/tabs/Schedule.vue b/client/components/modals/item/tabs/Schedule.vue index 80e396c8..032936d1 100644 --- a/client/components/modals/item/tabs/Schedule.vue +++ b/client/components/modals/item/tabs/Schedule.vue @@ -59,6 +59,14 @@ export default { newMaxNewEpisodesToDownload: 0 } }, + watch: { + libraryItem: { + immediate: true, + handler(newVal) { + if (newVal) this.init() + } + } + }, computed: { isProcessing: { get() { @@ -176,4 +184,4 @@ export default { height: calc(100% - 80px); max-height: calc(100% - 80px); } - \ No newline at end of file + diff --git a/client/components/modals/podcast/EditEpisode.vue b/client/components/modals/podcast/EditEpisode.vue index 236a0d87..ed9c8f28 100644 --- a/client/components/modals/podcast/EditEpisode.vue +++ b/client/components/modals/podcast/EditEpisode.vue @@ -11,8 +11,15 @@
+
+
arrow_back_ios
+
+
+
arrow_forward_ios
+
+
- +
@@ -21,8 +28,8 @@ export default { data() { return { + episodeItem: null, processing: false, - selectedTab: 'details', tabs: [ { id: 'details', @@ -37,6 +44,29 @@ export default { ] } }, + watch: { + show: { + handler(newVal) { + if (newVal) { + const availableTabIds = this.tabs.map((tab) => tab.id) + if (!availableTabIds.length) { + this.show = false + return + } + + if (!availableTabIds.includes(this.selectedTab)) { + this.selectedTab = availableTabIds[0] + } + + this.episodeItem = null + this.init() + this.registerListeners() + } else { + this.unregisterListeners() + } + } + } + }, computed: { show: { get() { @@ -46,27 +76,118 @@ export default { this.$store.commit('globals/setShowEditPodcastEpisodeModal', val) } }, + selectedTab: { + get() { + return this.$store.state.editPodcastModalTab + }, + set(val) { + this.$store.commit('setEditPodcastModalTab', val) + } + }, libraryItem() { return this.$store.state.selectedLibraryItem }, episode() { return this.$store.state.globals.selectedEpisode }, + selectedEpisodeId() { + return this.episode.id + }, title() { - if (!this.libraryItem) return '' - return this.libraryItem.media.metadata.title || 'Unknown' + return this.libraryItem?.media.metadata.title || 'Unknown' }, tabComponentName() { - var _tab = this.tabs.find((t) => t.id === this.selectedTab) + const _tab = this.tabs.find((t) => t.id === this.selectedTab) return _tab ? _tab.component : '' + }, + episodeTableEpisodeIds() { + return this.$store.state.episodeTableEpisodeIds || [] + }, + currentEpisodeIndex() { + if (!this.episodeTableEpisodeIds.length) return 0 + return this.episodeTableEpisodeIds.findIndex((bid) => bid === this.selectedEpisodeId) + }, + canGoPrev() { + return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex > 0 + }, + canGoNext() { + return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex < this.episodeTableEpisodeIds.length - 1 } }, methods: { + async goPrevEpisode() { + if (this.currentEpisodeIndex - 1 < 0) return + const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1] + this.processing = true + const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => { + const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode' + this.$toast.error(errorMsg) + return null + }) + this.processing = false + if (prevEpisode) { + this.episodeItem = prevEpisode + this.$store.commit('globals/setSelectedEpisode', prevEpisode) + } else { + console.error('Episode not found', prevEpisodeId) + } + }, + async goNextEpisode() { + if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return + this.processing = true + const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1] + const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => { + const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book' + this.$toast.error(errorMsg) + return null + }) + this.processing = false + if (nextEpisode) { + this.episodeItem = nextEpisode + this.$store.commit('globals/setSelectedEpisode', nextEpisode) + } else { + console.error('Episode not found', nextEpisodeId) + } + }, selectTab(tab) { - this.selectedTab = tab + if (this.selectedTab === tab) return + if (this.tabs.find((t) => t.id === tab)) { + this.selectedTab = tab + this.processing = false + } + }, + init() { + this.fetchFull() + }, + async fetchFull() { + try { + this.processing = true + this.episodeItem = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${this.selectedEpisodeId}`) + this.processing = false + } catch (error) { + console.error('Failed to fetch episode', this.selectedEpisodeId, error) + this.processing = false + this.show = false + } + }, + hotkey(action) { + if (action === this.$hotkeys.Modal.NEXT_PAGE) { + this.goNextEpisode() + } else if (action === this.$hotkeys.Modal.PREV_PAGE) { + this.goPrevEpisode() + } + }, + registerListeners() { + this.$eventBus.$on('modal-hotkey', this.hotkey) + }, + unregisterListeners() { + this.$eventBus.$off('modal-hotkey', this.hotkey) } }, - mounted() {} + mounted() {}, + beforeDestroy() { + this.unregisterListeners() + } } @@ -77,4 +198,4 @@ export default { .tab.tab-selected { height: 41px; } - \ No newline at end of file + diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 21b9cdfc..c8be0345 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -24,8 +24,15 @@
-

#{{ episode.episode }}

-

{{ episode.title }}

+
+
#
+
{{ episode.season }}x
+
{{ episode.episode }}
+
+
+
{{ episode.title }}
+ +

{{ episode.subtitle }}

Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}

diff --git a/client/components/modals/podcast/NewModal.vue b/client/components/modals/podcast/NewModal.vue index 59b8dfd9..d9e3e461 100644 --- a/client/components/modals/podcast/NewModal.vue +++ b/client/components/modals/podcast/NewModal.vue @@ -28,6 +28,17 @@
+
+
+ +
+
+ +
+
+ +
+
@@ -82,7 +93,10 @@ export default { itunesPageUrl: '', itunesId: '', itunesArtistId: '', - autoDownloadEpisodes: false + autoDownloadEpisodes: false, + language: '', + explicit: false, + type: '' } } }, @@ -140,6 +154,9 @@ export default { selectedFolderPath() { if (!this.selectedFolder) return '' return this.selectedFolder.fullPath + }, + podcastTypes() { + return this.$store.state.globals.podcastTypes || [] } }, methods: { @@ -170,7 +187,9 @@ export default { itunesPageUrl: this.podcast.itunesPageUrl, itunesId: this.podcast.itunesId, itunesArtistId: this.podcast.itunesArtistId, - language: this.podcast.language + language: this.podcast.language, + explicit: this.podcast.explicit, + type: this.podcast.type }, autoDownloadEpisodes: this.podcast.autoDownloadEpisodes } @@ -205,9 +224,11 @@ export default { this.podcast.itunesPageUrl = this._podcastData.pageUrl || '' this.podcast.itunesId = this._podcastData.id || '' this.podcast.itunesArtistId = this._podcastData.artistId || '' - this.podcast.language = this._podcastData.language || '' + this.podcast.language = this._podcastData.language || this.feedMetadata.language || '' this.podcast.autoDownloadEpisodes = false + this.podcast.type = this._podcastData.type || this.feedMetadata.type || 'episodic' + this.podcast.explicit = this._podcastData.explicit || this.feedMetadata.explicit === 'yes' || this.feedMetadata.explicit == 'true' if (this.folderItems[0]) { this.selectedFolderId = this.folderItems[0].value this.folderUpdated() @@ -226,4 +247,4 @@ export default { #episodes-scroll { max-height: calc(80vh - 200px); } - \ No newline at end of file + diff --git a/client/components/modals/podcast/tabs/EpisodeDetails.vue b/client/components/modals/podcast/tabs/EpisodeDetails.vue index ffa95ec0..debf9155 100644 --- a/client/components/modals/podcast/tabs/EpisodeDetails.vue +++ b/client/components/modals/podcast/tabs/EpisodeDetails.vue @@ -8,7 +8,7 @@
- +
@@ -24,7 +24,12 @@
- {{ $strings.ButtonSubmit }} + + + + + + {{ $strings.ButtonSave }}

Episode URL from RSS feed

@@ -89,6 +94,9 @@ export default { }, enclosureUrl() { return this.enclosure.url + }, + episodeTypes() { + return this.$store.state.globals.episodeTypes || [] } }, methods: { @@ -122,28 +130,43 @@ export default { } return updatePayload }, - submit() { - const payload = this.getUpdatePayload() - if (!Object.keys(payload).length) { - return this.$toast.info('No updates were made') + async saveAndClose() { + const wasUpdated = await this.submit() + if (wasUpdated !== null) this.$emit('close') + }, + async submit() { + if (this.isProcessing) { + return null } + const updatedDetails = this.getUpdatePayload() + if (!Object.keys(updatedDetails).length) { + this.$toast.info('No changes were made') + return false + } + return this.updateDetails(updatedDetails) + }, + async updateDetails(updatedDetails) { this.isProcessing = true - this.$axios - .$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload) - .then(() => { - this.isProcessing = false + const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => { + console.error('Failed update episode', error) + this.isProcessing = false + this.$toast.error(error?.response?.data || 'Failed to update episode') + return false + }) + + this.isProcessing = false + if (updateResult) { + if (updateResult) { this.$toast.success('Podcast episode updated') - this.$emit('close') - }) - .catch((error) => { - var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode' - console.error('Failed update episode', error) - this.isProcessing = false - this.$toast.error(errorMsg) - }) + return true + } else { + this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) + } + } + return false } }, mounted() {} } - \ No newline at end of file + diff --git a/client/components/modals/rssfeed/OpenCloseModal.vue b/client/components/modals/rssfeed/OpenCloseModal.vue index 576bb4d7..f5fe647c 100644 --- a/client/components/modals/rssfeed/OpenCloseModal.vue +++ b/client/components/modals/rssfeed/OpenCloseModal.vue @@ -14,6 +14,27 @@ content_copy
+ +
+
+
+ {{ $strings.LabelRSSFeedPreventIndexing }} +
+
{{ currentFeed.meta.preventIndexing ? 'Yes' : 'No' }}
+
+
+
+ {{ $strings.LabelRSSFeedCustomOwnerName }} +
+
{{ currentFeed.meta.ownerName }}
+
+
+
+ {{ $strings.LabelRSSFeedCustomOwnerEmail }} +
+
{{ currentFeed.meta.ownerEmail }}
+
+

{{ $strings.HeaderOpenRSSFeed }}

@@ -22,6 +43,7 @@

{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}

+

{{ $strings.NoteRSSFeedPodcastAppsHttps }}

{{ $strings.NoteRSSFeedPodcastAppsPubDate }}

@@ -41,7 +63,12 @@ export default { return { processing: false, newFeedSlug: null, - currentFeed: null + currentFeed: null, + metadataDetails: { + preventIndexing: true, + ownerName: '', + ownerEmail: '' + } } }, watch: { @@ -107,7 +134,8 @@ export default { const payload = { serverAddress: window.origin, - slug: this.newFeedSlug + slug: this.newFeedSlug, + metadataDetails: this.metadataDetails } if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}` diff --git a/client/components/tables/BackupsTable.vue b/client/components/tables/BackupsTable.vue index 98f017ad..6df009b8 100644 --- a/client/components/tables/BackupsTable.vue +++ b/client/components/tables/BackupsTable.vue @@ -17,7 +17,7 @@

/{{ backup.path.replace(/\\/g, '/') }}

- {{ backup.datePretty }} + {{ $formatDatetime(backup.createdAt, dateFormat, timeFormat) }} {{ $bytesPretty(backup.fileSize) }}
@@ -46,7 +46,7 @@

{{ $strings.MessageImportantNotice }}

-

{{ $strings.MessageRestoreBackupConfirm }} {{ selectedBackup.datePretty }}?

+

{{ $strings.MessageRestoreBackupConfirm }} {{ $formatDatetime(selectedBackup.createdAt, dateFormat, timeFormat) }}?

{{ $strings.ButtonNevermind }}
@@ -71,6 +71,12 @@ export default { computed: { userToken() { return this.$store.getters['user/getToken'] + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -90,7 +96,7 @@ export default { }) }, deleteBackupClick(backup) { - if (confirm(this.$getString('MessageConfirmDeleteBackup', [backup.datePretty]))) { + if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) { this.processing = true this.$axios .$delete(`/api/backups/${backup.id}`) @@ -208,4 +214,4 @@ export default { padding-bottom: 5px; background-color: #333; } - \ No newline at end of file + diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue index 79876ca3..88323a74 100644 --- a/client/components/tables/UsersTable.vue +++ b/client/components/tables/UsersTable.vue @@ -25,13 +25,13 @@
- + {{ $dateDistanceFromNow(user.lastSeen) }} - - {{ $formatDate(user.createdAt, 'MMM d, yyyy') }} + + {{ $formatDate(user.createdAt, dateFormat) }} @@ -74,6 +74,12 @@ export default { var usermap = {} this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u)) return usermap + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -201,4 +207,4 @@ export default { padding-bottom: 5px; background-color: #272727; } - \ No newline at end of file + diff --git a/client/components/tables/podcast/DownloadQueueTable.vue b/client/components/tables/podcast/DownloadQueueTable.vue new file mode 100644 index 00000000..4b911229 --- /dev/null +++ b/client/components/tables/podcast/DownloadQueueTable.vue @@ -0,0 +1,65 @@ + + + diff --git a/client/components/tables/podcast/EpisodeTableRow.vue b/client/components/tables/podcast/EpisodeTableRow.vue index a184b3c8..3012faf4 100644 --- a/client/components/tables/podcast/EpisodeTableRow.vue +++ b/client/components/tables/podcast/EpisodeTableRow.vue @@ -2,16 +2,17 @@
-

- {{ title }} -

+
+ {{ title }} + +

{{ subtitle }}

Season #{{ episode.season }}

Episode #{{ episode.episode }}

-

Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}

+

Published {{ $formatDate(publishedAt, dateFormat) }}

@@ -128,6 +129,9 @@ export default { }, publishedAt() { return this.episode.publishedAt + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat } }, methods: { @@ -205,4 +209,4 @@ export default { } } } - \ No newline at end of file + diff --git a/client/components/tables/podcast/EpisodesTable.vue b/client/components/tables/podcast/EpisodesTable.vue index 05eaa50d..a5af04cd 100644 --- a/client/components/tables/podcast/EpisodesTable.vue +++ b/client/components/tables/podcast/EpisodesTable.vue @@ -160,6 +160,12 @@ export default { var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id) return !itemProgress || !itemProgress.isFinished }) + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -222,7 +228,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.mediaMetadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.media.coverPath || null } @@ -290,7 +296,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.mediaMetadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.media.coverPath || null }) @@ -308,6 +314,8 @@ export default { this.showPodcastRemoveModal = true }, editEpisode(episode) { + const episodeIds = this.episodesSorted.map((e) => e.id) + this.$store.commit('setEpisodeTableEpisodeIds', episodeIds) this.$store.commit('setSelectedLibraryItem', this.libraryItem) this.$store.commit('globals/setSelectedEpisode', episode) this.$store.commit('globals/setShowEditPodcastEpisodeModal', true) diff --git a/client/components/ui/RichTextEditor.vue b/client/components/ui/RichTextEditor.vue index 068bc95f..582f5e8f 100644 --- a/client/components/ui/RichTextEditor.vue +++ b/client/components/ui/RichTextEditor.vue @@ -68,8 +68,6 @@ export default { } }, mounted() {}, - beforeDestroy() { - console.log('Before destroy') - } + beforeDestroy() {} } \ No newline at end of file diff --git a/client/components/ui/Tooltip.vue b/client/components/ui/Tooltip.vue index f376329b..c1eabfc6 100644 --- a/client/components/ui/Tooltip.vue +++ b/client/components/ui/Tooltip.vue @@ -51,8 +51,8 @@ export default { tooltip.style.zIndex = 100 tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)' tooltip.innerHTML = this.text - tooltip.addEventListener('mouseover', this.cancelHide); - tooltip.addEventListener('mouseleave', this.hideTooltip); + tooltip.addEventListener('mouseover', this.cancelHide) + tooltip.addEventListener('mouseleave', this.hideTooltip) this.setTooltipPosition(tooltip) @@ -107,7 +107,7 @@ export default { this.isShowing = false }, cancelHide() { - if (this.hideTimeout) clearTimeout(this.hideTimeout); + if (this.hideTimeout) clearTimeout(this.hideTimeout) }, mouseover() { if (!this.isShowing) this.showTooltip() diff --git a/client/components/widgets/AlreadyInLibraryIndicator.vue b/client/components/widgets/AlreadyInLibraryIndicator.vue new file mode 100644 index 00000000..e765b4e2 --- /dev/null +++ b/client/components/widgets/AlreadyInLibraryIndicator.vue @@ -0,0 +1,19 @@ + + + diff --git a/client/components/widgets/CronExpressionBuilder.vue b/client/components/widgets/CronExpressionBuilder.vue index 3997a0f3..04ec2fd1 100644 --- a/client/components/widgets/CronExpressionBuilder.vue +++ b/client/components/widgets/CronExpressionBuilder.vue @@ -36,6 +36,10 @@

{{ $strings.MessageValidCronExpression }}

+
+ event +

{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}

+
@@ -63,6 +67,14 @@ export default { isValid: true } }, + watch: { + value: { + immediate: true, + handler(newVal) { + this.init() + } + } + }, computed: { minuteIsValid() { return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59) @@ -70,6 +82,11 @@ export default { hourIsValid() { return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23) }, + nextRun() { + if (!this.cronExpression) return '' + const parsed = this.$getNextScheduledDate(this.cronExpression) + return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || '' + }, description() { if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return '' @@ -271,6 +288,11 @@ export default { }) }, init() { + this.selectedInterval = 'custom' + this.selectedHour = 0 + this.selectedMinute = 0 + this.selectedWeekdays = [] + if (!this.value) return const pieces = this.value.split(' ') if (pieces.length !== 5) { @@ -309,4 +331,4 @@ export default { this.init() } } - \ No newline at end of file + diff --git a/client/components/widgets/ExplicitIndicator.vue b/client/components/widgets/ExplicitIndicator.vue new file mode 100644 index 00000000..3ff68603 --- /dev/null +++ b/client/components/widgets/ExplicitIndicator.vue @@ -0,0 +1,19 @@ + + + diff --git a/client/components/widgets/NotificationWidget.vue b/client/components/widgets/NotificationWidget.vue index 6c3e82fd..f70f840d 100644 --- a/client/components/widgets/NotificationWidget.vue +++ b/client/components/widgets/NotificationWidget.vue @@ -1,15 +1,51 @@ \ No newline at end of file + + + diff --git a/client/components/widgets/PodcastDetailsEdit.vue b/client/components/widgets/PodcastDetailsEdit.vue index 8030028d..4c2fd739 100644 --- a/client/components/widgets/PodcastDetailsEdit.vue +++ b/client/components/widgets/PodcastDetailsEdit.vue @@ -39,6 +39,11 @@
+
+
+ +
+
@@ -65,7 +70,8 @@ export default { itunesId: null, itunesArtistId: null, explicit: false, - language: null + language: null, + type: null }, newTags: [] } @@ -93,6 +99,9 @@ export default { }, filterData() { return this.$store.state.libraries.filterData || {} + }, + podcastTypes() { + return this.$store.state.globals.podcastTypes || [] } }, methods: { @@ -219,6 +228,7 @@ export default { this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || '' this.details.language = this.mediaMetadata.language || '' this.details.explicit = !!this.mediaMetadata.explicit + this.details.type = this.mediaMetadata.type || 'episodic' this.newTags = [...(this.media.tags || [])] }, @@ -228,4 +238,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/client/components/widgets/PodcastTypeIndicator.vue b/client/components/widgets/PodcastTypeIndicator.vue new file mode 100644 index 00000000..d914d283 --- /dev/null +++ b/client/components/widgets/PodcastTypeIndicator.vue @@ -0,0 +1,31 @@ + + + diff --git a/client/components/widgets/RssFeedMetadataBuilder.vue b/client/components/widgets/RssFeedMetadataBuilder.vue new file mode 100644 index 00000000..5e629485 --- /dev/null +++ b/client/components/widgets/RssFeedMetadataBuilder.vue @@ -0,0 +1,90 @@ + + + diff --git a/client/package-lock.json b/client/package-lock.json index 3ef807eb..aa308947 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,17 +1,18 @@ { "name": "audiobookshelf-client", - "version": "2.2.15", + "version": "2.2.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.2.15", + "version": "2.2.16", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", "@nuxtjs/proxy": "^2.1.0", "core-js": "^3.16.0", + "cron-parser": "^4.7.1", "date-fns": "^2.25.0", "epubjs": "^0.3.88", "hls.js": "^1.0.7", @@ -5464,6 +5465,17 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/cron-parser": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz", + "integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -9134,6 +9146,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", + "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -21582,6 +21602,14 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "cron-parser": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz", + "integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==", + "requires": { + "luxon": "^3.2.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -24397,6 +24425,11 @@ "yallist": "^3.0.2" } }, + "luxon": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz", + "integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", diff --git a/client/package.json b/client/package.json index 26c40356..63aec8a6 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.2.15", + "version": "2.2.16", "description": "Self-hosted audiobook and podcast client", "main": "index.js", "scripts": { @@ -16,6 +16,7 @@ "@nuxtjs/axios": "^5.13.6", "@nuxtjs/proxy": "^2.1.0", "core-js": "^3.16.0", + "cron-parser": "^4.7.1", "date-fns": "^2.25.0", "epubjs": "^0.3.88", "hls.js": "^1.0.7", diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index c263119a..8c936ded 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -9,10 +9,17 @@
-
- schedule -

{{ scheduleDescription }}

- edit +
+ schedule +
{{ $strings.HeaderSchedule }}:
+
{{ scheduleDescription }}
+ edit +
+ +
+ event +
{{ $strings.LabelNextBackupDate }}:
+
{{ nextBackupDate }}
@@ -64,10 +71,21 @@ export default { serverSettings() { return this.$store.state.serverSettings }, + dateFormat() { + return this.serverSettings.dateFormat + }, + timeFormat() { + return this.serverSettings.timeFormat + }, scheduleDescription() { if (!this.cronExpression) return '' const parsed = this.$parseCronExpression(this.cronExpression) - return parsed ? parsed.description : 'Custom cron expression ' + this.cronExpression + return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}` + }, + nextBackupDate() { + if (!this.cronExpression) return '' + const parsed = this.$getNextScheduledDate(this.cronExpression) + return this.$formatJsDatetime(parsed, this.dateFormat, this.timeFormat) || '' } }, methods: { @@ -90,15 +108,15 @@ export default { updateServerSettings(payload) { this.updatingServerSettings = true this.$store - .dispatch('updateServerSettings', payload) - .then((success) => { - console.log('Updated Server Settings', success) - this.updatingServerSettings = false - }) - .catch((error) => { - console.error('Failed to update server settings', error) - this.updatingServerSettings = false - }) + .dispatch('updateServerSettings', payload) + .then((success) => { + console.log('Updated Server Settings', success) + this.updatingServerSettings = false + }) + .catch((error) => { + console.error('Failed to update server settings', error) + this.updatingServerSettings = false + }) }, initServerSettings() { this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} @@ -113,4 +131,4 @@ export default { this.initServerSettings() } } - \ No newline at end of file + diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 64f43e97..7f0170c7 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -68,8 +68,14 @@
-
+
+

{{ $strings.LabelExample }}: {{ dateExample }}

+
+ +
+ +

{{ $strings.LabelExample }}: {{ timeExample }}

@@ -293,6 +299,17 @@ export default { }, dateFormats() { return this.$store.state.globals.dateFormats + }, + timeFormats() { + return this.$store.state.globals.timeFormats + }, + dateExample() { + const date = new Date(2014, 2, 25) + return this.$formatJsDate(date, this.newServerSettings.dateFormat) + }, + timeExample() { + const date = new Date(2014, 2, 25, 17, 30, 0) + return this.$formatJsTime(date, this.newServerSettings.timeFormat) } }, methods: { @@ -420,4 +437,4 @@ export default { this.initServerSettings() } } - \ No newline at end of file + diff --git a/client/pages/config/library-stats.vue b/client/pages/config/library-stats.vue index 7a926e73..aa4200dc 100644 --- a/client/pages/config/library-stats.vue +++ b/client/pages/config/library-stats.vue @@ -60,6 +60,25 @@
+
+

{{ $strings.HeaderStatsLargestItems }}

+

{{ $strings.MessageNoItems }}

+ +
@@ -105,6 +124,13 @@ export default { if (!this.top10LongestItems.length) return 0 return this.top10LongestItems[0].duration }, + top10LargestItems() { + return this.libraryStats ? this.libraryStats.largestItems || [] : [] + }, + largestItemSize() { + if (!this.top10LargestItems.length) return 0 + return this.top10LargestItems[0].size + }, authorsWithCount() { return this.libraryStats ? this.libraryStats.authorsWithCount : [] }, @@ -135,4 +161,4 @@ export default { this.init() } } - \ No newline at end of file + diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index a3213c81..c31ce2cd 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -39,7 +39,7 @@

{{ $secondsToTimestamp(session.currentTime) }}

- +

{{ $dateDistanceFromNow(session.updatedAt) }}

@@ -105,6 +105,12 @@ export default { if (!this.userFilter) return null var user = this.users.find((u) => u.id === this.userFilter) return user ? user.username : null + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -149,7 +155,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: libraryItem.media.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: libraryItem.media.coverPath || null } @@ -266,4 +272,4 @@ export default { padding: 4px 8px; font-size: 0.75rem; } - \ No newline at end of file + diff --git a/client/pages/config/users/_id/index.vue b/client/pages/config/users/_id/index.vue index eb041258..ee136b43 100644 --- a/client/pages/config/users/_id/index.vue +++ b/client/pages/config/users/_id/index.vue @@ -79,12 +79,12 @@

{{ Math.floor(item.progress * 100) }}%

- +

{{ $dateDistanceFromNow(item.startedAt) }}

- +

{{ $dateDistanceFromNow(item.lastUpdate) }}

@@ -149,6 +149,12 @@ export default { latestSession() { if (!this.listeningSessions.sessions || !this.listeningSessions.sessions.length) return null return this.listeningSessions.sessions[0] + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue index 7377a91d..4dd4e643 100644 --- a/client/pages/config/users/_id/sessions.vue +++ b/client/pages/config/users/_id/sessions.vue @@ -46,7 +46,7 @@

{{ $secondsToTimestamp(session.currentTime) }}

- +

{{ $dateDistanceFromNow(session.updatedAt) }}

@@ -96,6 +96,12 @@ export default { }, userOnline() { return this.$store.getters['users/getIsUserOnline'](this.user.id) + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat + }, + timeFormat() { + return this.$store.state.serverSettings.timeFormat } }, methods: { @@ -140,7 +146,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: libraryItem.media.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: libraryItem.media.coverPath || null } @@ -252,4 +258,4 @@ export default { padding: 4px 8px; font-size: 0.75rem; } - \ No newline at end of file + diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index ef99cefb..86c1ea0e 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -25,7 +25,10 @@

- {{ title }} +
+ {{ title }} + +

{{ bookSubtitle }}

@@ -315,6 +318,9 @@ export default { isInvalid() { return this.libraryItem.isInvalid }, + isExplicit() { + return this.mediaMetadata.explicit || false + }, invalidAudioFiles() { if (!this.isBook) return [] return this.libraryItem.media.audioFiles.filter((af) => af.invalid) @@ -632,7 +638,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.audioFile.duration || null, coverPath: this.libraryItem.media.coverPath || null }) @@ -753,9 +759,8 @@ export default { } }, mounted() { - if (this.libraryItem.episodesDownloading) { - this.episodeDownloadsQueued = this.libraryItem.episodesDownloading || [] - } + this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || [] + this.episodesDownloading = this.libraryItem.episodesDownloading || [] // use this items library id as the current if (this.libraryId) { diff --git a/client/pages/library/_library/podcast/download-queue.vue b/client/pages/library/_library/podcast/download-queue.vue new file mode 100644 index 00000000..713ee869 --- /dev/null +++ b/client/pages/library/_library/podcast/download-queue.vue @@ -0,0 +1,140 @@ + + + diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index 43eff416..96c8ebf6 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -14,19 +14,36 @@
- {{ episode.podcast.metadata.title }} - +
+
+ {{ episode.podcast.metadata.title }} +
+ +

{{ $dateDistanceFromNow(episode.publishedAt) }}

-

{{ episode.title }}

+
+
#
+
{{ episode.season }}x
+
{{ episode.episode }}
+
+ +
+
{{ episode.title }}
+ +

{{ episode.subtitle }}

@@ -113,6 +130,9 @@ export default { if (i.episodeId) episodeIds[i.episodeId] = true }) return episodeIds + }, + dateFormat() { + return this.$store.state.serverSettings.dateFormat } }, methods: { @@ -156,7 +176,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: episode.podcast.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.duration || null, coverPath: episode.podcast.coverPath || null }) @@ -194,7 +214,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: episode.podcast.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date', + caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', duration: episode.duration || null, coverPath: episode.podcast.coverPath || null } @@ -206,4 +226,4 @@ export default { this.loadRecentEpisodes() } } - \ No newline at end of file + diff --git a/client/pages/library/_library/podcast/search.vue b/client/pages/library/_library/podcast/search.vue index 7bbf3de3..428441f9 100644 --- a/client/pages/library/_library/podcast/search.vue +++ b/client/pages/library/_library/podcast/search.vue @@ -5,13 +5,12 @@
- + {{ $strings.ButtonSubmit }} {{ $strings.ButtonUploadOPMLFile }}
-

{{ $strings.MessageNoPodcastsFound }}