diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 766273e7..968e97c4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -12,4 +12,4 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # Move tone executable to appropriate directory -COPY --from=sandreas/tone:v0.1.2 /usr/local/bin/tone /usr/local/bin/ +COPY --from=sandreas/tone:v0.1.5 /usr/local/bin/tone /usr/local/bin/ diff --git a/Dockerfile b/Dockerfile index 5936ee6e..fe2e0059 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN npm ci && npm cache clean --force RUN npm run generate ### STAGE 1: Build server ### -FROM sandreas/tone:v0.1.2 AS tone +FROM sandreas/tone:v0.1.5 AS tone FROM node:16-alpine ENV NODE_ENV=production diff --git a/build/debian/DEBIAN/preinst b/build/debian/DEBIAN/preinst index be2d8b79..ad6657ef 100644 --- a/build/debian/DEBIAN/preinst +++ b/build/debian/DEBIAN/preinst @@ -50,7 +50,7 @@ install_ffmpeg() { echo "Starting FFMPEG Install" WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz" - WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.2/tone-0.1.2-linux-x64.tar.gz --output-document=tone-0.1.2-linux-x64.tar.gz" + WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.5/tone-0.1.5-linux-x64.tar.gz --output-document=tone-0.1.5-linux-x64.tar.gz" if ! cd "$FFMPEG_INSTALL_DIR"; then echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR" @@ -66,8 +66,8 @@ install_ffmpeg() { # Temp downloading tone library to the ffmpeg dir echo "Getting tone.." $WGET_TONE - tar xvf tone-0.1.2-linux-x64.tar.gz --strip-components=1 - rm tone-0.1.2-linux-x64.tar.gz + tar xvf tone-0.1.5-linux-x64.tar.gz --strip-components=1 + rm tone-0.1.5-linux-x64.tar.gz echo "Good to go on Ffmpeg (& tone)... hopefully" } diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index fe1d5df5..ca070e18 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -28,6 +28,9 @@

{{ $strings[shelf.labelStringKey] }}

+ +

{{ $strings[shelf.labelStringKey] }}

+
@@ -185,8 +188,8 @@ export default { this.shelves = categories }, async setShelvesFromSearch() { - var shelves = [] - if (this.results.books && this.results.books.length) { + const shelves = [] + if (this.results.books?.length) { shelves.push({ id: 'books', label: 'Books', @@ -196,7 +199,7 @@ export default { }) } - if (this.results.podcasts && this.results.podcasts.length) { + if (this.results.podcasts?.length) { shelves.push({ id: 'podcasts', label: 'Podcasts', @@ -206,7 +209,7 @@ export default { }) } - if (this.results.series && this.results.series.length) { + if (this.results.series?.length) { shelves.push({ id: 'series', label: 'Series', @@ -221,7 +224,7 @@ export default { }) }) } - if (this.results.tags && this.results.tags.length) { + if (this.results.tags?.length) { shelves.push({ id: 'tags', label: 'Tags', @@ -236,7 +239,7 @@ export default { }) }) } - if (this.results.authors && this.results.authors.length) { + if (this.results.authors?.length) { shelves.push({ id: 'authors', label: 'Authors', @@ -250,6 +253,20 @@ export default { }) }) } + if (this.results.narrators?.length) { + shelves.push({ + id: 'narrators', + label: 'Narrators', + labelStringKey: 'LabelNarrators', + type: 'narrators', + entities: this.results.narrators.map((n) => { + return { + ...n, + type: 'narrator' + } + }) + }) + } this.shelves = shelves }, scan() { diff --git a/client/components/app/BookShelfRow.vue b/client/components/app/BookShelfRow.vue index bd97d56a..60632db7 100644 --- a/client/components/app/BookShelfRow.vue +++ b/client/components/app/BookShelfRow.vue @@ -41,6 +41,11 @@ +
+ +
@@ -88,6 +93,7 @@ export default { return this.bookCoverWidth * this.bookCoverAspectRatio }, shelfHeight() { + if (this.shelf.type === 'narrators') return 148 return this.bookCoverHeight + 48 }, paddingLeft() { diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index a3879279..57599877 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -189,6 +189,9 @@ export default { currentLibraryId() { return this.$store.state.libraries.currentLibraryId }, + libraryProvider() { + return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' + }, currentLibraryMediaType() { return this.$store.getters['libraries/getCurrentLibraryMediaType'] }, @@ -323,7 +326,11 @@ export default { const payload = {} if (author.asin) payload.asin = author.asin else payload.q = author.name - console.log('Payload', payload, 'author', author) + + payload.region = 'us' + if (this.libraryProvider.startsWith('audible.')) { + payload.region = this.libraryProvider.split('.').pop() || 'us' + } this.$eventBus.$emit(`searching-author-${author.id}`, true) diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index ce4f0d3b..db4e7e9a 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -77,6 +77,12 @@ export default { }, userCanUpdate() { return this.$store.getters['user/getUserCanUpdate'] + }, + currentLibraryId() { + return this.$store.state.libraries.currentLibraryId + }, + libraryProvider() { + return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' } }, methods: { @@ -92,6 +98,11 @@ export default { if (this.asin) payload.asin = this.asin else payload.q = this.name + payload.region = 'us' + if (this.libraryProvider.startsWith('audible.')) { + payload.region = this.libraryProvider.split('.').pop() || 'us' + } + var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, payload).catch((error) => { console.error('Failed', error) return null diff --git a/client/components/cards/ItemSearchCard.vue b/client/components/cards/ItemSearchCard.vue index f29888e5..1784db0f 100644 --- a/client/components/cards/ItemSearchCard.vue +++ b/client/components/cards/ItemSearchCard.vue @@ -10,7 +10,7 @@

by {{ authorName }}

-

+
@@ -67,12 +67,13 @@ export default { // but with removing commas periods etc this is no longer plausible const html = this.matchText - if (this.matchKey === 'episode') return `

Episode: ${html}

` - if (this.matchKey === 'tags') return `

Tags: ${html}

` + if (this.matchKey === 'episode') return `

${this.$strings.LabelEpisode}: ${html}

` + if (this.matchKey === 'tags') return `

${this.$strings.LabelTags}: ${html}

` if (this.matchKey === 'authors') return `by ${html}` if (this.matchKey === 'isbn') return `

ISBN: ${html}

` if (this.matchKey === 'asin') return `

ASIN: ${html}

` - if (this.matchKey === 'series') return `

Series: ${html}

` + if (this.matchKey === 'series') return `

${this.$strings.LabelSeries}: ${html}

` + if (this.matchKey === 'narrators') return `

${this.$strings.LabelNarrator}: ${html}

` return `${html}` } }, diff --git a/client/components/cards/NarratorCard.vue b/client/components/cards/NarratorCard.vue new file mode 100644 index 00000000..9d3e5a30 --- /dev/null +++ b/client/components/cards/NarratorCard.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/client/components/cards/NarratorSearchCard.vue b/client/components/cards/NarratorSearchCard.vue new file mode 100644 index 00000000..e147e079 --- /dev/null +++ b/client/components/cards/NarratorSearchCard.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/client/components/controls/GlobalSearch.vue b/client/components/controls/GlobalSearch.vue index 670eb6e7..3dbebf1c 100644 --- a/client/components/controls/GlobalSearch.vue +++ b/client/components/controls/GlobalSearch.vue @@ -63,6 +63,15 @@ + +

{{ $strings.LabelNarrators }}

+ @@ -84,6 +93,7 @@ export default { authorResults: [], seriesResults: [], tagResults: [], + narratorResults: [], searchTimeout: null, lastSearch: null } @@ -114,6 +124,7 @@ export default { this.authorResults = [] this.seriesResults = [] this.tagResults = [] + this.narratorResults = [] this.showMenu = false this.isFetching = false this.isTyping = false @@ -142,7 +153,7 @@ export default { } this.isFetching = true - var searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => { + const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => { console.error('Search error', error) return [] }) @@ -155,6 +166,7 @@ export default { this.authorResults = searchResults.authors || [] this.seriesResults = searchResults.series || [] this.tagResults = searchResults.tags || [] + this.narratorResults = searchResults.narrators || [] this.isFetching = false if (!this.showMenu) { diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue index b1cdabf0..070baab4 100644 --- a/client/components/modals/AccountModal.vue +++ b/client/components/modals/AccountModal.vue @@ -6,7 +6,7 @@
-
+
@@ -96,7 +96,14 @@
- +
+ +
+

{{ $strings.LabelInvert }}

+ +
+
+
@@ -185,6 +192,9 @@ export default { value: t } }) + }, + tagsSelectionText() { + return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser } }, methods: { @@ -193,8 +203,11 @@ export default { if (this.$refs.modal) this.$refs.modal.setHide() }, accessAllTagsToggled(val) { - if (val && this.newUser.itemTagsAccessible.length) { - this.newUser.itemTagsAccessible = [] + if (val) { + if (this.newUser.itemTagsSelected?.length) { + this.newUser.itemTagsSelected = [] + } + this.newUser.permissions.selectedTagsNotAccessible = false } }, fetchAllTags() { @@ -226,7 +239,7 @@ export default { this.$toast.error('Must select at least one library') return } - if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsAccessible.length) { + if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) { this.$toast.error('Must select at least one tag') return } @@ -307,12 +320,12 @@ export default { delete: type === 'admin', upload: type === 'admin', accessAllLibraries: true, - accessAllTags: true + accessAllTags: true, + selectedTagsNotAccessible: false } }, init() { this.fetchAllTags() - this.isNew = !this.account if (this.account) { this.newUser = { @@ -322,9 +335,10 @@ export default { isActive: this.account.isActive, permissions: { ...this.account.permissions }, librariesAccessible: [...(this.account.librariesAccessible || [])], - itemTagsAccessible: [...(this.account.itemTagsAccessible || [])] + itemTagsSelected: [...(this.account.itemTagsSelected || [])] } } else { + this.fetchAllTags() this.newUser = { username: null, password: null, @@ -336,7 +350,8 @@ export default { delete: false, upload: false, accessAllLibraries: true, - accessAllTags: true + accessAllTags: true, + selectedTagsNotAccessible: false }, librariesAccessible: [] } diff --git a/client/components/modals/authors/EditModal.vue b/client/components/modals/authors/EditModal.vue index f2430390..40292dca 100644 --- a/client/components/modals/authors/EditModal.vue +++ b/client/components/modals/authors/EditModal.vue @@ -85,6 +85,12 @@ export default { }, title() { return this.$strings.HeaderUpdateAuthor + }, + currentLibraryId() { + return this.$store.state.libraries.currentLibraryId + }, + libraryProvider() { + return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' } }, methods: { @@ -151,6 +157,11 @@ export default { if (this.authorCopy.asin) payload.asin = this.authorCopy.asin else payload.q = this.authorCopy.name + payload.region = 'us' + if (this.libraryProvider.startsWith('audible.')) { + payload.region = this.libraryProvider.split('.').pop() || 'us' + } + var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, payload).catch((error) => { console.error('Failed', error) return null diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index 3cbad218..ab63c7e8 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -49,13 +49,13 @@
-
+
-
+
{{ $strings.ButtonSearch }} @@ -128,7 +128,7 @@ export default { }, providers() { if (this.isPodcast) return this.$store.state.scanners.podcastProviders - return this.$store.state.scanners.providers + return [...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders] }, searchTitleLabel() { if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 8cf49bbd..58c52df8 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -115,7 +115,7 @@
- +

{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}

@@ -300,6 +300,12 @@ export default { }, isPodcast() { return this.mediaType == 'podcast' + }, + genres() { + const filterData = this.$store.state.libraries.filterData || {} + const currentGenres = filterData.genres || [] + const selectedMatchGenres = this.selectedMatch.genres || [] + return [...new Set([...currentGenres ,...selectedMatchGenres])] } }, methods: { diff --git a/client/components/modals/notification/NotificationEditModal.vue b/client/components/modals/notification/NotificationEditModal.vue index 55264f03..2fc10ce6 100644 --- a/client/components/modals/notification/NotificationEditModal.vue +++ b/client/components/modals/notification/NotificationEditModal.vue @@ -10,7 +10,7 @@
- + @@ -103,6 +103,8 @@ export default { if (this.$refs.modal) this.$refs.modal.setHide() }, submitForm() { + this.$refs.urlsInput?.forceBlur() + if (!this.newNotification.urls.length) { this.$toast.error('Must enter an Apprise URL') return diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 5092d8b1..9c199325 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -6,7 +6,7 @@
-
+
@@ -16,12 +16,12 @@ v-for="(episode, index) in episodesList" :key="index" class="relative" - :class="itemEpisodeMap[episode.enclosure.url?.split('?')[0]] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'" - @click="toggleSelectEpisode(index, episode)" + :class="itemEpisodeMap[episode.cleanUrl] ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'" + @click="toggleSelectEpisode(episode)" >
- download_done - + download_done +
@@ -63,6 +63,7 @@ export default { data() { return { processing: false, + episodesCleaned: [], selectedEpisodes: {}, selectAll: false, search: null, @@ -92,7 +93,7 @@ export default { return this.libraryItem.media.metadata.title || 'Unknown' }, allDownloaded() { - return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) + return !this.episodesCleaned.some((episode) => !this.itemEpisodeMap[episode.cleanUrl]) }, episodesSelected() { return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key]) @@ -113,7 +114,7 @@ export default { return map }, episodesList() { - return this.episodes.filter((episode) => { + return this.episodesCleaned.filter((episode) => { if (!this.searchText) return true return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText)) }) @@ -131,31 +132,29 @@ export default { }, 500) }, toggleSelectAll(val) { - for (let i = 0; i < this.episodes.length; i++) { - const episode = this.episodes[i] - if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) this.selectedEpisodes[String(i)] = false - else this.$set(this.selectedEpisodes, String(i), val) + for (const episode of this.episodesCleaned) { + if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false + else this.$set(this.selectedEpisodes, episode.cleanUrl, val) } }, checkSetIsSelectedAll() { - for (let i = 0; i < this.episodes.length; i++) { - const episode = this.episodes[i] - if (!this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]] && !this.selectedEpisodes[String(i)]) { + for (const episode of this.episodesCleaned) { + if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) { this.selectAll = false return } } this.selectAll = true }, - toggleSelectEpisode(index, episode) { + toggleSelectEpisode(episode) { if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return - this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)]) + this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl]) this.checkSetIsSelectedAll() }, submit() { var episodesToDownload = [] if (this.episodesSelected.length) { - episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)]) + episodesToDownload = this.episodesSelected.map((cleanUrl) => this.episodesCleaned.find((ep) => ep.cleanUrl == cleanUrl)) } var payloadSize = JSON.stringify(episodesToDownload).length @@ -185,7 +184,15 @@ export default { }) }, init() { - this.episodes.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1)) + this.episodesCleaned = this.episodes + .filter((ep) => ep.enclosure?.url) + .map((_ep) => { + return { + ..._ep, + cleanUrl: _ep.enclosure.url.split('?')[0] + } + }) + this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1)) this.selectAll = false this.selectedEpisodes = {} } diff --git a/client/components/modals/podcast/tabs/EpisodeDetails.vue b/client/components/modals/podcast/tabs/EpisodeDetails.vue index debf9155..d7130eba 100644 --- a/client/components/modals/podcast/tabs/EpisodeDetails.vue +++ b/client/components/modals/podcast/tabs/EpisodeDetails.vue @@ -31,9 +31,10 @@ {{ $strings.ButtonSave }}
-
-

Episode URL from RSS feed

- {{ enclosureUrl }} +
+ + +

Episode not linked to RSS feed episode

diff --git a/client/components/widgets/NarratorsSlider.vue b/client/components/widgets/NarratorsSlider.vue new file mode 100644 index 00000000..77302f6f --- /dev/null +++ b/client/components/widgets/NarratorsSlider.vue @@ -0,0 +1,100 @@ + + + \ No newline at end of file diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 8c9316fa..b201fc4a 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -569,6 +569,7 @@ export default { changeLanguage(code) { console.log('Changed lang', code) this.currentLang = code + document.documentElement.lang = code } }, beforeMount() { @@ -593,6 +594,11 @@ export default { this.$toast.error(this.$route.query.error) this.$router.replace(this.$route.path) } + + // Set lang on HTML tag + if (this.$languageCodes?.current) { + document.documentElement.lang = this.$languageCodes.current + } }, beforeDestroy() { this.$eventBus.$off('change-lang', this.changeLanguage) diff --git a/client/package-lock.json b/client/package-lock.json index 0e890c9c..b995afe0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.2.18", + "version": "2.2.19", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.2.18", + "version": "2.2.19", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index dcacf68f..25579ab7 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.2.18", + "version": "2.2.19", "description": "Self-hosted audiobook and podcast client", "main": "index.js", "scripts": { diff --git a/client/pages/library/_library/search.vue b/client/pages/library/_library/search.vue index b338323f..2f43d202 100644 --- a/client/pages/library/_library/search.vue +++ b/client/pages/library/_library/search.vue @@ -11,27 +11,27 @@