diff --git a/.github/workflows/apply_comments.yaml b/.github/workflows/apply_comments.yaml new file mode 100644 index 000000000..69a7ce280 --- /dev/null +++ b/.github/workflows/apply_comments.yaml @@ -0,0 +1,55 @@ +name: Add issue comments by label +on: + issues: + types: + - labeled +jobs: + help-wanted: + if: github.event.label.name == 'help wanted' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Help wanted comment + run: gh issue comment "$NUMBER" --body "$BODY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} + BODY: > + This issue is not able to be completed due to limited bandwidth or access to the required test hardware. + + This issue is available for anyone to work on. + + + config-issue: + if: github.event.label.name == 'config-issue' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Config issue comment + run: gh issue close "$NUMBER" --reason "not planned" --comment "$BODY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} + BODY: > + After reviewing this issue, this appears to be a problem with your setup and not Audiobookshelf. This issue is being closed to keep the issue tracker focused on Audiobookshelf itself. Please reach out on the Audiobookshelf Discord for community support. + + Some common search terms to help you find the solution to your problem: + - Reverse proxy + - Enabling websockets + - SSL (https vs http) + - Configuring a static IP + - `localhost` versus IP address + - hairpin NAT + - VPN + - firewall ports + - public versus private network + - bridge versus host mode + - Docker networking + - DNS (such as EAI_AGAIN errors) + + After you have followed these steps, please post the solution or steps you followed to fix the problem to help others in the future, or show that it is a problem with Audiobookshelf so we can reopen the issue. + diff --git a/.github/workflows/lint-openapi.yml b/.github/workflows/lint-openapi.yml index 3c6072d8d..817e94b97 100644 --- a/.github/workflows/lint-openapi.yml +++ b/.github/workflows/lint-openapi.yml @@ -1,13 +1,15 @@ name: API linting -# Run on pull requests or pushes when there is a change to the OpenAPI file +# Run on pull requests or pushes when there is a change to any OpenAPI files in docs/ on: + pull_request: push: paths: - - docs/ - pull_request: - paths: - - docs/ + - 'docs/**' + +# This action only needs read permissions +permissions: + contents: read jobs: build: diff --git a/client/assets/fonts.css b/client/assets/fonts.css index 4e280dc93..c568ffa6e 100644 --- a/client/assets/fonts.css +++ b/client/assets/fonts.css @@ -2,14 +2,7 @@ font-family: 'Material Symbols Rounded'; font-style: normal; font-weight: 400; - src: url(~static/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2) format('woff2'); -} - -@font-face { - font-family: 'Material Symbols Outlined'; - font-style: normal; - font-weight: 400; - src: url(~static/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2) format('woff2'); + src: url(~static/fonts/MaterialSymbolsRounded.woff2) format('woff2'); } .material-symbols { @@ -32,26 +25,6 @@ 'FILL' 1 } -.material-symbols-outlined { - font-family: 'Material Symbols Outlined'; - font-weight: normal; - font-style: normal; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-smoothing: antialiased; - vertical-align: top; -} - -.material-symbols-outlined.fill { - font-variation-settings: - 'FILL' 1 -} - /* cyrillic-ext */ @font-face { font-family: 'Source Sans Pro'; diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 1ded2f7a4..19b8fe3c6 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -16,7 +16,7 @@
- cast + cast
@@ -26,19 +26,19 @@ - upload + - settings + @@ -47,7 +47,7 @@ {{ username }} - person +
@@ -264,7 +264,6 @@ export default { libraryItems.forEach((item) => { let subtitle = '' if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ') - else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ') queueItems.push({ libraryItemId: item.id, libraryId: item.libraryId, @@ -332,13 +331,13 @@ export default { libraryItemIds: this.selectedMediaItems.map((i) => i.id) }) .then(() => { - this.$toast.success('Batch delete success') + this.$toast.success(this.$strings.ToastBatchDeleteSuccess) this.$store.commit('globals/resetSelectedMediaItems', []) this.$eventBus.$emit('bookshelf_clear_selection') }) .catch((error) => { console.error('Batch delete failed', error) - this.$toast.error('Batch delete failed') + this.$toast.error(this.$strings.ToastBatchDeleteFailed) }) .finally(() => { this.$store.commit('setProcessingBatch', false) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 94e095c7c..00b7ee343 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -24,11 +24,11 @@

{{ $strings.ButtonPlaylists }}

- queue_music +

{{ $strings.ButtonCollections }}

- collections_bookmark +

{{ $strings.ButtonAuthors }}

@@ -159,6 +159,7 @@ export default { } this.addSubtitlesMenuItem(items) + this.addCollapseSubSeriesMenuItem(items) return items }, @@ -245,9 +246,6 @@ export default { isPodcastLibrary() { return this.currentLibraryMediaType === 'podcast' }, - isMusicLibrary() { - return this.currentLibraryMediaType === 'music' - }, isLibraryPage() { return this.page === '' }, @@ -280,7 +278,6 @@ export default { }, entityName() { if (this.isAlbumsPage) return 'Albums' - if (this.isMusicLibrary) return 'Tracks' if (this.isPodcastLibrary) return this.$strings.LabelPodcasts if (!this.page) return this.$strings.LabelBooks @@ -371,6 +368,21 @@ export default { } } }, + addCollapseSubSeriesMenuItem(items) { + if (this.selectedSeries && this.isBookLibrary && !this.isBatchSelecting) { + if (this.settings.collapseBookSeries) { + items.push({ + text: this.$strings.LabelExpandSubSeries, + action: 'expand-sub-series' + }) + } else { + items.push({ + text: this.$strings.LabelCollapseSubSeries, + action: 'collapse-sub-series' + }) + } + } + }, handleSubtitlesAction(action) { if (action === 'show-subtitles') { this.settings.showSubtitles = true @@ -397,6 +409,19 @@ export default { } return false }, + handleCollapseSubSeriesAction(action) { + if (action === 'collapse-sub-series') { + this.settings.collapseBookSeries = true + this.updateCollapseSubSeries() + return true + } + if (action === 'expand-sub-series') { + this.settings.collapseBookSeries = false + this.updateCollapseSubSeries() + return true + } + return false + }, contextMenuAction({ action }) { if (action === 'export-opml') { this.exportOPML() @@ -427,6 +452,8 @@ export default { this.markSeriesFinished() } else if (this.handleSubtitlesAction(action)) { return + } else if (this.handleCollapseSubSeriesAction(action)) { + return } }, showOpenSeriesRSSFeed() { @@ -442,11 +469,11 @@ export default { this.$axios .$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`) .then(() => { - this.$toast.success('Series re-added to continue listening') + this.$toast.success(this.$strings.ToastItemUpdateSuccess) }) .catch((error) => { console.error('Failed to re-add series to continue listening', error) - this.$toast.error('Failed to re-add series to continue listening') + this.$toast.error(this.$strings.ToastItemUpdateFailed) }) .finally(() => { this.processingSeries = false @@ -473,7 +500,7 @@ export default { }) if (!response) { console.error(`Author ${author.name} not found`) - this.$toast.error(`Author ${author.name} not found`) + this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name])) } else if (response.updated) { if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`) else console.log(`Author ${response.author.name} was updated (no image found)`) @@ -491,13 +518,13 @@ export default { this.$axios .$delete(`/api/libraries/${this.currentLibraryId}/issues`) .then(() => { - this.$toast.success('Removed library items with issues') + this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess) this.$router.push(`/library/${this.currentLibraryId}/bookshelf`) this.$store.dispatch('libraries/fetch', this.currentLibraryId) }) .catch((error) => { console.error('Failed to remove library items with issues', error) - this.$toast.error('Failed to remove library items with issues') + this.$toast.error(this.$strings.ToastRemoveItemsWithIssuesFailed) }) .finally(() => { this.processingIssues = false @@ -553,7 +580,7 @@ export default { updateCollapseSeries() { this.saveSettings() }, - updateCollapseBookSeries() { + updateCollapseSubSeries() { this.saveSettings() }, updateShowSubtitles() { diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index cbc768034..259e0c98b 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -1,10 +1,9 @@ @@ -170,7 +170,8 @@ export default { abridged: false }, appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'], - openMapOptions: false + openMapOptions: false, + itemsWithChanges: [] } }, computed: { @@ -221,9 +222,19 @@ export default { }, hasSelectedBatchUsage() { return Object.values(this.selectedBatchUsage).some((b) => !!b) + }, + hasChanges() { + return this.itemsWithChanges.length > 0 } }, methods: { + handleItemChange(itemChange) { + if (!itemChange.hasChanges) { + this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId) + } else if (!this.itemsWithChanges.includes(itemChange.libraryItemId)) { + this.itemsWithChanges.push(itemChange.libraryItemId) + } + }, blurBatchForm() { if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) { this.$refs.seriesSelect.forceBlur() @@ -283,38 +294,10 @@ export default { removedSeriesItem(item) {}, newNarratorItem(item) {}, removedNarratorItem(item) {}, - newTagItem(item) { - // if (item && !this.newTagItems.includes(item)) { - // this.newTagItems.push(item) - // } - }, - removedTagItem(item) { - // If newly added, remove if not used on any other items - // if (item && this.newTagItems.includes(item)) { - // var usedByOtherAb = this.libraryItemCopies.find((ab) => { - // return ab.tags && ab.tags.includes(item) - // }) - // if (!usedByOtherAb) { - // this.newTagItems = this.newTagItems.filter((t) => t !== item) - // } - // } - }, - newGenreItem(item) { - // if (item && !this.newGenreItems.includes(item)) { - // this.newGenreItems.push(item) - // } - }, - removedGenreItem(item) { - // If newly added, remove if not used on any other items - // if (item && this.newGenreItems.includes(item)) { - // var usedByOtherAb = this.libraryItemCopies.find((ab) => { - // return ab.book.genres && ab.book.genres.includes(item) - // }) - // if (!usedByOtherAb) { - // this.newGenreItems = this.newGenreItems.filter((t) => t !== item) - // } - // } - }, + newTagItem(item) {}, + removedTagItem(item) {}, + newGenreItem(item) {}, + removedGenreItem(item) {}, init() { // TODO: Better deep cloning of library items this.libraryItemCopies = this.libraryItems.map((li) => { @@ -366,7 +349,7 @@ export default { } } if (!updates.length) { - return this.$toast.warning('No updates were made') + return this.$toast.warning(this.$strings.ToastNoUpdatesNecessary) } console.log('Pushing updates', updates) @@ -376,6 +359,7 @@ export default { .then((data) => { this.isProcessing = false if (data.updates) { + this.itemsWithChanges = [] this.$toast.success(`Successfully updated ${data.updates} items`) this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`) } else { @@ -387,10 +371,28 @@ export default { this.$toast.error('Failed to batch update') this.isProcessing = false }) + }, + beforeUnload(e) { + if (!e || !this.hasChanges) return + e.preventDefault() + e.returnValue = '' + } + }, + beforeRouteLeave(to, from, next) { + if (this.hasChanges) { + next(false) + window.location = to.path + } else { + next() } }, mounted() { this.init() + + window.addEventListener('beforeunload', this.beforeUnload) + }, + beforeDestroy() { + window.removeEventListener('beforeunload', this.beforeUnload) } } @@ -406,4 +408,4 @@ export default { transform: translateY(-100%); transition: all 150ms ease-in 0s; } - \ No newline at end of file + diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index 44a92f2e9..0c64ddf6f 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -3,7 +3,7 @@
- folder + folder {{ $strings.LabelBackupLocation }}:
@@ -33,7 +33,7 @@
- schedule + schedule
{{ $strings.HeaderSchedule }}:
@@ -44,7 +44,7 @@
- event + event
{{ $strings.LabelNextBackupDate }}:
@@ -162,7 +162,7 @@ export default { }) .catch((error) => { console.error('Failed to save backup path', error) - const errorMsg = error.response?.data || 'Failed to save backup path' + const errorMsg = error.response?.data || this.$strings.ToastBackupPathUpdateFailed this.$toast.error(errorMsg) }) .finally(() => { @@ -171,11 +171,11 @@ export default { }, updateBackupsSettings() { if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) { - this.$toast.error('Invalid maximum backup size') + this.$toast.error(this.$strings.ToastBackupInvalidMaxSize) return } if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) { - this.$toast.error('Invalid number of backups to keep') + this.$toast.error(this.$strings.ToastBackupInvalidMaxKeep) return } const updatePayload = { diff --git a/client/pages/config/email.vue b/client/pages/config/email.vue index 3637e3124..212c51f31 100644 --- a/client/pages/config/email.vue +++ b/client/pages/config/email.vue @@ -109,7 +109,7 @@
-

No Devices

+

{{ $strings.MessageNoDevices }}

@@ -199,7 +199,7 @@ export default { }, deleteDeviceClick(device) { const payload = { - message: `Are you sure you want to delete e-reader device "${device.name}"?`, + message: this.$getString('MessageConfirmDeleteDevice', [device.name]), callback: (confirmed) => { if (confirmed) { this.deleteDevice(device) @@ -218,11 +218,10 @@ export default { .$post(`/api/emails/ereader-devices`, payload) .then((data) => { this.ereaderDevicesUpdated(data.ereaderDevices) - this.$toast.success('Device deleted') }) .catch((error) => { console.error('Failed to delete device', error) - this.$toast.error('Failed to delete device') + this.$toast.error(this.$strings.ToastRemoveFailed) }) .finally(() => { this.deletingDeviceName = null @@ -246,11 +245,11 @@ export default { this.$axios .$post('/api/emails/test') .then(() => { - this.$toast.success('Test Email Sent') + this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess) }) .catch((error) => { console.error('Failed to send test email', error) - const errorMsg = error.response.data || 'Failed to send test email' + const errorMsg = error.response.data || this.$strings.ToastDeviceTestEmailFailed this.$toast.error(errorMsg) }) .finally(() => { @@ -289,11 +288,11 @@ export default { this.newSettings = { ...data.settings } - this.$toast.success('Email settings updated') + this.$toast.success(this.$strings.ToastEmailSettingsUpdateSuccess) }) .catch((error) => { console.error('Failed to update email settings', error) - this.$toast.error('Failed to update email settings') + this.$toast.error(this.$strings.ToastEmailSettingsUpdateFailed) }) .finally(() => { this.savingSettings = false diff --git a/client/pages/config/item-metadata-utils/genres.vue b/client/pages/config/item-metadata-utils/genres.vue index 5a61d51a6..e041244cb 100644 --- a/client/pages/config/item-metadata-utils/genres.vue +++ b/client/pages/config/item-metadata-utils/genres.vue @@ -130,7 +130,7 @@ export default { }) .catch((error) => { console.error('Failed to rename genre', error) - this.$toast.error('Failed to rename genre') + this.$toast.error(this.$strings.ToastRenameFailed) }) .finally(() => { this.loading = false @@ -147,7 +147,7 @@ export default { }) .catch((error) => { console.error('Failed to remove genre', error) - this.$toast.error('Failed to remove genre') + this.$toast.error(this.$strings.ToastRemoveFailed) }) .finally(() => { this.loading = false diff --git a/client/pages/config/item-metadata-utils/tags.vue b/client/pages/config/item-metadata-utils/tags.vue index a98f39b4e..0e14f97c8 100644 --- a/client/pages/config/item-metadata-utils/tags.vue +++ b/client/pages/config/item-metadata-utils/tags.vue @@ -126,7 +126,7 @@ export default { }) .catch((error) => { console.error('Failed to rename tag', error) - this.$toast.error('Failed to rename tag') + this.$toast.error(this.$strings.ToastRenameFailed) }) .finally(() => { this.loading = false @@ -143,7 +143,7 @@ export default { }) .catch((error) => { console.error('Failed to remove tag', error) - this.$toast.error('Failed to remove tag') + this.$toast.error(this.$strings.ToastRemoveFailed) }) .finally(() => { this.loading = false diff --git a/client/pages/config/notifications.vue b/client/pages/config/notifications.vue index ad346a5d7..24ea6a6cc 100644 --- a/client/pages/config/notifications.vue +++ b/client/pages/config/notifications.vue @@ -105,12 +105,12 @@ export default { } if (isNaN(this.maxNotificationQueue) || this.maxNotificationQueue <= 0) { - this.$toast.error('Max notification queue must be >= 0') + this.$toast.error(this.$strings.ToastNotificationQueueMaximum) return false } if (isNaN(this.maxFailedAttempts) || this.maxFailedAttempts <= 0) { - this.$toast.error('Max failed attempts must be >= 0') + this.$toast.error(this.$strings.ToastNotificationFailedMaximum) return false } @@ -128,11 +128,11 @@ export default { this.$axios .$patch('/api/notifications', updatePayload) .then(() => { - this.$toast.success('Notification settings updated') + this.$toast.success(this.$strings.ToastNotificationSettingsUpdateSuccess) }) .catch((error) => { console.error('Failed to update notification settings', error) - this.$toast.error('Failed to update notification settings') + this.$toast.error(this.$strings.ToastNotificationSettingsUpdateFailed) }) .finally(() => { this.savingSettings = false diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index edb14cd23..59ff75587 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -290,7 +290,6 @@ export default { this.$axios .$post(`/api/sessions/batch/delete`, payload) .then(() => { - this.$toast.success('Sessions removed') if (isAllSessions) { // If all sessions were removed from the current page then go to the previous page if (this.currentPage > 0) { @@ -303,7 +302,7 @@ export default { } }) .catch((error) => { - const errorMsg = error.response?.data || 'Failed to remove sessions' + const errorMsg = error.response?.data || this.$strings.ToastRemoveFailed this.$toast.error(errorMsg) }) .finally(() => { @@ -358,12 +357,13 @@ export default { }) if (!libraryItem) { - this.$toast.error('Failed to get library item') + this.$toast.error(this.$strings.ToastFailedToLoadData) this.processingGoToTimestamp = false return } if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { - this.$toast.error('Failed to get podcast episode') + console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes) + this.$toast.error(this.$strings.ToastFailedToLoadData) this.processingGoToTimestamp = false return } @@ -377,7 +377,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: libraryItem.media.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', + caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate, duration: episode.audioFile.duration || null, coverPath: libraryItem.media.coverPath || null } diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue index f51fc8dd4..c4df6df9a 100644 --- a/client/pages/config/stats.vue +++ b/client/pages/config/stats.vue @@ -20,7 +20,7 @@

{{ $formatNumber(totalDaysListened) }}

@@ -30,7 +30,7 @@

{{ $formatNumber(totalMinutesListening) }}

diff --git a/client/pages/config/users/_id/sessions.vue b/client/pages/config/users/_id/sessions.vue index 8e7ebfb86..6b4756776 100644 --- a/client/pages/config/users/_id/sessions.vue +++ b/client/pages/config/users/_id/sessions.vue @@ -127,12 +127,13 @@ export default { }) if (!libraryItem) { - this.$toast.error('Failed to get library item') + this.$toast.error(this.$strings.ToastFailedToLoadData) this.processingGoToTimestamp = false return } if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { - this.$toast.error('Failed to get podcast episode') + console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes) + this.$toast.error(this.$strings.ToastFailedToLoadData) this.processingGoToTimestamp = false return } @@ -146,7 +147,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: libraryItem.media.metadata.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', + caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate, duration: episode.audioFile.duration || null, coverPath: libraryItem.media.coverPath || null } diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index f6ffe7642..7cd616a06 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -39,16 +39,11 @@ >, - +

{{ $getString('LabelByAuthor', [podcastAuthor]) }}

+

+ by {{ author.name }} +

+

by Unknown

@@ -80,14 +75,14 @@

{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}

- close +
- + {{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }} @@ -106,10 +101,10 @@ - + - + @@ -121,7 +116,7 @@ @@ -129,9 +124,7 @@

{{ description }}

- +
@@ -222,12 +215,6 @@ export default { isPodcast() { return this.libraryItem.mediaType === 'podcast' }, - isVideo() { - return this.libraryItem.mediaType === 'video' - }, - isMusic() { - return this.libraryItem.mediaType === 'music' - }, isMissing() { return this.libraryItem.isMissing }, @@ -242,8 +229,6 @@ export default { }, showPlayButton() { if (this.isMissing || this.isInvalid) return false - if (this.isMusic) return !!this.audioFile - if (this.isVideo) return !!this.videoFile if (this.isPodcast) return this.podcastEpisodes.length return this.tracks.length }, @@ -294,9 +279,6 @@ export default { authors() { return this.mediaMetadata.authors || [] }, - musicArtists() { - return this.mediaMetadata.artists || [] - }, series() { return this.mediaMetadata.series || [] }, @@ -311,7 +293,7 @@ export default { }) }, duration() { - if (!this.tracks.length && !this.audioFile) return 0 + if (!this.tracks.length) return 0 return this.media.duration }, libraryFiles() { @@ -323,18 +305,10 @@ export default { ebookFile() { return this.media.ebookFile }, - videoFile() { - return this.media.videoFile - }, - audioFile() { - // Music track - return this.media.audioFile - }, description() { return this.mediaMetadata.description || '' }, userMediaProgress() { - if (this.isMusic) return null return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) }, userIsFinished() { @@ -486,23 +460,23 @@ export default { this.$axios .$get(`/api/podcasts/${this.libraryItemId}/clear-queue`) .then(() => { - this.$toast.success('Episode download queue cleared') + this.$toast.success(this.$strings.ToastEpisodeDownloadQueueClearSuccess) this.episodeDownloadQueued = [] }) .catch((error) => { console.error('Failed to clear queue', error) - this.$toast.error('Failed to clear queue') + this.$toast.error(this.$strings.ToastEpisodeDownloadQueueClearFailed) }) } }, async findEpisodesClick() { if (!this.mediaMetadata.feedUrl) { - return this.$toast.error('Podcast does not have an RSS Feed') + return this.$toast.error(this.$strings.ToastNoRSSFeed) } this.fetchingRSSFeed = true var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => { console.error('Failed to get feed', error) - this.$toast.error('Failed to get podcast feed') + this.$toast.error(this.$strings.ToastPodcastGetFeedFailed) return null }) this.fetchingRSSFeed = false @@ -511,7 +485,7 @@ export default { console.log('Podcast feed', payload) const podcastfeed = payload.podcast if (!podcastfeed.episodes || !podcastfeed.episodes.length) { - this.$toast.info('No episodes found in RSS feed') + this.$toast.info(this.$strings.ToastPodcastNoEpisodesInFeed) return } @@ -580,7 +554,7 @@ export default { episodeId: episode.id, title: episode.title, subtitle: this.title, - caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', + caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate, duration: episode.audioFile.duration || null, coverPath: this.libraryItem.media.coverPath || null }) @@ -624,13 +598,12 @@ export default { }, clearProgressClick() { if (!this.userMediaProgress) return - if (confirm(`Are you sure you want to reset your progress?`)) { + if (confirm(this.$strings.MessageConfirmResetProgress)) { this.resettingProgress = true this.$axios .$delete(`/api/me/progress/${this.userMediaProgress.id}`) .then(() => { console.log('Progress reset complete') - this.$toast.success(`Your progress was reset`) this.resettingProgress = false }) .catch((error) => { @@ -724,12 +697,12 @@ export default { this.$axios .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .then(() => { - this.$toast.success('Item deleted') + this.$toast.success(this.$strings.ToastItemDeletedSuccess) this.$router.replace(`/library/${this.libraryId}`) }) .catch((error) => { console.error('Failed to delete item', error) - this.$toast.error('Failed to delete item') + this.$toast.error(this.$strings.ToastItemDeleteFailed) }) } }, diff --git a/client/pages/library/_library/authors/index.vue b/client/pages/library/_library/authors/index.vue index 75e9f083e..9820fbce6 100644 --- a/client/pages/library/_library/authors/index.vue +++ b/client/pages/library/_library/authors/index.vue @@ -61,6 +61,8 @@ export default { const bDesc = this.authorSortDesc ? -1 : 1 return this.authors.sort((a, b) => { if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') { + // Fallback to name sort if equal + if (a[sortProp] === b[sortProp]) return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) * bDesc return a[sortProp] > b[sortProp] ? bDesc : -bDesc } return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc diff --git a/client/pages/library/_library/narrators.vue b/client/pages/library/_library/narrators.vue index 22d583c70..e2a45da44 100644 --- a/client/pages/library/_library/narrators.vue +++ b/client/pages/library/_library/narrators.vue @@ -138,7 +138,7 @@ export default { }) .catch((error) => { console.error('Failed to remove narrator', error) - this.$toast.error('Failed to remove narrator') + this.$toast.error(this.$strings.ToastRemoveFailed) this.loading = false }) }, @@ -158,4 +158,4 @@ export default { }, beforeDestroy() {} } - \ No newline at end of file + diff --git a/client/pages/library/_library/podcast/download-queue.vue b/client/pages/library/_library/podcast/download-queue.vue index 49b4d4da6..777ddfc16 100644 --- a/client/pages/library/_library/podcast/download-queue.vue +++ b/client/pages/library/_library/podcast/download-queue.vue @@ -111,7 +111,7 @@ export default { this.processing = true const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => { console.error('Failed to get download queue', error) - this.$toast.error('Failed to get download queue') + this.$toast.error(this.$strings.ToastFailedToLoadData) return null }) this.processing = false diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue index e0637578c..c663b8f59 100644 --- a/client/pages/library/_library/podcast/latest.vue +++ b/client/pages/library/_library/podcast/latest.vue @@ -48,7 +48,7 @@

-