Compare commits

..

50 Commits

Author SHA1 Message Date
Aram Becker 7d0401f463 CI: update docker image for nuxt server target 2023-05-01 21:33:01 +02:00
Aram Becker eecd8be78d Update: run client from server and proxy requests 2023-05-01 21:32:56 +02:00
Aram Becker 31dc10ba51 Add: build client as server instead of static 2023-05-01 21:32:51 +02:00
Aram Becker 883cb46481 Merge commit '5286b533347477ae47d4e56f086285019e8095c1' into feature/nuxt-target-server 2023-04-30 16:15:01 +02:00
advplyr 5286b53334 Add:Progress bar on series covers #1734 2023-04-29 16:26:56 -05:00
advplyr 4db26f9f79 Add:Log user and ip on successful login #1740 2023-04-28 16:16:47 -05:00
advplyr ff8a58c7bc Remove log about not modifying permissions 2023-04-28 16:08:57 -05:00
advplyr 6f67c7bfa2 Merge pull request #1686 from divyangbw/feat-user-access-by-tag-enhancement
Invert Tag Selection
2023-04-27 17:20:16 -05:00
advplyr e9f5bd9bfe Merge branch 'master' into feat-user-access-by-tag-enhancement 2023-04-27 17:18:56 -05:00
advplyr 56e213d654 Update itemTagsSelected migration 2023-04-27 17:18:54 -05:00
advplyr 98323de64c Merge pull request #1736 from divyangbw/fix-fr-json-corrupted
Remove what seems to be a paste error in the french translations
2023-04-27 16:44:13 -05:00
Divyang Joshi 4a13712b1c fix: Remove what seems to be a paste error in the french translations 2023-04-27 17:07:09 -04:00
Divyang Joshi 0387436111 feat: add support for inverting the selection on libraries and tags 2023-04-27 17:02:15 -04:00
advplyr 7685ead000 Merge pull request #1729 from apineiro97/master
update es translation
2023-04-27 04:46:39 -05:00
advplyr 8665d66923 Merge pull request #1730 from Hallo951/master
Update german strings
2023-04-27 04:46:15 -05:00
Hallo951 9a808602c4 Update german strings 2023-04-27 10:41:06 +02:00
Arturo Pineiro 813e553dbb update es translation 2023-04-26 18:20:46 -07:00
advplyr be050a7d57 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-04-26 18:15:55 -05:00
advplyr 065675697d Fix:Catch exception when failing to download podcast episodes 2023-04-26 18:15:50 -05:00
advplyr 8f6832fc2e Merge pull request #1727 from tomazed/translation-fr
update on pending translation
2023-04-26 10:50:54 -05:00
Tomazed bdb154a6e5 update on pending translation 2023-04-26 17:39:15 +02:00
advplyr f557289274 Update:Set lang in HTML tag #1399 2023-04-25 19:00:57 -05:00
advplyr a5627a1b52 Add:Search for narrators #1495 2023-04-24 18:25:30 -05:00
advplyr 33f20d54cc Updated docker template xml 2023-04-23 17:08:36 -05:00
advplyr dadd41cb5c Fix:Podcast episode quick match crash #1711 2023-04-21 17:49:25 -05:00
advplyr 35e27e4f61 Merge pull request #1710 from Weldawadyathink/audiobook-covers-2
Add AudiobookCovers.com metadata provider
2023-04-21 16:17:48 -05:00
advplyr 84839bea44 Cleanup audiobookcovers.com addition 2023-04-21 16:17:52 -05:00
Spenser Bushey 1342897858 Removed useless comments 2023-04-20 16:39:04 -07:00
advplyr c32efb8db8 Fix:Podcast episode search modal search filter #1699 2023-04-20 17:51:06 -05:00
Spenser Bushey f9ed412e4e Add AudiobookCovers.com metadata provider
AudiobookCovers.com acts as a cover-only metadata provider, therefore will only show up in the covers selector.
2023-04-19 22:13:52 -07:00
advplyr 6ae3ad508e Readme update 2023-04-19 18:03:43 -05:00
advplyr 24af702b41 Merge pull request #1707 from coldshouldermedia/master
Updated SWAG Reverse Proxy guide
2023-04-19 17:59:59 -05:00
advplyr a57ff20f35 Merge pull request #1692 from divyangbw/fix-show-all-genres-on-match-tab
fix: Make sure all existing genres also show up on the match tab
2023-04-19 17:39:04 -05:00
advplyr 39e710deb1 Merge pull request #1706 from ajaxbits/master
Update:Fix filename issue in tone, bump to v0.1.5
2023-04-19 17:22:24 -05:00
advplyr 3b6fa73ac0 Update tone in debian package to v0.1.5 2023-04-19 17:22:25 -05:00
coldshouldermedia e2dd66d450 Updated SWAG Reverse Proxy guide 2023-04-19 16:19:29 -06:00
Alex Jackson b1b53a1eae Update:Bump tone version in devcontainer to v0.1.5 2023-04-19 16:40:46 -05:00
Alex Jackson 6f73345f39 Merge branch 'advplyr:master' into master 2023-04-19 16:24:21 -05:00
Alex Jackson c7b4b3bd3e Update:Bump tone version
Addresses #1703. Paths with quotations were not handled by tone<v0.1.3.
2023-04-19 16:22:15 -05:00
advplyr 98d543e3e5 Merge pull request #1695 from jkuehnemundt/de-lang-fix
Improve German (de.json) translation
2023-04-18 18:07:43 -05:00
advplyr 4de4e958a0 Merge pull request #1684 from ThinkSalat/bugfix/notification-url-blur
blur the url input when clicking submit to add info currently in input
2023-04-18 18:06:28 -05:00
advplyr cc5e92ec8e Update client/components/modals/notification/NotificationEditModal.vue 2023-04-18 18:06:22 -05:00
Jannik Kühnemundt 6cb9dfaa85 Update de.json 2023-04-18 17:55:59 +02:00
Jannik Kühnemundt 8790166ac1 Fix further translation 2023-04-18 16:37:34 +02:00
Divyang Joshi 3b97e2146d fix: Make sure all existing genres also show up on the match tab 2023-04-17 20:09:59 -05:00
advplyr 0bb1cf002d Fix:Crash when podcasts put empty spaces with episode file path in RSS feed #1650 2023-04-17 17:03:58 -05:00
advplyr 307c7ebc9d Merge pull request #1688 from Machou/patch-1
Update fr.json
2023-04-17 16:35:31 -05:00
Jannik Kühnemundt cc1b41995d Fixed german translation 2023-04-17 23:17:40 +02:00
Machou 730d60575e Update fr.json 2023-04-17 22:42:48 +02:00
Shawn Salat 1b96297cc7 blur the url input when clicking submit to add info currently in input 2023-04-17 08:25:20 -06:00
58 changed files with 1518 additions and 284 deletions
+1 -1
View File
@@ -12,4 +12,4 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Move tone executable to appropriate directory # 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/
+4
View File
@@ -3,6 +3,9 @@ node_modules
npm-debug.log npm-debug.log
.git .git
.gitignore .gitignore
.devcontainer/
.github/
.vscode/
/config /config
/audiobooks /audiobooks
/audiobooks2 /audiobooks2
@@ -13,4 +16,5 @@ test/
/client/.nuxt/ /client/.nuxt/
/client/dist/ /client/dist/
/dist/ /dist/
/build/
/deploy/ /deploy/
+18 -7
View File
@@ -1,14 +1,19 @@
### STAGE 0: Build client ### ### STAGE 0: Build client ###
FROM node:16-alpine AS build FROM node:16-alpine AS build
WORKDIR /client WORKDIR /client
COPY /client /client
COPY /client/package*.json /client/
RUN npm ci && npm cache clean --force RUN npm ci && npm cache clean --force
RUN npm run generate
COPY /client /client
RUN npm run build
### STAGE 1: Build server ### ### STAGE 1: Build server ###
FROM sandreas/tone:v0.1.2 AS tone FROM sandreas/tone:v0.1.5 AS tone
FROM node:16-alpine FROM node:16-alpine
WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
RUN apk update && \ RUN apk update && \
apk add --no-cache --update \ apk add --no-cache --update \
@@ -17,11 +22,17 @@ RUN apk update && \
ffmpeg ffmpeg
COPY --from=tone /usr/local/bin/tone /usr/local/bin/ COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist COPY package* ./
COPY index.js package* / COPY --from=build /client/package*.json client/
COPY server server
RUN npm ci --only=production RUN npm ci --omit=dev
RUN cd client && npm ci --omit=dev && cd ..
COPY index.js package* ./
COPY server server
COPY --from=build /client/nuxt.config.js* client/
COPY --from=build /client/.nuxt client/.nuxt
COPY --from=build /client/modules client/modules
EXPOSE 80 EXPOSE 80
HEALTHCHECK \ HEALTHCHECK \
+3 -3
View File
@@ -50,7 +50,7 @@ install_ffmpeg() {
echo "Starting FFMPEG Install" 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="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 if ! cd "$FFMPEG_INSTALL_DIR"; then
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR" echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
@@ -66,8 +66,8 @@ install_ffmpeg() {
# Temp downloading tone library to the ffmpeg dir # Temp downloading tone library to the ffmpeg dir
echo "Getting tone.." echo "Getting tone.."
$WGET_TONE $WGET_TONE
tar xvf tone-0.1.2-linux-x64.tar.gz --strip-components=1 tar xvf tone-0.1.5-linux-x64.tar.gz --strip-components=1
rm tone-0.1.2-linux-x64.tar.gz rm tone-0.1.5-linux-x64.tar.gz
echo "Good to go on Ffmpeg (& tone)... hopefully" echo "Good to go on Ffmpeg (& tone)... hopefully"
} }
+1 -1
View File
@@ -5,7 +5,7 @@
@import './absicons.css'; @import './absicons.css';
:root { :root {
--bookshelf-texture-img: url(/textures/wood_default.jpg); --bookshelf-texture-img: url(~static//textures/wood_default.jpg);
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); --bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
} }
+23 -6
View File
@@ -28,6 +28,9 @@
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6"> <widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p> <p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-authors-slider> </widgets-authors-slider>
<widgets-narrators-slider v-else-if="shelf.type === 'narrators'" :key="index + '.'" :items="shelf.entities" :height="100 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-narrators-slider>
</template> </template>
</div> </div>
<!-- Regular bookshelf view --> <!-- Regular bookshelf view -->
@@ -185,8 +188,8 @@ export default {
this.shelves = categories this.shelves = categories
}, },
async setShelvesFromSearch() { async setShelvesFromSearch() {
var shelves = [] const shelves = []
if (this.results.books && this.results.books.length) { if (this.results.books?.length) {
shelves.push({ shelves.push({
id: 'books', id: 'books',
label: 'Books', label: 'Books',
@@ -196,7 +199,7 @@ export default {
}) })
} }
if (this.results.podcasts && this.results.podcasts.length) { if (this.results.podcasts?.length) {
shelves.push({ shelves.push({
id: 'podcasts', id: 'podcasts',
label: 'Podcasts', label: 'Podcasts',
@@ -206,7 +209,7 @@ export default {
}) })
} }
if (this.results.series && this.results.series.length) { if (this.results.series?.length) {
shelves.push({ shelves.push({
id: 'series', id: 'series',
label: 'Series', label: 'Series',
@@ -221,7 +224,7 @@ export default {
}) })
}) })
} }
if (this.results.tags && this.results.tags.length) { if (this.results.tags?.length) {
shelves.push({ shelves.push({
id: 'tags', id: 'tags',
label: 'Tags', label: 'Tags',
@@ -236,7 +239,7 @@ export default {
}) })
}) })
} }
if (this.results.authors && this.results.authors.length) { if (this.results.authors?.length) {
shelves.push({ shelves.push({
id: 'authors', id: 'authors',
label: '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 this.shelves = shelves
}, },
scan() { scan() {
+6
View File
@@ -41,6 +41,11 @@
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" /> <cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'narrators'" class="flex items-center">
<template v-for="entity in shelf.entities">
<cards-narrator-card :key="entity.name" :width="150" :height="100" :narrator="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" />
</template>
</div>
</div> </div>
</div> </div>
@@ -88,6 +93,7 @@ export default {
return this.bookCoverWidth * this.bookCoverAspectRatio return this.bookCoverWidth * this.bookCoverAspectRatio
}, },
shelfHeight() { shelfHeight() {
if (this.shelf.type === 'narrators') return 148
return this.bookCoverHeight + 48 return this.bookCoverHeight + 48
}, },
paddingLeft() { paddingLeft() {
+5 -4
View File
@@ -10,7 +10,7 @@
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p> <p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" /> <p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" /> <div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div> </div>
</div> </div>
</template> </template>
@@ -67,12 +67,13 @@ export default {
// but with removing commas periods etc this is no longer plausible // but with removing commas periods etc this is no longer plausible
const html = this.matchText const html = this.matchText
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>` if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>` if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
if (this.matchKey === 'authors') return `by ${html}` if (this.matchKey === 'authors') return `by ${html}`
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>` if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>` if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>` if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
if (this.matchKey === 'narrators') return `<p class="truncate">${this.$strings.LabelNarrator}: ${html}</p>`
return `${html}` return `${html}`
} }
}, },
+11 -3
View File
@@ -7,7 +7,7 @@
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div> <div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" /> <div v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }"> <div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p> <p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
@@ -87,10 +87,10 @@ export default {
case 'totalDuration': case 'totalDuration':
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}` return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
case 'lastBookUpdated': case 'lastBookUpdated':
const lastUpdated = Math.max(...(this.books).map(x => x.updatedAt), 0) const lastUpdated = Math.max(...this.books.map((x) => x.updatedAt), 0)
return `${this.$strings.LabelLastBookUpdated} ${this.$formatDate(lastUpdated, this.dateFormat)}` return `${this.$strings.LabelLastBookUpdated} ${this.$formatDate(lastUpdated, this.dateFormat)}`
case 'lastBookAdded': case 'lastBookAdded':
const lastBookAdded = Math.max(...(this.books).map(x => x.addedAt), 0) const lastBookAdded = Math.max(...this.books.map((x) => x.addedAt), 0)
return `${this.$strings.LabelLastBookAdded} ${this.$formatDate(lastBookAdded, this.dateFormat)}` return `${this.$strings.LabelLastBookAdded} ${this.$formatDate(lastBookAdded, this.dateFormat)}`
default: default:
return null return null
@@ -115,6 +115,14 @@ export default {
seriesBooksFinished() { seriesBooksFinished() {
return this.seriesBookProgress.filter((p) => p.isFinished) return this.seriesBookProgress.filter((p) => p.isFinished)
}, },
hasSeriesBookInProgress() {
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
},
seriesPercentInProgress() {
let totalFinishedAndInProgress = this.seriesBooksFinished.length
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length))
},
isSeriesFinished() { isSeriesFinished() {
return this.books.length === this.seriesBooksFinished.length return this.books.length === this.seriesBooksFinished.length
}, },
+50
View File
@@ -0,0 +1,50 @@
<template>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-20">
<span class="material-icons-outlined text-8xl">record_voice_over</span>
</div>
<!-- Narrator name & num books overlay -->
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
</div>
</div>
</nuxt-link>
</template>
<script>
export default {
props: {
narrator: {
type: Object,
default: () => {}
},
width: Number,
height: Number,
sizeMultiplier: {
type: Number,
default: 1
}
},
data() {
return {}
},
computed: {
name() {
return this.narrator?.name || ''
},
numBooks() {
return this.narrator?.books?.length || 0
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
}
},
methods: {}
}
</script>
@@ -0,0 +1,34 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-200">record_voice_over</span>
</div>
<div class="flex-grow px-2 narratorSearchCardContent h-full">
<p class="truncate text-sm">{{ narrator }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
narrator: String
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
<style scoped>
.narratorSearchCardContent {
width: calc(100% - 40px);
height: 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
+13 -1
View File
@@ -63,6 +63,15 @@
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
<p v-if="narratorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelNarrators }}</p>
<template v-for="narrator in narratorResults">
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
<cards-narrator-search-card :narrator="narrator.name" />
</nuxt-link>
</li>
</template>
</template> </template>
</ul> </ul>
</div> </div>
@@ -84,6 +93,7 @@ export default {
authorResults: [], authorResults: [],
seriesResults: [], seriesResults: [],
tagResults: [], tagResults: [],
narratorResults: [],
searchTimeout: null, searchTimeout: null,
lastSearch: null lastSearch: null
} }
@@ -114,6 +124,7 @@ export default {
this.authorResults = [] this.authorResults = []
this.seriesResults = [] this.seriesResults = []
this.tagResults = [] this.tagResults = []
this.narratorResults = []
this.showMenu = false this.showMenu = false
this.isFetching = false this.isFetching = false
this.isTyping = false this.isTyping = false
@@ -142,7 +153,7 @@ export default {
} }
this.isFetching = true 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) console.error('Search error', error)
return [] return []
}) })
@@ -155,6 +166,7 @@ export default {
this.authorResults = searchResults.authors || [] this.authorResults = searchResults.authors || []
this.seriesResults = searchResults.series || [] this.seriesResults = searchResults.series || []
this.tagResults = searchResults.tags || [] this.tagResults = searchResults.tags || []
this.narratorResults = searchResults.narrators || []
this.isFetching = false this.isFetching = false
if (!this.showMenu) { if (!this.showMenu) {
+24 -9
View File
@@ -6,7 +6,7 @@
</div> </div>
</template> </template>
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
<div class="w-full p-8"> <div class="w-full p-8">
<div class="flex py-2"> <div class="flex py-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
@@ -96,7 +96,14 @@
</div> </div>
</div> </div>
<div v-if="!newUser.permissions.accessAllTags" class="my-4"> <div v-if="!newUser.permissions.accessAllTags" class="my-4">
<ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" :label="$strings.LabelTagsAccessibleToUser" /> <div class="flex items-center">
<ui-multi-select-dropdown v-model="newUser.itemTagsSelected" :items="itemTags" :label="tagsSelectionText" />
<div class="flex items-center pt-4 px-2">
<p class="px-3 font-semibold" id="selected-tags-not-accessible--permissions-toggle">{{ $strings.LabelInvert }}</p>
<ui-toggle-switch labeledBy="selected-tags-not-accessible--permissions-toggle" v-model="newUser.permissions.selectedTagsNotAccessible" />
</div>
</div>
</div> </div>
</div> </div>
@@ -185,6 +192,9 @@ export default {
value: t value: t
} }
}) })
},
tagsSelectionText() {
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
} }
}, },
methods: { methods: {
@@ -193,8 +203,11 @@ export default {
if (this.$refs.modal) this.$refs.modal.setHide() if (this.$refs.modal) this.$refs.modal.setHide()
}, },
accessAllTagsToggled(val) { accessAllTagsToggled(val) {
if (val && this.newUser.itemTagsAccessible.length) { if (val) {
this.newUser.itemTagsAccessible = [] if (this.newUser.itemTagsSelected?.length) {
this.newUser.itemTagsSelected = []
}
this.newUser.permissions.selectedTagsNotAccessible = false
} }
}, },
fetchAllTags() { fetchAllTags() {
@@ -226,7 +239,7 @@ export default {
this.$toast.error('Must select at least one library') this.$toast.error('Must select at least one library')
return 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') this.$toast.error('Must select at least one tag')
return return
} }
@@ -307,12 +320,12 @@ export default {
delete: type === 'admin', delete: type === 'admin',
upload: type === 'admin', upload: type === 'admin',
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true accessAllTags: true,
selectedTagsNotAccessible: false
} }
}, },
init() { init() {
this.fetchAllTags() this.fetchAllTags()
this.isNew = !this.account this.isNew = !this.account
if (this.account) { if (this.account) {
this.newUser = { this.newUser = {
@@ -322,9 +335,10 @@ export default {
isActive: this.account.isActive, isActive: this.account.isActive,
permissions: { ...this.account.permissions }, permissions: { ...this.account.permissions },
librariesAccessible: [...(this.account.librariesAccessible || [])], librariesAccessible: [...(this.account.librariesAccessible || [])],
itemTagsAccessible: [...(this.account.itemTagsAccessible || [])] itemTagsSelected: [...(this.account.itemTagsSelected || [])]
} }
} else { } else {
this.fetchAllTags()
this.newUser = { this.newUser = {
username: null, username: null,
password: null, password: null,
@@ -336,7 +350,8 @@ export default {
delete: false, delete: false,
upload: false, upload: false,
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true accessAllTags: true,
selectedTagsNotAccessible: false
}, },
librariesAccessible: [] librariesAccessible: []
} }
+3 -3
View File
@@ -49,13 +49,13 @@
</div> </div>
<form @submit.prevent="submitSearchForm"> <form @submit.prevent="submitSearchForm">
<div class="flex items-center justify-start -mx-1 h-20"> <div class="flex items-center justify-start -mx-1 h-20">
<div class="w-40 px-1"> <div class="w-48 px-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small /> <ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div> </div>
<div class="w-72 px-1"> <div class="w-72 px-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" /> <ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div> </div>
<div v-show="provider != 'itunes'" class="w-72 px-1"> <div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 px-1">
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
</div> </div>
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn> <ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
@@ -128,7 +128,7 @@ export default {
}, },
providers() { providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders 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() { searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
+7 -1
View File
@@ -115,7 +115,7 @@
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2"> <div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.genres" :items="selectedMatch.genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" /> <ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p> <p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p>
</div> </div>
</div> </div>
@@ -300,6 +300,12 @@ export default {
}, },
isPodcast() { isPodcast() {
return this.mediaType == 'podcast' 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: { methods: {
@@ -10,7 +10,7 @@
<div class="w-full px-3 py-5 md:p-12"> <div class="w-full px-3 py-5 md:p-12">
<ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" /> <ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" />
<ui-multi-select v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" /> <ui-multi-select ref="urlsInput" v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" />
<ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" /> <ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" />
@@ -103,6 +103,8 @@ export default {
if (this.$refs.modal) this.$refs.modal.setHide() if (this.$refs.modal) this.$refs.modal.setHide()
}, },
submitForm() { submitForm() {
this.$refs.urlsInput?.forceBlur()
if (!this.newNotification.urls.length) { if (!this.newNotification.urls.length) {
this.$toast.error('Must enter an Apprise URL') this.$toast.error('Must enter an Apprise URL')
return return
@@ -6,7 +6,7 @@
</div> </div>
</template> </template>
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden"> <div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div v-if="episodes.length" class="w-full py-3 mx-auto flex"> <div v-if="episodesCleaned.length" class="w-full py-3 mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow"> <form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" /> <ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
</form> </form>
@@ -16,12 +16,12 @@
v-for="(episode, index) in episodesList" v-for="(episode, index) in episodesList"
:key="index" :key="index"
class="relative" 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'" :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(index, episode)" @click="toggleSelectEpisode(episode)"
> >
<div class="absolute top-0 left-0 h-full flex items-center p-2"> <div class="absolute top-0 left-0 h-full flex items-center p-2">
<span v-if="itemEpisodeMap[episode.enclosure.url?.split('?')[0]]" class="material-icons text-success text-xl">download_done</span> <span v-if="itemEpisodeMap[episode.cleanUrl]" class="material-icons text-success text-xl">download_done</span>
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" /> <ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
</div> </div>
<div class="px-8 py-2"> <div class="px-8 py-2">
<div class="flex items-center font-semibold text-gray-200"> <div class="flex items-center font-semibold text-gray-200">
@@ -63,6 +63,7 @@ export default {
data() { data() {
return { return {
processing: false, processing: false,
episodesCleaned: [],
selectedEpisodes: {}, selectedEpisodes: {},
selectAll: false, selectAll: false,
search: null, search: null,
@@ -92,7 +93,7 @@ export default {
return this.libraryItem.media.metadata.title || 'Unknown' return this.libraryItem.media.metadata.title || 'Unknown'
}, },
allDownloaded() { allDownloaded() {
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return !this.episodesCleaned.some((episode) => !this.itemEpisodeMap[episode.cleanUrl])
}, },
episodesSelected() { episodesSelected() {
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key]) return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
@@ -113,7 +114,7 @@ export default {
return map return map
}, },
episodesList() { episodesList() {
return this.episodes.filter((episode) => { return this.episodesCleaned.filter((episode) => {
if (!this.searchText) return true if (!this.searchText) return true
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText)) return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
}) })
@@ -131,31 +132,29 @@ export default {
}, 500) }, 500)
}, },
toggleSelectAll(val) { toggleSelectAll(val) {
for (let i = 0; i < this.episodes.length; i++) { for (const episode of this.episodesCleaned) {
const episode = this.episodes[i] if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) this.selectedEpisodes[String(i)] = false else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
else this.$set(this.selectedEpisodes, String(i), val)
} }
}, },
checkSetIsSelectedAll() { checkSetIsSelectedAll() {
for (let i = 0; i < this.episodes.length; i++) { for (const episode of this.episodesCleaned) {
const episode = this.episodes[i] if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) {
if (!this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]] && !this.selectedEpisodes[String(i)]) {
this.selectAll = false this.selectAll = false
return return
} }
} }
this.selectAll = true this.selectAll = true
}, },
toggleSelectEpisode(index, episode) { toggleSelectEpisode(episode) {
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return 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() this.checkSetIsSelectedAll()
}, },
submit() { submit() {
var episodesToDownload = [] var episodesToDownload = []
if (this.episodesSelected.length) { 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 var payloadSize = JSON.stringify(episodesToDownload).length
@@ -185,7 +184,15 @@ export default {
}) })
}, },
init() { 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.selectAll = false
this.selectedEpisodes = {} this.selectedEpisodes = {}
} }
@@ -31,9 +31,10 @@
<!-- mobile --> <!-- mobile -->
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn> <ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
<div v-if="enclosureUrl" class="py-4"> <div v-if="enclosureUrl" class="pb-4 pt-6">
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p> <ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
<a :href="enclosureUrl" target="_blank" class="text-xs text-blue-400 hover:text-blue-500 hover:underline">{{ enclosureUrl }}</a> <label class="px-1 text-xs text-gray-200 font-semibold">Episode URL from RSS feed</label>
</ui-text-input-with-label>
</div> </div>
<div v-else class="py-4"> <div v-else class="py-4">
<p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p> <p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p>
@@ -0,0 +1,100 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<template v-for="item in items">
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
cardHeight() {
return this.height
},
cardWidth() {
return this.cardHeight * 1.5
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
}
},
methods: {
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {},
beforeDestroy() {}
}
</script>
+6
View File
@@ -569,6 +569,7 @@ export default {
changeLanguage(code) { changeLanguage(code) {
console.log('Changed lang', code) console.log('Changed lang', code)
this.currentLang = code this.currentLang = code
document.documentElement.lang = code
} }
}, },
beforeMount() { beforeMount() {
@@ -593,6 +594,11 @@ export default {
this.$toast.error(this.$route.query.error) this.$toast.error(this.$route.query.error)
this.$router.replace(this.$route.path) this.$router.replace(this.$route.path)
} }
// Set lang on HTML tag
if (this.$languageCodes?.current) {
document.documentElement.lang = this.$languageCodes.current
}
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off('change-lang', this.changeLanguage) this.$eventBus.$off('change-lang', this.changeLanguage)
+55
View File
@@ -0,0 +1,55 @@
const Path = require('path')
const { promises: { readFile, writeFile } } = require('fs')
const { glob } = require('glob')
/**
* Rewrite PWA manifest paths to include router base path
* @nuxtjs/pwa module does not support a dynamic router base path,
* so we have to rewrite the manifest paths manually
*
* @see https://github.com/nuxt/nuxt/pull/8520
* @see https://github.com/nuxt-community/pwa-module/issues/435
*/
export default function rewritePwaManifest () {
this.nuxt.hook('ready', async ({ options }) => {
// Do not run on build, this is where the manifest is generated
if (options._build) return
const routerBasePath = options.router.base
const clientDir = Path.join(options.buildDir, 'dist/client')
let rewritten = false
// Find manifest file generated to the build directory
const manifestPaths = await glob('manifest.*.json', { cwd: clientDir })
if (manifestPaths.length === 0) {
console.warn(`[PWA] No manifest not found under ${clientDir}/manifest.*.json`)
return
}
// Rewrite manifest paths for all found manifest files
for (const manifestPath of manifestPaths) {
const manifestJson = await readFile(Path.join(clientDir, manifestPath), 'utf8')
const manifest = JSON.parse(manifestJson)
const currentBasePath = manifest.start_url.split('?')[0]
if (currentBasePath !== routerBasePath) {
// Rewrite start_url and icons paths
manifest.start_url = `${routerBasePath || '/'}${manifest.start_url.slice(currentBasePath.length)}`
for (const icon of manifest.icons) {
icon.src = currentBasePath.startsWith('.')
? `${routerBasePath}${icon.src}` // Initially, the start_url is `./`
: `${routerBasePath}/${icon.src.slice(currentBasePath.length)}`
}
// Update manifest file
await writeFile(Path.join(clientDir, manifestPath), JSON.stringify(manifest), 'utf8')
rewritten = true
}
}
if (rewritten) {
console.info('[PWA] Manifest paths rewritten')
}
})
}
+25 -19
View File
@@ -1,9 +1,9 @@
const pkg = require('./package.json') const pkg = require('./package.json')
module.exports = { module.exports = ({ command }) => ({
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode // Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false, ssr: false,
target: 'static', target: 'server',
dev: process.env.NODE_ENV !== 'production', dev: process.env.NODE_ENV !== 'production',
env: { env: {
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333', serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333',
@@ -26,19 +26,12 @@ module.exports = {
{ charset: 'utf-8' }, { charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' } { hid: 'description', name: 'description', content: '' }
],
script: [
{
src: (process.env.ROUTER_BASE_PATH || '') + '/libs/sortable.js'
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }
] ]
}, },
router: { router: {
base: process.env.ROUTER_BASE_PATH || '' // We must specify `./` during build to support dynamic router base paths (https://github.com/nuxt/nuxt/issues/10088)
base: command == 'build' ? './' : process.env.ROUTER_BASE_PATH || ''
}, },
// Global CSS: https://go.nuxtjs.dev/config-css // Global CSS: https://go.nuxtjs.dev/config-css
@@ -46,10 +39,14 @@ module.exports = {
'@/assets/app.css' '@/assets/app.css'
], ],
favicon: '/favicon.ico',
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [ plugins: [
'@/plugins/constants.js', '@/plugins/constants.js',
'@/plugins/init.client.js', '@/plugins/init.client.js',
'@/plugins/sortable.js',
'@/plugins/favicon.js',
'@/plugins/axios.js', '@/plugins/axios.js',
'@/plugins/toast.js', '@/plugins/toast.js',
'@/plugins/utils.js', '@/plugins/utils.js',
@@ -71,13 +68,21 @@ module.exports = {
modules: [ modules: [
'nuxt-socket-io', 'nuxt-socket-io',
'@nuxtjs/axios', '@nuxtjs/axios',
'@nuxtjs/proxy' '@nuxtjs/proxy',
'@/modules/rewritePwaManifest.js'
], ],
proxy: { proxy: {
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }, [`${process.env.ROUTER_BASE_PATH || ''}/dev/`]: {
'/ebook/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' }, target: `http://localhost:3333${process.env.ROUTER_BASE_PATH || ''}`,
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' + process.env : '/' }, pathRewrite: { [`^${process.env.ROUTER_BASE_PATH || ''}/dev/`]: process.env.ROUTER_BASE_PATH || '' }
},
[`${process.env.ROUTER_BASE_PATH || ''}/ebook/`]: {
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
},
[`${process.env.ROUTER_BASE_PATH || ''}/s/`]: {
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
}
}, },
io: { io: {
@@ -106,17 +111,18 @@ module.exports = {
nativeUI: true nativeUI: true
}, },
manifest: { manifest: {
publicPath: `${(process.env.ROUTER_BASE_PATH || '')}_nuxt`,
name: 'Audiobookshelf', name: 'Audiobookshelf',
short_name: 'Audiobookshelf', short_name: 'Audiobookshelf',
display: 'standalone', display: 'standalone',
background_color: '#373838', background_color: '#373838',
icons: [ icons: [
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', src: '/icon.svg',
sizes: "any" sizes: "any"
}, },
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon64.png', src: '/icon64.png',
type: "image/png", type: "image/png",
sizes: "64x64" sizes: "64x64"
} }
@@ -146,7 +152,7 @@ module.exports = {
} }
}, },
server: { server: {
port: process.env.NODE_ENV === 'production' ? 80 : 3000, port: process.env.CLIENT_PORT || 3000,
host: '0.0.0.0' host: '0.0.0.0'
}, },
@@ -157,4 +163,4 @@ module.exports = {
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480) * See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
*/ */
devServerHandlers: [], devServerHandlers: [],
} })
+609 -21
View File
@@ -15,6 +15,7 @@
"cron-parser": "^4.7.1", "cron-parser": "^4.7.1",
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"epubjs": "^0.3.88", "epubjs": "^0.3.88",
"glob": "^10.2.2",
"hls.js": "^1.0.7", "hls.js": "^1.0.7",
"libarchive.js": "^1.3.0", "libarchive.js": "^1.3.0",
"nuxt": "^2.15.8", "nuxt": "^2.15.8",
@@ -1860,6 +1861,25 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@npmcli/move-file/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@npmcli/move-file/node_modules/mkdirp": { "node_modules/@npmcli/move-file/node_modules/mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -1937,6 +1957,25 @@
"upath": "^2.0.1" "upath": "^2.0.1"
} }
}, },
"node_modules/@nuxt/builder/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@nuxt/cli": { "node_modules/@nuxt/cli": {
"version": "2.15.8", "version": "2.15.8",
"resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-2.15.8.tgz", "resolved": "https://registry.npmjs.org/@nuxt/cli/-/cli-2.15.8.tgz",
@@ -1994,6 +2033,25 @@
"consola": "*" "consola": "*"
} }
}, },
"node_modules/@nuxt/components/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@nuxt/config": { "node_modules/@nuxt/config": {
"version": "2.15.8", "version": "2.15.8",
"resolved": "https://registry.npmjs.org/@nuxt/config/-/config-2.15.8.tgz", "resolved": "https://registry.npmjs.org/@nuxt/config/-/config-2.15.8.tgz",
@@ -2445,6 +2503,25 @@
"webpack": "^4.27.0 || ^5.0.0" "webpack": "^4.27.0 || ^5.0.0"
} }
}, },
"node_modules/@nuxt/webpack/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@nuxt/webpack/node_modules/icss-utils": { "node_modules/@nuxt/webpack/node_modules/icss-utils": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
@@ -2761,6 +2838,26 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@nuxtjs/tailwindcss/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@nuxtjs/tailwindcss/node_modules/glob-parent": { "node_modules/@nuxtjs/tailwindcss/node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2973,6 +3070,15 @@
"stack-trace": "0.0.10" "stack-trace": "0.0.10"
} }
}, },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@polka/url": { "node_modules/@polka/url": {
"version": "1.0.0-next.21", "version": "1.0.0-next.21",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
@@ -4588,6 +4694,25 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/cacache/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/cacache/node_modules/lru-cache": { "node_modules/cacache/node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -4977,7 +5102,6 @@
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"dependencies": { "dependencies": {
"string-width": "^4.2.0", "string-width": "^4.2.0",
"strip-ansi": "^6.0.1", "strip-ansi": "^6.0.1",
@@ -7084,6 +7208,32 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz",
"integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@@ -7317,19 +7467,21 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "10.2.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.2.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ==",
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "foreground-child": "^3.1.0",
"inflight": "^1.0.4", "jackspeak": "^2.0.3",
"inherits": "2", "minimatch": "^9.0.0",
"minimatch": "^3.1.1", "minipass": "^5.0.0",
"once": "^1.3.0", "path-scurry": "^1.7.0"
"path-is-absolute": "^1.0.0" },
"bin": {
"glob": "dist/cjs/src/bin.js"
}, },
"engines": { "engines": {
"node": "*" "node": ">=16 || 14 >=14.17"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@@ -7346,6 +7498,36 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz",
"integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/globals": { "node_modules/globals": {
"version": "11.12.0", "version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -8707,6 +8889,23 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/jackspeak": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.1.1.tgz",
"integrity": "sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw==",
"dependencies": {
"cliui": "^8.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest-worker": { "node_modules/jest-worker": {
"version": "26.6.2", "version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
@@ -9917,6 +10116,25 @@
"tiny-emitter": "^2.1.0" "tiny-emitter": "^2.1.0"
} }
}, },
"node_modules/nuxt-socket-io/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -10380,6 +10598,37 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"node_modules/path-scurry": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz",
"integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==",
"dependencies": {
"lru-cache": "^9.0.0",
"minipass": "^5.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz",
"integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==",
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/path-scurry/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
@@ -13291,6 +13540,26 @@
"node": ">= 12" "node": ">= 12"
} }
}, },
"node_modules/purgecss/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/q": { "node_modules/q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -13674,6 +13943,26 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/replace-in-file/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -13826,6 +14115,25 @@
"rimraf": "bin.js" "rimraf": "bin.js"
} }
}, },
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ripemd160": { "node_modules/ripemd160": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@@ -14709,6 +15017,25 @@
"webpack": "^3.0.0 || ^4.0.0 || ^5.0.0" "webpack": "^3.0.0 || ^4.0.0 || ^5.0.0"
} }
}, },
"node_modules/style-resources-loader/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/style-resources-loader/node_modules/schema-utils": { "node_modules/style-resources-loader/node_modules/schema-utils": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -16680,6 +17007,25 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/webpack/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/webpack/node_modules/is-accessor-descriptor": { "node_modules/webpack/node_modules/is-accessor-descriptor": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
@@ -18641,6 +18987,19 @@
"rimraf": "^3.0.2" "rimraf": "^3.0.2"
}, },
"dependencies": { "dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"mkdirp": { "mkdirp": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -18706,6 +19065,21 @@
"pify": "^5.0.0", "pify": "^5.0.0",
"serialize-javascript": "^5.0.1", "serialize-javascript": "^5.0.1",
"upath": "^2.0.1" "upath": "^2.0.1"
},
"dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
} }
}, },
"@nuxt/cli": { "@nuxt/cli": {
@@ -18759,6 +19133,21 @@
"semver": "^7.3.5", "semver": "^7.3.5",
"upath": "^2.0.1", "upath": "^2.0.1",
"vue-template-compiler": "^2.6.14" "vue-template-compiler": "^2.6.14"
},
"dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
} }
}, },
"@nuxt/config": { "@nuxt/config": {
@@ -19173,6 +19562,19 @@
"semver": "^7.3.2" "semver": "^7.3.2"
} }
}, },
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"icss-utils": { "icss-utils": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
@@ -19434,6 +19836,20 @@
"universalify": "^2.0.0" "universalify": "^2.0.0"
} }
}, },
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": { "glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -19581,6 +19997,12 @@
"stack-trace": "0.0.10" "stack-trace": "0.0.10"
} }
}, },
"@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true
},
"@polka/url": { "@polka/url": {
"version": "1.0.0-next.21", "version": "1.0.0-next.21",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
@@ -20920,6 +21342,19 @@
"unique-filename": "^1.1.1" "unique-filename": "^1.1.1"
}, },
"dependencies": { "dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"lru-cache": { "lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -21192,7 +21627,6 @@
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"requires": { "requires": {
"string-width": "^4.2.0", "string-width": "^4.2.0",
"strip-ansi": "^6.0.1", "strip-ansi": "^6.0.1",
@@ -22863,6 +23297,22 @@
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
"integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==" "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="
}, },
"foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"requires": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"dependencies": {
"signal-exit": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.1.tgz",
"integrity": "sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw=="
}
}
},
"fraction.js": { "fraction.js": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@@ -23034,16 +23484,38 @@
} }
}, },
"glob": { "glob": {
"version": "7.2.3", "version": "10.2.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.2.2.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-Xsa0BcxIC6th9UwNjZkhrMtNo/MnyRL8jGCP+uEwhA5oFOCY1f2s1/oNKY47xQ0Bg5nkjsfAEIej1VeH62bDDQ==",
"requires": { "requires": {
"fs.realpath": "^1.0.0", "foreground-child": "^3.1.0",
"inflight": "^1.0.4", "jackspeak": "^2.0.3",
"inherits": "2", "minimatch": "^9.0.0",
"minimatch": "^3.1.1", "minipass": "^5.0.0",
"once": "^1.3.0", "path-scurry": "^1.7.0"
"path-is-absolute": "^1.0.0" },
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"minimatch": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz",
"integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==",
"requires": {
"brace-expansion": "^2.0.1"
}
},
"minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="
}
} }
}, },
"glob-parent": { "glob-parent": {
@@ -24044,6 +24516,15 @@
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="
}, },
"jackspeak": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.1.1.tgz",
"integrity": "sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw==",
"requires": {
"@pkgjs/parseargs": "^0.11.0",
"cliui": "^8.0.1"
}
},
"jest-worker": { "jest-worker": {
"version": "26.6.2", "version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
@@ -25033,6 +25514,21 @@
"socket.io": "^4.1.1", "socket.io": "^4.1.1",
"socket.io-client": "^4.1.1", "socket.io-client": "^4.1.1",
"tiny-emitter": "^2.1.0" "tiny-emitter": "^2.1.0"
},
"dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
} }
}, },
"object-assign": { "object-assign": {
@@ -25387,6 +25883,27 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"path-scurry": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz",
"integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==",
"requires": {
"lru-cache": "^9.0.0",
"minipass": "^5.0.0"
},
"dependencies": {
"lru-cache": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz",
"integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A=="
},
"minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="
}
}
},
"path-to-regexp": { "path-to-regexp": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
@@ -27596,6 +28113,20 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true "dev": true
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
} }
} }
}, },
@@ -27891,6 +28422,22 @@
"chalk": "^4.1.2", "chalk": "^4.1.2",
"glob": "^7.2.0", "glob": "^7.2.0",
"yargs": "^17.2.1" "yargs": "^17.2.1"
},
"dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
} }
}, },
"require-directory": { "require-directory": {
@@ -28007,6 +28554,21 @@
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": { "requires": {
"glob": "^7.1.3" "glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
} }
}, },
"ripemd160": { "ripemd160": {
@@ -28708,6 +29270,19 @@
"tslib": "^2.3.1" "tslib": "^2.3.1"
}, },
"dependencies": { "dependencies": {
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"schema-utils": { "schema-utils": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -30148,6 +30723,19 @@
"locate-path": "^3.0.0" "locate-path": "^3.0.0"
} }
}, },
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"is-accessor-descriptor": { "is-accessor-descriptor": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+1
View File
@@ -19,6 +19,7 @@
"cron-parser": "^4.7.1", "cron-parser": "^4.7.1",
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"epubjs": "^0.3.88", "epubjs": "^0.3.88",
"glob": "^10.2.2",
"hls.js": "^1.0.7", "hls.js": "^1.0.7",
"libarchive.js": "^1.3.0", "libarchive.js": "^1.3.0",
"nuxt": "^2.15.8", "nuxt": "^2.15.8",
+17 -16
View File
@@ -11,27 +11,27 @@
<script> <script>
export default { export default {
async asyncData({ store, params, redirect, query, app }) { async asyncData({ store, params, redirect, query, app }) {
var libraryId = params.library const libraryId = params.library
var library = await store.dispatch('libraries/fetch', libraryId) const library = await store.dispatch('libraries/fetch', libraryId)
if (!library) { if (!library) {
return redirect('/oops?message=Library not found') return redirect('/oops?message=Library not found')
} }
var query = query.q let results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query.q}`).catch((error) => {
var results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query}`).catch((error) => {
console.error('Failed to search library', error) console.error('Failed to search library', error)
return null return null
}) })
results = { results = {
podcasts: results && results.podcast ? results.podcast : null, podcasts: results?.podcast || [],
books: results && results.book ? results.book : null, books: results?.book || [],
authors: results && results.authors.length ? results.authors : null, authors: results?.authors || [],
series: results && results.series.length ? results.series : null, series: results?.series || [],
tags: results && results.tags.length ? results.tags : null tags: results?.tags || [],
narrators: results?.narrators || []
} }
return { return {
libraryId, libraryId,
results, results,
query query: query.q
} }
}, },
data() { data() {
@@ -55,16 +55,17 @@ export default {
}, },
methods: { methods: {
async search() { async search() {
var results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => { const results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => {
console.error('Failed to search library', error) console.error('Failed to search library', error)
return null return null
}) })
this.results = { this.results = {
podcasts: results && results.podcast ? results.podcast : null, podcasts: results?.podcast || [],
books: results && results.book ? results.book : null, books: results?.book || [],
authors: results && results.authors.length ? results.authors : null, authors: results?.authors || [],
series: results && results.series.length ? results.series : null, series: results?.series || [],
tags: results && results.tags.length ? results.tags : null tags: results?.tags || [],
narrators: results?.narrators || []
} }
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.bookshelf) { if (this.$refs.bookshelf) {
+5
View File
@@ -16,6 +16,11 @@ export default function ({ $axios, store, $config }) {
config.url = `/dev${config.url}` config.url = `/dev${config.url}`
console.log('Making request to ' + config.url) console.log('Making request to ' + config.url)
} }
console.log($config)
if ($config.routerBasePath) {
config.url = `${$config.routerBasePath}${config.url}`
}
}) })
$axios.onError(error => { $axios.onError(error => {
+10
View File
@@ -0,0 +1,10 @@
export default function ({ $config }) {
const faviconPath = $config.favicon || '/favicon.ico'
const link = document.createElement('link')
link.rel = 'icon'
link.type = 'image/x-icon'
link.href = `${$config.routerBasePath || ''}${faviconPath}`
document.head.appendChild(link)
}
+7
View File
@@ -0,0 +1,7 @@
export default function ({ $config }) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = `${$config.routerBasePath || ''}/libs/sortable.js`
document.head.appendChild(script)
}
+6
View File
@@ -63,6 +63,12 @@ export const state = () => ({
text: 'iTunes', text: 'iTunes',
value: 'itunes' value: 'itunes'
} }
],
coverOnlyProviders: [
{
text: 'AudiobookCovers.com',
value: 'audiobookcovers'
}
] ]
}) })
+34 -32
View File
@@ -20,7 +20,7 @@
"ButtonCreate": "Erstellen", "ButtonCreate": "Erstellen",
"ButtonCreateBackup": "Sicherung erstellen", "ButtonCreateBackup": "Sicherung erstellen",
"ButtonDelete": "Löschen", "ButtonDelete": "Löschen",
"ButtonDownloadQueue": "Queue", "ButtonDownloadQueue": "Warteschlange",
"ButtonEdit": "Bearbeiten", "ButtonEdit": "Bearbeiten",
"ButtonEditChapters": "Kapitel bearbeiten", "ButtonEditChapters": "Kapitel bearbeiten",
"ButtonEditPodcast": "Podcast bearbeiten", "ButtonEditPodcast": "Podcast bearbeiten",
@@ -93,9 +93,9 @@
"HeaderCollection": "Sammlungen", "HeaderCollection": "Sammlungen",
"HeaderCollectionItems": "Sammlungseinträge", "HeaderCollectionItems": "Sammlungseinträge",
"HeaderCover": "Titelbild", "HeaderCover": "Titelbild",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "Aktuelle Downloads",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue", "HeaderDownloadQueue": "Download Warteschlange",
"HeaderEpisodes": "Episoden", "HeaderEpisodes": "Episoden",
"HeaderFiles": "Dateien", "HeaderFiles": "Dateien",
"HeaderFindChapters": "Kapitel suchen", "HeaderFindChapters": "Kapitel suchen",
@@ -142,8 +142,8 @@
"HeaderSettingsGeneral": "Allgemein", "HeaderSettingsGeneral": "Allgemein",
"HeaderSettingsScanner": "Scanner", "HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Einschlaf-Timer", "HeaderSleepTimer": "Einschlaf-Timer",
"HeaderStatsLargestItems": "Largest Items", "HeaderStatsLargestItems": "Größte Medien",
"HeaderStatsLongestItems": "Längste Einträge (h)", "HeaderStatsLongestItems": "Längste Medien (h)",
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)", "HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
"HeaderStatsRecentSessions": "Neueste Ereignisse", "HeaderStatsRecentSessions": "Neueste Ereignisse",
"HeaderStatsTop10Authors": "Top 10 Autoren", "HeaderStatsTop10Authors": "Top 10 Autoren",
@@ -161,7 +161,7 @@
"LabelAccountTypeGuest": "Gast", "LabelAccountTypeGuest": "Gast",
"LabelAccountTypeUser": "Benutzer", "LabelAccountTypeUser": "Benutzer",
"LabelActivity": "Aktivitäten", "LabelActivity": "Aktivitäten",
"LabelAdded": "Added", "LabelAdded": "Hinzugefügt",
"LabelAddedAt": "Hinzugefügt am", "LabelAddedAt": "Hinzugefügt am",
"LabelAddToCollection": "Zur Sammlung hinzufügen", "LabelAddToCollection": "Zur Sammlung hinzufügen",
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
@@ -169,7 +169,7 @@
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllUsers": "Alle Benutzer", "LabelAllUsers": "Alle Benutzer",
"LabelAlreadyInYourLibrary": "Already in your library", "LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
"LabelAppend": "Anhängen", "LabelAppend": "Anhängen",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Vorname Nachname)", "LabelAuthorFirstLast": "Autor (Vorname Nachname)",
@@ -186,7 +186,7 @@
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Bücher", "LabelBooks": "Bücher",
"LabelChangePassword": "Passwort ändern", "LabelChangePassword": "Passwort ändern",
"LabelChannels": "Channels", "LabelChannels": "Kanäle",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelChaptersFound": "gefundene Kapitel", "LabelChaptersFound": "gefundene Kapitel",
"LabelChapterTitle": "Kapitelüberschrift", "LabelChapterTitle": "Kapitelüberschrift",
@@ -201,10 +201,10 @@
"LabelCover": "Titelbild", "LabelCover": "Titelbild",
"LabelCoverImageURL": "URL des Titelbildes", "LabelCoverImageURL": "URL des Titelbildes",
"LabelCreatedAt": "Erstellt am", "LabelCreatedAt": "Erstellt am",
"LabelCronExpression": "Cron Ausdruck", "LabelCronExpression": "Cron-Ausdruck",
"LabelCurrent": "Aktuell", "LabelCurrent": "Aktuell",
"LabelCurrently": "Aktuell:", "LabelCurrently": "Aktuell:",
"LabelCustomCronExpression": "Custom Cron Expression:", "LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck",
"LabelDatetime": "Datum & Uhrzeit", "LabelDatetime": "Datum & Uhrzeit",
"LabelDescription": "Beschreibung", "LabelDescription": "Beschreibung",
"LabelDeselectAll": "Alles abwählen", "LabelDeselectAll": "Alles abwählen",
@@ -217,13 +217,13 @@
"LabelDuration": "Laufzeit", "LabelDuration": "Laufzeit",
"LabelDurationFound": "Gefundene Laufzeit:", "LabelDurationFound": "Gefundene Laufzeit:",
"LabelEdit": "Bearbeiten", "LabelEdit": "Bearbeiten",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Eingebettetes Cover",
"LabelEnable": "Aktivieren", "LabelEnable": "Aktivieren",
"LabelEnd": "Ende", "LabelEnd": "Ende",
"LabelEpisode": "Episode", "LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episodentitel", "LabelEpisodeTitle": "Episodentitel",
"LabelEpisodeType": "Episodentyp", "LabelEpisodeType": "Episodentyp",
"LabelExample": "Example", "LabelExample": "Beispiel",
"LabelExplicit": "Explizit (Altersbeschränkung)", "LabelExplicit": "Explizit (Altersbeschränkung)",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFile": "Datei", "LabelFile": "Datei",
@@ -254,11 +254,12 @@
"LabelIntervalEveryDay": "Jeden Tag", "LabelIntervalEveryDay": "Jeden Tag",
"LabelIntervalEveryHour": "Jede Stunde", "LabelIntervalEveryHour": "Jede Stunde",
"LabelInvalidParts": "Ungültige Teile", "LabelInvalidParts": "Ungültige Teile",
"LabelInvert": "Invert",
"LabelItem": "Medium", "LabelItem": "Medium",
"LabelLanguage": "Sprache", "LabelLanguage": "Sprache",
"LabelLanguageDefaultServer": "Standard-Server-Sprache", "LabelLanguageDefaultServer": "Standard-Server-Sprache",
"LabelLastBookAdded": "Last Book Added", "LabelLastBookAdded": "Zuletzt hinzugefügtes Medium",
"LabelLastBookUpdated": "Last Book Updated", "LabelLastBookUpdated": "Zuletzt aktualisiertes Medium",
"LabelLastSeen": "Zuletzt angesehen", "LabelLastSeen": "Zuletzt angesehen",
"LabelLastTime": "Letztes Mal", "LabelLastTime": "Letztes Mal",
"LabelLastUpdate": "Letzte Aktualisierung", "LabelLastUpdate": "Letzte Aktualisierung",
@@ -290,8 +291,8 @@
"LabelNewestAuthors": "Neuste Autoren", "LabelNewestAuthors": "Neuste Autoren",
"LabelNewestEpisodes": "Neueste Episoden", "LabelNewestEpisodes": "Neueste Episoden",
"LabelNewPassword": "Neues Passwort", "LabelNewPassword": "Neues Passwort",
"LabelNextBackupDate": "Next backup date", "LabelNextBackupDate": "Nächstes Sicherungsdatum",
"LabelNextScheduledRun": "Next scheduled run", "LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
"LabelNotes": "Hinweise", "LabelNotes": "Hinweise",
"LabelNotFinished": "nicht beendet", "LabelNotFinished": "nicht beendet",
"LabelNotificationAppriseURL": "Apprise URL(s)", "LabelNotificationAppriseURL": "Apprise URL(s)",
@@ -324,7 +325,7 @@
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type", "LabelPodcastType": "Podcast Type",
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)", "LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories", "LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
"LabelProgress": "Fortschritt", "LabelProgress": "Fortschritt",
"LabelProvider": "Anbieter", "LabelProvider": "Anbieter",
"LabelPubDate": "Veröffentlichungsdatum", "LabelPubDate": "Veröffentlichungsdatum",
@@ -332,14 +333,14 @@
"LabelPublishYear": "Jahr", "LabelPublishYear": "Jahr",
"LabelRecentlyAdded": "Kürzlich hinzugefügt", "LabelRecentlyAdded": "Kürzlich hinzugefügt",
"LabelRecentSeries": "Aktuelle Serien", "LabelRecentSeries": "Aktuelle Serien",
"LabelRecommended": "Recommended", "LabelRecommended": "Empfohlen",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum", "LabelReleaseDate": "Veröffentlichungsdatum",
"LabelRemoveCover": "Lösche Titelbild", "LabelRemoveCover": "Lösche Titelbild",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed Offen", "LabelRSSFeedOpen": "RSS Feed Offen",
"LabelRSSFeedPreventIndexing": "Prevent Indexing", "LabelRSSFeedPreventIndexing": "Indizierung verhindern",
"LabelRSSFeedSlug": "RSS Feed Schlagwort", "LabelRSSFeedSlug": "RSS Feed Schlagwort",
"LabelRSSFeedURL": "RSS Feed URL", "LabelRSSFeedURL": "RSS Feed URL",
"LabelSearchTerm": "Begriff suchen", "LabelSearchTerm": "Begriff suchen",
@@ -384,7 +385,7 @@
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.", "LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern", "LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.", "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
"LabelSettingsTimeFormat": "Time Format", "LabelSettingsTimeFormat": "Zeitformat",
"LabelShowAll": "Alles anzeigen", "LabelShowAll": "Alles anzeigen",
"LabelSize": "Größe", "LabelSize": "Größe",
"LabelSleepTimer": "Einschlaf-Timer", "LabelSleepTimer": "Einschlaf-Timer",
@@ -412,8 +413,9 @@
"LabelTag": "Schlagwort", "LabelTag": "Schlagwort",
"LabelTags": "Schlagwörter", "LabelTags": "Schlagwörter",
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter", "LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
"LabelTasks": "Tasks Running", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTimeBase": "Time Base", "LabelTasks": "Laufende Aufgaben",
"LabelTimeBase": "Basiszeit",
"LabelTimeListened": "Gehörte Zeit", "LabelTimeListened": "Gehörte Zeit",
"LabelTimeListenedToday": "Heute gehörte Zeit", "LabelTimeListenedToday": "Heute gehörte Zeit",
"LabelTimeRemaining": "{0} verbleibend", "LabelTimeRemaining": "{0} verbleibend",
@@ -468,9 +470,9 @@
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)", "MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen", "MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)", "MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)", "MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)", "MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
"MessageCheckingCron": "Überprüfe cron...", "MessageCheckingCron": "Überprüfe Cron...",
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?", "MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?", "MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?", "MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
@@ -517,8 +519,8 @@
"MessageNoCollections": "Keine Sammlungen", "MessageNoCollections": "Keine Sammlungen",
"MessageNoCoversFound": "Keine Titelbilder gefunden", "MessageNoCoversFound": "Keine Titelbilder gefunden",
"MessageNoDescription": "Keine Beschreibung", "MessageNoDescription": "Keine Beschreibung",
"MessageNoDownloadsInProgress": "No downloads currently in progress", "MessageNoDownloadsInProgress": "Derzeit keine Downloads in Arbeit",
"MessageNoDownloadsQueued": "No downloads queued", "MessageNoDownloadsQueued": "Keine Downloads in der Warteschlange",
"MessageNoEpisodeMatchesFound": "Keine Episodenübereinstimmungen gefunden", "MessageNoEpisodeMatchesFound": "Keine Episodenübereinstimmungen gefunden",
"MessageNoEpisodes": "Keine Episoden", "MessageNoEpisodes": "Keine Episoden",
"MessageNoFoldersAvailable": "Keine Ordner verfügbar", "MessageNoFoldersAvailable": "Keine Ordner verfügbar",
@@ -535,7 +537,7 @@
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"", "MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
"MessageNoSeries": "Keine Serien", "MessageNoSeries": "Keine Serien",
"MessageNoTags": "Keine Tags", "MessageNoTags": "Keine Tags",
"MessageNoTasksRunning": "No Tasks Running", "MessageNoTasksRunning": "Keine laufenden Aufgaben",
"MessageNotYetImplemented": "Noch nicht implementiert", "MessageNotYetImplemented": "Noch nicht implementiert",
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich", "MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig", "MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
@@ -549,7 +551,7 @@
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Online-Abgleichs, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?", "MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Online-Abgleichs, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
"MessageRemoveChapter": "Kapitel löschen", "MessageRemoveChapter": "Kapitel löschen",
"MessageRemoveEpisodes": "Entferne {0} Episode(n)", "MessageRemoveEpisodes": "Entferne {0} Episode(n)",
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen Remove from player queue", "MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
"MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?", "MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?",
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf", "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
"MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?", "MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?",
@@ -563,7 +565,7 @@
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen", "MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!", "MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
"MessageUploading": "Hochladen...", "MessageUploading": "Hochladen...",
"MessageValidCronExpression": "Gültiger cron-ausdruck", "MessageValidCronExpression": "Gültiger Cron-Ausdruck",
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert", "MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!", "MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer", "MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
@@ -581,7 +583,7 @@
"PlaceholderNewFolderPath": "Neuer Ordnerpfad", "PlaceholderNewFolderPath": "Neuer Ordnerpfad",
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname", "PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
"PlaceholderSearch": "Suche...", "PlaceholderSearch": "Suche...",
"PlaceholderSearchEpisode": "Search episode...", "PlaceholderSearchEpisode": "Suche Episode...",
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen", "ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
"ToastAccountUpdateSuccess": "Konto aktualisiert", "ToastAccountUpdateSuccess": "Konto aktualisiert",
"ToastAuthorImageRemoveFailed": "Bild konnte nicht entfernt werden", "ToastAuthorImageRemoveFailed": "Bild konnte nicht entfernt werden",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Every day", "LabelIntervalEveryDay": "Every day",
"LabelIntervalEveryHour": "Every hour", "LabelIntervalEveryHour": "Every hour",
"LabelInvalidParts": "Invalid Parts", "LabelInvalidParts": "Invalid Parts",
"LabelInvert": "Invert",
"LabelItem": "Item", "LabelItem": "Item",
"LabelLanguage": "Language", "LabelLanguage": "Language",
"LabelLanguageDefaultServer": "Default Server Language", "LabelLanguageDefaultServer": "Default Server Language",
@@ -412,6 +413,7 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags Accessible to User", "LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running", "LabelTasks": "Tasks Running",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Time Listened", "LabelTimeListened": "Time Listened",
+9 -7
View File
@@ -186,8 +186,8 @@
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Libros", "LabelBooks": "Libros",
"LabelChangePassword": "Cambiar Contraseña", "LabelChangePassword": "Cambiar Contraseña",
"LabelChannels": "Channels", "LabelChannels": "Canales",
"LabelChapters": "Chapters", "LabelChapters": "Capitulos",
"LabelChaptersFound": "Capitulo Encontrado", "LabelChaptersFound": "Capitulo Encontrado",
"LabelChapterTitle": "Titulo del Capitulo", "LabelChapterTitle": "Titulo del Capitulo",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
@@ -217,7 +217,7 @@
"LabelDuration": "Duración", "LabelDuration": "Duración",
"LabelDurationFound": "Duración Comprobada:", "LabelDurationFound": "Duración Comprobada:",
"LabelEdit": "Editar", "LabelEdit": "Editar",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Portada Integrada",
"LabelEnable": "Habilitar", "LabelEnable": "Habilitar",
"LabelEnd": "Fin", "LabelEnd": "Fin",
"LabelEpisode": "Episodio", "LabelEpisode": "Episodio",
@@ -235,7 +235,7 @@
"LabelFinished": "Terminado", "LabelFinished": "Terminado",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFolders": "Carpetas", "LabelFolders": "Carpetas",
"LabelFormat": "Format", "LabelFormat": "Formato",
"LabelGenre": "Genero", "LabelGenre": "Genero",
"LabelGenres": "Géneros", "LabelGenres": "Géneros",
"LabelHardDeleteFile": "Eliminar Definitivamente", "LabelHardDeleteFile": "Eliminar Definitivamente",
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Cada Dia", "LabelIntervalEveryDay": "Cada Dia",
"LabelIntervalEveryHour": "Cada Hora", "LabelIntervalEveryHour": "Cada Hora",
"LabelInvalidParts": "Partes Invalidas", "LabelInvalidParts": "Partes Invalidas",
"LabelInvert": "Invert",
"LabelItem": "Elemento", "LabelItem": "Elemento",
"LabelLanguage": "Lenguaje", "LabelLanguage": "Lenguaje",
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor", "LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
@@ -282,7 +283,7 @@
"LabelMissing": "Ausente", "LabelMissing": "Ausente",
"LabelMissingParts": "Partes Ausentes", "LabelMissingParts": "Partes Ausentes",
"LabelMore": "Mas", "LabelMore": "Mas",
"LabelMoreInfo": "More Info", "LabelMoreInfo": "Mas Información",
"LabelName": "Nombre", "LabelName": "Nombre",
"LabelNarrator": "Narrador", "LabelNarrator": "Narrador",
"LabelNarrators": "Narradores", "LabelNarrators": "Narradores",
@@ -412,6 +413,7 @@
"LabelTag": "Etiqueta", "LabelTag": "Etiqueta",
"LabelTags": "Etiquetas", "LabelTags": "Etiquetas",
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario", "LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tareas Corriendo", "LabelTasks": "Tareas Corriendo",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Tiempo Escuchando", "LabelTimeListened": "Tiempo Escuchando",
@@ -477,7 +479,7 @@
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?", "MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?", "MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?", "MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
"MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?", "MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?",
"MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?", "MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?", "MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?",
@@ -549,7 +551,7 @@
"MessageRemoveAllItemsWarning": "ADVERTENCIA! Esta acción eliminará todos los elementos de la biblioteca de la base de datos incluyendo cualquier actualización o match. Esto no hace nada a sus archivos reales. Esta seguro que desea continuar?", "MessageRemoveAllItemsWarning": "ADVERTENCIA! Esta acción eliminará todos los elementos de la biblioteca de la base de datos incluyendo cualquier actualización o match. Esto no hace nada a sus archivos reales. Esta seguro que desea continuar?",
"MessageRemoveChapter": "Remover capítulos", "MessageRemoveChapter": "Remover capítulos",
"MessageRemoveEpisodes": "Remover {0} episodio(s)", "MessageRemoveEpisodes": "Remover {0} episodio(s)",
"MessageRemoveFromPlayerQueue": "Remover de player queue", "MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion",
"MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?", "MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?",
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en", "MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en",
"MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?", "MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?",
+20 -18
View File
@@ -20,7 +20,7 @@
"ButtonCreate": "Créer", "ButtonCreate": "Créer",
"ButtonCreateBackup": "Créer une sauvegarde", "ButtonCreateBackup": "Créer une sauvegarde",
"ButtonDelete": "Effacer", "ButtonDelete": "Effacer",
"ButtonDownloadQueue": "Queue de téléchargement", "ButtonDownloadQueue": "File dattente de téléchargement",
"ButtonEdit": "Modifier", "ButtonEdit": "Modifier",
"ButtonEditChapters": "Modifier les chapitres", "ButtonEditChapters": "Modifier les chapitres",
"ButtonEditPodcast": "Modifier les podcasts", "ButtonEditPodcast": "Modifier les podcasts",
@@ -93,7 +93,7 @@
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Entrées de la Collection", "HeaderCollectionItems": "Entrées de la Collection",
"HeaderCover": "Couverture", "HeaderCover": "Couverture",
"HeaderCurrentDownloads": "Current Downloads", "HeaderCurrentDownloads": "File dattente de téléchargement",
"HeaderDetails": "Détails", "HeaderDetails": "Détails",
"HeaderDownloadQueue": "Queue de téléchargement", "HeaderDownloadQueue": "Queue de téléchargement",
"HeaderEpisodes": "Épisodes", "HeaderEpisodes": "Épisodes",
@@ -161,7 +161,7 @@
"LabelAccountTypeGuest": "Invité", "LabelAccountTypeGuest": "Invité",
"LabelAccountTypeUser": "Utilisateur", "LabelAccountTypeUser": "Utilisateur",
"LabelActivity": "Activité", "LabelActivity": "Activité",
"LabelAdded": "Added", "LabelAdded": "Ajouté",
"LabelAddedAt": "Date dajout", "LabelAddedAt": "Date dajout",
"LabelAddToCollection": "Ajouter à la collection", "LabelAddToCollection": "Ajouter à la collection",
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection", "LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
@@ -186,8 +186,8 @@
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBooks": "Livres", "LabelBooks": "Livres",
"LabelChangePassword": "Modifier le mot de passe", "LabelChangePassword": "Modifier le mot de passe",
"LabelChannels": "Channels", "LabelChannels": "Canaux",
"LabelChapters": "Chapters", "LabelChapters": "Chapitres",
"LabelChaptersFound": "Chapitres trouvés", "LabelChaptersFound": "Chapitres trouvés",
"LabelChapterTitle": "Titres du chapitre", "LabelChapterTitle": "Titres du chapitre",
"LabelClosePlayer": "Fermer le lecteur", "LabelClosePlayer": "Fermer le lecteur",
@@ -217,7 +217,7 @@
"LabelDuration": "Durée", "LabelDuration": "Durée",
"LabelDurationFound": "Durée trouvée :", "LabelDurationFound": "Durée trouvée :",
"LabelEdit": "Modifier", "LabelEdit": "Modifier",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Couverture du livre intégrée",
"LabelEnable": "Activer", "LabelEnable": "Activer",
"LabelEnd": "Fin", "LabelEnd": "Fin",
"LabelEpisode": "Épisode", "LabelEpisode": "Épisode",
@@ -254,11 +254,12 @@
"LabelIntervalEveryDay": "Tous les jours", "LabelIntervalEveryDay": "Tous les jours",
"LabelIntervalEveryHour": "Toutes les heures", "LabelIntervalEveryHour": "Toutes les heures",
"LabelInvalidParts": "Parties invalides", "LabelInvalidParts": "Parties invalides",
"LabelInvert": "Invert",
"LabelItem": "Article", "LabelItem": "Article",
"LabelLanguage": "Langue", "LabelLanguage": "Langue",
"LabelLanguageDefaultServer": "Langue par défaut", "LabelLanguageDefaultServer": "Langue par défaut",
"LabelLastBookAdded": "Last Book Added", "LabelLastBookAdded": "Dernier livre ajouté",
"LabelLastBookUpdated": "Last Book Updated", "LabelLastBookUpdated": "Dernier livre mis à jour",
"LabelLastSeen": "Vu dernièrement", "LabelLastSeen": "Vu dernièrement",
"LabelLastTime": "Progression", "LabelLastTime": "Progression",
"LabelLastUpdate": "Dernière mise à jour", "LabelLastUpdate": "Dernière mise à jour",
@@ -277,12 +278,12 @@
"LabelMediaType": "Type de média", "LabelMediaType": "Type de média",
"LabelMetadataProvider": "Fournisseur de métadonnées", "LabelMetadataProvider": "Fournisseur de métadonnées",
"LabelMetaTag": "Etiquette de métadonnée", "LabelMetaTag": "Etiquette de métadonnée",
"LabelMetaTags": "Meta Tags", "LabelMetaTags": "Etiquettes de métadonnée",
"LabelMinute": "Minute", "LabelMinute": "Minute",
"LabelMissing": "Manquant", "LabelMissing": "Manquant",
"LabelMissingParts": "Parties manquantes", "LabelMissingParts": "Parties manquantes",
"LabelMore": "Plus", "LabelMore": "Plus",
"LabelMoreInfo": "More Info", "LabelMoreInfo": "Plus dinfo",
"LabelName": "Nom", "LabelName": "Nom",
"LabelNarrator": "Narrateur", "LabelNarrator": "Narrateur",
"LabelNarrators": "Narrateurs", "LabelNarrators": "Narrateurs",
@@ -290,7 +291,7 @@
"LabelNewestAuthors": "Nouveaux auteurs", "LabelNewestAuthors": "Nouveaux auteurs",
"LabelNewestEpisodes": "Derniers épisodes", "LabelNewestEpisodes": "Derniers épisodes",
"LabelNewPassword": "Nouveau mot de passe", "LabelNewPassword": "Nouveau mot de passe",
"LabelNextBackupDate": "Prochaine date de sauvegarde", "LabelNextBackupDate": "Date de la prochaine sauvegarde",
"LabelNextScheduledRun": "Prochain lancement prévu", "LabelNextScheduledRun": "Prochain lancement prévu",
"LabelNotes": "Notes", "LabelNotes": "Notes",
"LabelNotFinished": "Non terminé(e)", "LabelNotFinished": "Non terminé(e)",
@@ -324,7 +325,7 @@
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Type de Podcast", "LabelPodcastType": "Type de Podcast",
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)", "LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories", "LabelPreventIndexing": "Empêcher lindexation de votre flux par les bases de donénes iTunes et Google podcast",
"LabelProgress": "Progression", "LabelProgress": "Progression",
"LabelProvider": "Fournisseur", "LabelProvider": "Fournisseur",
"LabelPubDate": "Date de publication", "LabelPubDate": "Date de publication",
@@ -339,7 +340,7 @@
"LabelRSSFeedCustomOwnerEmail": "Email propriétaire personnalisé", "LabelRSSFeedCustomOwnerEmail": "Email propriétaire personnalisé",
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé", "LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedOpen": "Flux RSS ouvert", "LabelRSSFeedOpen": "Flux RSS ouvert",
"LabelRSSFeedPreventIndexing": "Empêcher l'indexation", "LabelRSSFeedPreventIndexing": "Empêcher lindexation",
"LabelRSSFeedSlug": "Identificateur dadresse du Flux RSS ", "LabelRSSFeedSlug": "Identificateur dadresse du Flux RSS ",
"LabelRSSFeedURL": "Adresse du flux RSS", "LabelRSSFeedURL": "Adresse du flux RSS",
"LabelSearchTerm": "Terme de recherche", "LabelSearchTerm": "Terme de recherche",
@@ -384,7 +385,7 @@
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de larticle. Seul un fichier nommé « cover » sera conservé.", "LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de larticle. Seul un fichier nommé « cover » sera conservé.",
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles", "LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de larticle avec une extension « .abs ».", "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de larticle avec une extension « .abs ».",
"LabelSettingsTimeFormat": "Time Format", "LabelSettingsTimeFormat": "Format dheure",
"LabelShowAll": "Afficher Tout", "LabelShowAll": "Afficher Tout",
"LabelSize": "Taille", "LabelSize": "Taille",
"LabelSleepTimer": "Minuterie", "LabelSleepTimer": "Minuterie",
@@ -412,8 +413,9 @@
"LabelTag": "Étiquette", "LabelTag": "Étiquette",
"LabelTags": "Étiquettes", "LabelTags": "Étiquettes",
"LabelTagsAccessibleToUser": "Étiquettes accessibles à lutilisateur", "LabelTagsAccessibleToUser": "Étiquettes accessibles à lutilisateur",
"LabelTasks": "Tasks Running", "LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTimeBase": "Time Base", "LabelTasks": "Tâches en cours",
"LabelTimeBase": "Base de temps",
"LabelTimeListened": "Temps d’écoute", "LabelTimeListened": "Temps d’écoute",
"LabelTimeListenedToday": "Nombres d’écoutes Aujourdhui", "LabelTimeListenedToday": "Nombres d’écoutes Aujourdhui",
"LabelTimeRemaining": "{0} restantes", "LabelTimeRemaining": "{0} restantes",
@@ -535,7 +537,7 @@
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »", "MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
"MessageNoSeries": "Aucune série", "MessageNoSeries": "Aucune série",
"MessageNoTags": "Aucune d’étiquettes", "MessageNoTags": "Aucune d’étiquettes",
"MessageNoTasksRunning": "No Tasks Running", "MessageNoTasksRunning": "Aucune tâche en cours",
"MessageNotYetImplemented": "Non implémenté", "MessageNotYetImplemented": "Non implémenté",
"MessageNoUpdateNecessary": "Aucune mise à jour nécessaire", "MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire", "MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
@@ -581,7 +583,7 @@
"PlaceholderNewFolderPath": "Nouveau chemin de dossier", "PlaceholderNewFolderPath": "Nouveau chemin de dossier",
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture", "PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
"PlaceholderSearch": "Recherche...", "PlaceholderSearch": "Recherche...",
"PlaceholderSearchEpisode": "Search episode...", "PlaceholderSearchEpisode": "Recherche d’épisode...",
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte", "ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
"ToastAccountUpdateSuccess": "Compte mis à jour", "ToastAccountUpdateSuccess": "Compte mis à jour",
"ToastAuthorImageRemoveFailed": "Échec de la suppression de limage", "ToastAuthorImageRemoveFailed": "Échec de la suppression de limage",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Every day", "LabelIntervalEveryDay": "Every day",
"LabelIntervalEveryHour": "Every hour", "LabelIntervalEveryHour": "Every hour",
"LabelInvalidParts": "Invalid Parts", "LabelInvalidParts": "Invalid Parts",
"LabelInvert": "Invert",
"LabelItem": "Item", "LabelItem": "Item",
"LabelLanguage": "Language", "LabelLanguage": "Language",
"LabelLanguageDefaultServer": "Default Server Language", "LabelLanguageDefaultServer": "Default Server Language",
@@ -412,6 +413,7 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags Accessible to User", "LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running", "LabelTasks": "Tasks Running",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Time Listened", "LabelTimeListened": "Time Listened",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Every day", "LabelIntervalEveryDay": "Every day",
"LabelIntervalEveryHour": "Every hour", "LabelIntervalEveryHour": "Every hour",
"LabelInvalidParts": "Invalid Parts", "LabelInvalidParts": "Invalid Parts",
"LabelInvert": "Invert",
"LabelItem": "Item", "LabelItem": "Item",
"LabelLanguage": "Language", "LabelLanguage": "Language",
"LabelLanguageDefaultServer": "Default Server Language", "LabelLanguageDefaultServer": "Default Server Language",
@@ -412,6 +413,7 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags Accessible to User", "LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running", "LabelTasks": "Tasks Running",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Time Listened", "LabelTimeListened": "Time Listened",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Every day", "LabelIntervalEveryDay": "Every day",
"LabelIntervalEveryHour": "Every hour", "LabelIntervalEveryHour": "Every hour",
"LabelInvalidParts": "Nevaljajuči dijelovi", "LabelInvalidParts": "Nevaljajuči dijelovi",
"LabelInvert": "Invert",
"LabelItem": "Stavka", "LabelItem": "Stavka",
"LabelLanguage": "Jezik", "LabelLanguage": "Jezik",
"LabelLanguageDefaultServer": "Default jezik servera", "LabelLanguageDefaultServer": "Default jezik servera",
@@ -412,6 +413,7 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags dostupni korisniku", "LabelTagsAccessibleToUser": "Tags dostupni korisniku",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running", "LabelTasks": "Tasks Running",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Vremena odslušano", "LabelTimeListened": "Vremena odslušano",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Ogni Giorno", "LabelIntervalEveryDay": "Ogni Giorno",
"LabelIntervalEveryHour": "Ogni ora", "LabelIntervalEveryHour": "Ogni ora",
"LabelInvalidParts": "Parti Invalide", "LabelInvalidParts": "Parti Invalide",
"LabelInvert": "Invert",
"LabelItem": "Oggetti", "LabelItem": "Oggetti",
"LabelLanguage": "Lingua", "LabelLanguage": "Lingua",
"LabelLanguageDefaultServer": "Lingua di Default", "LabelLanguageDefaultServer": "Lingua di Default",
@@ -412,6 +413,7 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti", "LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Processi in esecuzione", "LabelTasks": "Processi in esecuzione",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Tempo di Ascolto", "LabelTimeListened": "Tempo di Ascolto",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Każdego dnia", "LabelIntervalEveryDay": "Każdego dnia",
"LabelIntervalEveryHour": "Każdej godziny", "LabelIntervalEveryHour": "Każdej godziny",
"LabelInvalidParts": "Nieprawidłowe części", "LabelInvalidParts": "Nieprawidłowe części",
"LabelInvert": "Invert",
"LabelItem": "Pozycja", "LabelItem": "Pozycja",
"LabelLanguage": "Język", "LabelLanguage": "Język",
"LabelLanguageDefaultServer": "Domyślny język serwera", "LabelLanguageDefaultServer": "Domyślny język serwera",
@@ -412,6 +413,7 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tagi", "LabelTags": "Tagi",
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika", "LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running", "LabelTasks": "Tasks Running",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Czas odtwarzania", "LabelTimeListened": "Czas odtwarzania",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "Каждый день", "LabelIntervalEveryDay": "Каждый день",
"LabelIntervalEveryHour": "Каждый час", "LabelIntervalEveryHour": "Каждый час",
"LabelInvalidParts": "Неверные части", "LabelInvalidParts": "Неверные части",
"LabelInvert": "Invert",
"LabelItem": "Элемент", "LabelItem": "Элемент",
"LabelLanguage": "Язык", "LabelLanguage": "Язык",
"LabelLanguageDefaultServer": "Язык сервера по умолчанию", "LabelLanguageDefaultServer": "Язык сервера по умолчанию",
@@ -412,6 +413,7 @@
"LabelTag": "Тег", "LabelTag": "Тег",
"LabelTags": "Теги", "LabelTags": "Теги",
"LabelTagsAccessibleToUser": "Теги доступные для пользователя", "LabelTagsAccessibleToUser": "Теги доступные для пользователя",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Запущенные задачи", "LabelTasks": "Запущенные задачи",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "Время прослушивания", "LabelTimeListened": "Время прослушивания",
+2
View File
@@ -254,6 +254,7 @@
"LabelIntervalEveryDay": "每天", "LabelIntervalEveryDay": "每天",
"LabelIntervalEveryHour": "每小时", "LabelIntervalEveryHour": "每小时",
"LabelInvalidParts": "无效部件", "LabelInvalidParts": "无效部件",
"LabelInvert": "Invert",
"LabelItem": "项目", "LabelItem": "项目",
"LabelLanguage": "语言", "LabelLanguage": "语言",
"LabelLanguageDefaultServer": "默认服务器语言", "LabelLanguageDefaultServer": "默认服务器语言",
@@ -412,6 +413,7 @@
"LabelTag": "标签", "LabelTag": "标签",
"LabelTags": "标签", "LabelTags": "标签",
"LabelTagsAccessibleToUser": "用户可访问的标签", "LabelTagsAccessibleToUser": "用户可访问的标签",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "正在运行的任务", "LabelTasks": "正在运行的任务",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeListened": "收听时间", "LabelTimeListened": "收听时间",
-12
View File
@@ -48,18 +48,6 @@
<Mode>rw</Mode> <Mode>rw</Mode>
</Volume> </Volume>
</Data> </Data>
<Environment>
<Variable>
<Value>99</Value>
<Name>AUDIOBOOKSHELF_UID</Name>
<Mode/>
</Variable>
<Variable>
<Value>100</Value>
<Name>AUDIOBOOKSHELF_GID</Name>
<Mode/>
</Variable>
</Environment>
<Labels/> <Labels/>
<Config Name="Audiobooks" Target="/audiobooks" Default="" Mode="rw" Description="Container Path: /audiobooks" Type="Path" Display="always" Required="true" Mask="false" /> <Config Name="Audiobooks" Target="/audiobooks" Default="" Mode="rw" Description="Container Path: /audiobooks" Type="Path" Display="always" Required="true" Mask="false" />
<Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config> <Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config>
+3 -1
View File
@@ -12,6 +12,7 @@ if (isDev) {
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
process.env.SOURCE = 'local' process.env.SOURCE = 'local'
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || '' process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
process.env.CLIENT_PORT = devEnv.ClientPort || 8080
} }
const PORT = process.env.PORT || 80 const PORT = process.env.PORT || 80
@@ -22,8 +23,9 @@ const UID = process.env.AUDIOBOOKSHELF_UID
const GID = process.env.AUDIOBOOKSHELF_GID const GID = process.env.AUDIOBOOKSHELF_GID
const SOURCE = process.env.SOURCE || 'docker' const SOURCE = process.env.SOURCE || 'docker'
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || '' const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
const CLIENT_PORT = process.env.CLIENT_PORT || 3000
console.log('Config', CONFIG_PATH, METADATA_PATH) console.log('Config', CONFIG_PATH, METADATA_PATH)
const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH, CLIENT_PORT)
Server.start() Server.start()
+165 -17
View File
@@ -13,6 +13,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"graceful-fs": "^4.2.10", "graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1", "htmlparser2": "^8.0.1",
"http-proxy-middleware": "^3.0.0-beta.1",
"node-tone": "^1.0.1", "node-tone": "^1.0.1",
"socket.io": "^4.5.4", "socket.io": "^4.5.4",
"xml2js": "^0.5.0" "xml2js": "^0.5.0"
@@ -42,6 +43,14 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/http-proxy": {
"version": "1.17.11",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz",
"integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.11.18", "version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
@@ -157,7 +166,6 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
}, },
@@ -451,6 +459,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/express": { "node_modules/express": {
"version": "4.18.2", "version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -496,7 +509,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@@ -682,6 +694,56 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-middleware": {
"version": "3.0.0-beta.1",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0-beta.1.tgz",
"integrity": "sha512-hdiTlVVoaxncf239csnEpG5ew2lRWnoNR1PMWOO6kYulSphlrfLs5JFZtFVH3R5EUWSZNMkeUqvkvfctuWaK8A==",
"dependencies": {
"@types/http-proxy": "^1.17.10",
"debug": "^4.3.4",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.1",
"is-plain-obj": "^3.0.0",
"micromatch": "^4.0.5"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/http-proxy-middleware/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/http-proxy-middleware/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -728,7 +790,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -737,7 +798,6 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@@ -749,11 +809,21 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-plain-obj": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -775,6 +845,18 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime": { "node_modules/mime": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -946,7 +1028,6 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@@ -1020,6 +1101,11 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1238,7 +1324,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@@ -1368,6 +1453,14 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/http-proxy": {
"version": "1.17.11",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz",
"integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==",
"requires": {
"@types/node": "*"
}
},
"@types/node": { "@types/node": {
"version": "18.11.18", "version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
@@ -1467,7 +1560,6 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": { "requires": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
} }
@@ -1671,6 +1763,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
}, },
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"express": { "express": {
"version": "4.18.2", "version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -1713,7 +1810,6 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": { "requires": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
} }
@@ -1835,6 +1931,44 @@
"toidentifier": "1.0.1" "toidentifier": "1.0.1"
} }
}, },
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
},
"http-proxy-middleware": {
"version": "3.0.0-beta.1",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0-beta.1.tgz",
"integrity": "sha512-hdiTlVVoaxncf239csnEpG5ew2lRWnoNR1PMWOO6kYulSphlrfLs5JFZtFVH3R5EUWSZNMkeUqvkvfctuWaK8A==",
"requires": {
"@types/http-proxy": "^1.17.10",
"debug": "^4.3.4",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.1",
"is-plain-obj": "^3.0.0",
"micromatch": "^4.0.5"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -1871,14 +2005,12 @@
"is-extglob": { "is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
"dev": true
}, },
"is-glob": { "is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": { "requires": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
} }
@@ -1886,8 +2018,12 @@
"is-number": { "is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
"dev": true },
"is-plain-obj": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="
}, },
"media-typer": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
@@ -1904,6 +2040,15 @@
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
}, },
"micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"requires": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
}
},
"mime": { "mime": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -2027,8 +2172,7 @@
"picomatch": { "picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
"dev": true
}, },
"proxy-addr": { "proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
@@ -2078,6 +2222,11 @@
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }
}, },
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"safe-buffer": { "safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2244,7 +2393,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": { "requires": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
} }
+1
View File
@@ -34,6 +34,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"graceful-fs": "^4.2.10", "graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1", "htmlparser2": "^8.0.1",
"http-proxy-middleware": "^3.0.0-beta.1",
"node-tone": "^1.0.1", "node-tone": "^1.0.1",
"socket.io": "^4.5.4", "socket.io": "^4.5.4",
"xml2js": "^0.5.0" "xml2js": "^0.5.0"
+1 -1
View File
@@ -147,7 +147,7 @@ For this to work you must enable at least the following mods using `a2enmod`:
### SWAG Reverse Proxy ### SWAG Reverse Proxy
[See this solution](https://forums.unraid.net/topic/112698-support-audiobookshelf/?do=findComment&comment=1049637) [See LinuxServer.io config sample](https://github.com/linuxserver/reverse-proxy-confs/blob/master/audiobookshelf.subdomain.conf.sample)
### Synology Reverse Proxy ### Synology Reverse Proxy
+7 -5
View File
@@ -126,12 +126,12 @@ class Auth {
async login(req, res) { async login(req, res) {
const ipAddress = requestIp.getClientIp(req) const ipAddress = requestIp.getClientIp(req)
var username = (req.body.username || '').toLowerCase() const username = (req.body.username || '').toLowerCase()
var password = req.body.password || '' const password = req.body.password || ''
var user = this.users.find(u => u.username.toLowerCase() === username) const user = this.users.find(u => u.username.toLowerCase() === username)
if (!user || !user.isActive) { if (!user?.isActive) {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
if (req.rateLimit.remaining <= 2) { if (req.rateLimit.remaining <= 2) {
Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`) Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
@@ -145,13 +145,15 @@ class Auth {
if (password) { if (password) {
return res.status(401).send('Invalid root password (hint: there is none)') return res.status(401).send('Invalid root password (hint: there is none)')
} else { } else {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
return res.json(this.getUserLoginResponsePayload(user)) return res.json(this.getUserLoginResponsePayload(user))
} }
} }
// Check password match // Check password match
var compare = await bcrypt.compare(password, user.pash) const compare = await bcrypt.compare(password, user.pash)
if (compare) { if (compare) {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
res.json(this.getUserLoginResponsePayload(user)) res.json(this.getUserLoginResponsePayload(user))
} else { } else {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`) Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
+10 -28
View File
@@ -22,6 +22,7 @@ const SocketAuthority = require('./SocketAuthority')
const ApiRouter = require('./routers/ApiRouter') const ApiRouter = require('./routers/ApiRouter')
const HlsRouter = require('./routers/HlsRouter') const HlsRouter = require('./routers/HlsRouter')
const StaticRouter = require('./routers/StaticRouter') const StaticRouter = require('./routers/StaticRouter')
const ClientRouter = require('./routers/ClientRouter')
const NotificationManager = require('./managers/NotificationManager') const NotificationManager = require('./managers/NotificationManager')
const CoverManager = require('./managers/CoverManager') const CoverManager = require('./managers/CoverManager')
@@ -37,7 +38,7 @@ const CronManager = require('./managers/CronManager')
const TaskManager = require('./managers/TaskManager') const TaskManager = require('./managers/TaskManager')
class Server { class Server {
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH, CLIENT_PORT) {
this.Port = PORT this.Port = PORT
this.Host = HOST this.Host = HOST
global.Source = SOURCE global.Source = SOURCE
@@ -47,6 +48,7 @@ class Server {
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH)) global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH)) global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
global.RouterBasePath = ROUTER_BASE_PATH global.RouterBasePath = ROUTER_BASE_PATH
global.ClientPort = CLIENT_PORT
global.XAccel = process.env.USE_X_ACCEL global.XAccel = process.env.USE_X_ACCEL
if (!fs.pathExistsSync(global.ConfigPath)) { if (!fs.pathExistsSync(global.ConfigPath)) {
@@ -82,6 +84,7 @@ class Server {
this.apiRouter = new ApiRouter(this) this.apiRouter = new ApiRouter(this)
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager) this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
this.staticRouter = new StaticRouter(this.db) this.staticRouter = new StaticRouter(this.db)
this.clientRouter = new ClientRouter(global.appRoot, global.ClientPort, global.RouterBasePath)
Logger.logManager = this.logManager Logger.logManager = this.logManager
@@ -136,6 +139,7 @@ class Server {
async start() { async start() {
Logger.info('=== Starting Server ===') Logger.info('=== Starting Server ===')
await this.init() await this.init()
this.clientRouter.start()
const app = express() const app = express()
const router = express.Router() const router = express.Router()
@@ -149,10 +153,6 @@ class Server {
router.use(express.urlencoded({ extended: true, limit: "5mb" })); router.use(express.urlencoded({ extended: true, limit: "5mb" }));
router.use(express.json({ limit: "5mb" })) router.use(express.json({ limit: "5mb" }))
// Static path to generated nuxt
const distPath = Path.join(global.appRoot, '/client/dist')
router.use(express.static(distPath))
// Metadata folder static path // Metadata folder static path
router.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath)) router.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
@@ -188,28 +188,6 @@ class Server {
this.rssFeedManager.getFeedItem(req, res) this.rssFeedManager.getFeedItem(req, res)
}) })
// Client dynamic routes
const dyanimicRoutes = [
'/item/:id',
'/author/:id',
'/audiobook/:id/chapters',
'/audiobook/:id/edit',
'/audiobook/:id/manage',
'/library/:library',
'/library/:library/search',
'/library/:library/bookshelf/:id?',
'/library/:library/authors',
'/library/:library/series/:id?',
'/library/:library/podcast/search',
'/library/:library/podcast/latest',
'/config/users/:id',
'/config/users/:id/sessions',
'/config/item-metadata-utils/:id',
'/collection/:id',
'/playlist/:id'
]
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res)) router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this)) router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
router.post('/init', (req, res) => { router.post('/init', (req, res) => {
@@ -238,6 +216,9 @@ class Server {
}) })
app.get('/healthcheck', (req, res) => res.sendStatus(200)) app.get('/healthcheck', (req, res) => res.sendStatus(200))
// Serve client on all other routes, 404 will be handled by client
router.use(this.clientRouter.router)
this.server.listen(this.Port, this.Host, () => { this.server.listen(this.Port, this.Host, () => {
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`) if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
else Logger.info(`Listening on port :${this.Port}`) else Logger.info(`Listening on port :${this.Port}`)
@@ -348,7 +329,8 @@ class Server {
} }
async stop() { async stop() {
await this.watcher.close() this.watcher.close()
this.clientRouter.stop()
Logger.info('Watcher Closed') Logger.info('Watcher Closed')
return new Promise((resolve) => { return new Promise((resolve) => {
+15 -4
View File
@@ -596,6 +596,7 @@ class LibraryController {
const itemMatches = [] const itemMatches = []
const authorMatches = {} const authorMatches = {}
const narratorMatches = {}
const seriesMatches = {} const seriesMatches = {}
const tagMatches = {} const tagMatches = {}
@@ -608,7 +609,7 @@ class LibraryController {
matchText: queryResult.matchText matchText: queryResult.matchText
}) })
} }
if (queryResult.series && queryResult.series.length) { if (queryResult.series?.length) {
queryResult.series.forEach((se) => { queryResult.series.forEach((se) => {
if (!seriesMatches[se.id]) { if (!seriesMatches[se.id]) {
const _series = this.db.series.find(_se => _se.id === se.id) const _series = this.db.series.find(_se => _se.id === se.id)
@@ -618,7 +619,7 @@ class LibraryController {
} }
}) })
} }
if (queryResult.authors && queryResult.authors.length) { if (queryResult.authors?.length) {
queryResult.authors.forEach((au) => { queryResult.authors.forEach((au) => {
if (!authorMatches[au.id]) { if (!authorMatches[au.id]) {
const _author = this.db.authors.find(_au => _au.id === au.id) const _author = this.db.authors.find(_au => _au.id === au.id)
@@ -631,7 +632,7 @@ class LibraryController {
} }
}) })
} }
if (queryResult.tags && queryResult.tags.length) { if (queryResult.tags?.length) {
queryResult.tags.forEach((tag) => { queryResult.tags.forEach((tag) => {
if (!tagMatches[tag]) { if (!tagMatches[tag]) {
tagMatches[tag] = { name: tag, books: [li.toJSON()] } tagMatches[tag] = { name: tag, books: [li.toJSON()] }
@@ -640,13 +641,23 @@ class LibraryController {
} }
}) })
} }
if (queryResult.narrators?.length) {
queryResult.narrators.forEach((narrator) => {
if (!narratorMatches[narrator]) {
narratorMatches[narrator] = { name: narrator, books: [li.toJSON()] }
} else {
narratorMatches[narrator].books.push(li.toJSON())
}
})
}
}) })
const itemKey = req.library.mediaType const itemKey = req.library.mediaType
const results = { const results = {
[itemKey]: itemMatches.slice(0, maxResults), [itemKey]: itemMatches.slice(0, maxResults),
tags: Object.values(tagMatches).slice(0, maxResults), tags: Object.values(tagMatches).slice(0, maxResults),
authors: Object.values(authorMatches).slice(0, maxResults), authors: Object.values(authorMatches).slice(0, maxResults),
series: Object.values(seriesMatches).slice(0, maxResults) series: Object.values(seriesMatches).slice(0, maxResults),
narrators: Object.values(narratorMatches).slice(0, maxResults)
} }
res.json(results) res.json(results)
} }
+16 -4
View File
@@ -4,6 +4,7 @@ const Audible = require('../providers/Audible')
const iTunes = require('../providers/iTunes') const iTunes = require('../providers/iTunes')
const Audnexus = require('../providers/Audnexus') const Audnexus = require('../providers/Audnexus')
const FantLab = require('../providers/FantLab') const FantLab = require('../providers/FantLab')
const AudiobookCovers = require('../providers/AudiobookCovers')
const Logger = require('../Logger') const Logger = require('../Logger')
const { levenshteinDistance } = require('../utils/index') const { levenshteinDistance } = require('../utils/index')
@@ -15,6 +16,7 @@ class BookFinder {
this.iTunesApi = new iTunes() this.iTunesApi = new iTunes()
this.audnexus = new Audnexus() this.audnexus = new Audnexus()
this.fantLab = new FantLab() this.fantLab = new FantLab()
this.audiobookCovers = new AudiobookCovers()
this.verbose = false this.verbose = false
} }
@@ -159,6 +161,12 @@ class BookFinder {
return books return books
} }
async getAudiobookCoversResults(search) {
const covers = await this.audiobookCovers.search(search)
if (this.verbose) Logger.debug(`AudiobookCovers Search Results: ${covers.length || 0}`)
return covers || []
}
async getiTunesAudiobooksResults(title, author) { async getiTunesAudiobooksResults(title, author) {
return this.iTunesApi.searchAudiobooks(title) return this.iTunesApi.searchAudiobooks(title)
} }
@@ -187,6 +195,8 @@ class BookFinder {
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
} else if (provider === 'fantlab') { } else if (provider === 'fantlab') {
books = await this.getFantLabResults(title, author) books = await this.getFantLabResults(title, author)
} else if (provider === 'audiobookcovers') {
books = await this.getAudiobookCoversResults(title)
} }
else { else {
books = await this.getGoogleBooksResults(title, author) books = await this.getGoogleBooksResults(title, author)
@@ -202,11 +212,13 @@ class BookFinder {
return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options) return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options)
} }
if (["google", "audible", "itunes", 'fantlab'].includes(provider)) return books if (provider === 'openlibrary') {
books.sort((a, b) => {
return a.totalDistance - b.totalDistance
})
}
return books.sort((a, b) => { return books
return a.totalDistance - b.totalDistance
})
} }
async findCovers(provider, title, author, options = {}) { async findCovers(provider, title, author, options = {}) {
+5 -2
View File
@@ -322,6 +322,7 @@ class Book {
tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)), tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)),
series: this.metadata.searchSeries(query), series: this.metadata.searchSeries(query),
authors: this.metadata.searchAuthors(query), authors: this.metadata.searchAuthors(query),
narrators: this.metadata.searchNarrators(query),
matchKey: null, matchKey: null,
matchText: null matchText: null
} }
@@ -336,10 +337,12 @@ class Book {
} else if (payload.series.length) { } else if (payload.series.length) {
payload.matchKey = 'series' payload.matchKey = 'series'
payload.matchText = this.metadata.seriesName payload.matchText = this.metadata.seriesName
} } else if (payload.tags.length) {
else if (payload.tags.length) {
payload.matchKey = 'tags' payload.matchKey = 'tags'
payload.matchText = this.tags.join(', ') payload.matchText = this.tags.join(', ')
} else if (payload.narrators.length) {
payload.matchKey = 'narrators'
payload.matchText = this.metadata.narratorName
} }
} }
return payload return payload
+3
View File
@@ -381,6 +381,9 @@ class BookMetadata {
searchAuthors(query) { searchAuthors(query) {
return this.authors.filter(au => cleanStringForSearch(au.name).includes(query)) return this.authors.filter(au => cleanStringForSearch(au.name).includes(query))
} }
searchNarrators(query) {
return this.narrators.filter(n => cleanStringForSearch(n).includes(query))
}
searchQuery(query) { // Returns key if match is found searchQuery(query) { // Returns key if match is found
const keysToCheck = ['title', 'asin', 'isbn'] const keysToCheck = ['title', 'asin', 'isbn']
for (const key of keysToCheck) { for (const key of keysToCheck) {
+25 -14
View File
@@ -20,7 +20,7 @@ class User {
this.permissions = {} this.permissions = {}
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries) this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
this.itemTagsAccessible = [] // Empty if ALL item tags accessible this.itemTagsSelected = [] // Empty if ALL item tags accessible
if (user) { if (user) {
this.construct(user) this.construct(user)
@@ -86,7 +86,7 @@ class User {
createdAt: this.createdAt, createdAt: this.createdAt,
permissions: this.permissions, permissions: this.permissions,
librariesAccessible: [...this.librariesAccessible], librariesAccessible: [...this.librariesAccessible],
itemTagsAccessible: [...this.itemTagsAccessible] itemTagsSelected: [...this.itemTagsSelected]
} }
} }
@@ -105,7 +105,7 @@ class User {
createdAt: this.createdAt, createdAt: this.createdAt,
permissions: this.permissions, permissions: this.permissions,
librariesAccessible: [...this.librariesAccessible], librariesAccessible: [...this.librariesAccessible],
itemTagsAccessible: [...this.itemTagsAccessible] itemTagsSelected: [...this.itemTagsSelected]
} }
if (minimal) { if (minimal) {
delete json.mediaProgress delete json.mediaProgress
@@ -169,9 +169,14 @@ class User {
if (this.permissions.accessAllTags === undefined) this.permissions.accessAllTags = true if (this.permissions.accessAllTags === undefined) this.permissions.accessAllTags = true
// Explicit content restriction permission added v2.0.18 // Explicit content restriction permission added v2.0.18
if (this.permissions.accessExplicitContent === undefined) this.permissions.accessExplicitContent = true if (this.permissions.accessExplicitContent === undefined) this.permissions.accessExplicitContent = true
// itemTagsAccessible was renamed to itemTagsSelected in version v2.2.20
if (user.itemTagsAccessible?.length) {
this.permissions.selectedTagsNotAccessible = false
user.itemTagsSelected = user.itemTagsAccessible
}
this.librariesAccessible = [...(user.librariesAccessible || [])] this.librariesAccessible = [...(user.librariesAccessible || [])]
this.itemTagsAccessible = [...(user.itemTagsAccessible || [])] this.itemTagsSelected = [...(user.itemTagsSelected || [])]
} }
update(payload) { update(payload) {
@@ -228,19 +233,21 @@ class User {
// Update accessible tags // Update accessible tags
if (this.permissions.accessAllTags) { if (this.permissions.accessAllTags) {
// Access all tags // Access all tags
if (this.itemTagsAccessible.length) { if (this.itemTagsSelected.length) {
this.itemTagsAccessible = [] this.itemTagsSelected = []
this.permissions.selectedTagsNotAccessible = false
hasUpdates = true hasUpdates = true
} }
} else if (payload.itemTagsAccessible !== undefined) { } else if (payload.itemTagsSelected !== undefined) {
if (payload.itemTagsAccessible.length) { if (payload.itemTagsSelected.length) {
if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) { if (payload.itemTagsSelected.join(',') !== this.itemTagsSelected.join(',')) {
hasUpdates = true hasUpdates = true
this.itemTagsAccessible = [...payload.itemTagsAccessible] this.itemTagsSelected = [...payload.itemTagsSelected]
} }
} else if (this.itemTagsAccessible.length > 0) { } else if (this.itemTagsSelected.length > 0) {
hasUpdates = true hasUpdates = true
this.itemTagsAccessible = [] this.itemTagsSelected = []
this.permissions.selectedTagsNotAccessible = false
} }
} }
return hasUpdates return hasUpdates
@@ -343,8 +350,12 @@ class User {
checkCanAccessLibraryItemWithTags(tags) { checkCanAccessLibraryItemWithTags(tags) {
if (this.permissions.accessAllTags) return true if (this.permissions.accessAllTags) return true
if (!tags || !tags.length) return false if (this.permissions.selectedTagsNotAccessible) {
return this.itemTagsAccessible.some(tag => tags.includes(tag)) if (!tags?.length) return true
return tags.every(tag => !this.itemTagsSelected.includes(tag))
}
if (!tags?.length) return false
return this.itemTagsSelected.some(tag => tags.includes(tag))
} }
checkCanAccessLibraryItem(libraryItem) { checkCanAccessLibraryItem(libraryItem) {
+23
View File
@@ -0,0 +1,23 @@
const axios = require('axios')
const Logger = require('../Logger')
class AudiobookCovers {
constructor() { }
async search(search) {
const url = `https://api.audiobookcovers.com/cover/bytext/`
const params = new URLSearchParams([['q', search]])
const items = await axios.get(url, { params }).then((res) => {
if (!res || !res.data) return []
return res.data
}).catch(error => {
Logger.error('[AudiobookCovers] Cover search error', error)
return []
})
return items.map(item => ({ cover: item.filename }))
}
}
module.exports = AudiobookCovers
+57
View File
@@ -0,0 +1,57 @@
const express = require('express')
const Path = require('path')
const childProcess = require('child_process')
const { createProxyMiddleware } = require('http-proxy-middleware');
const Logger = require('../Logger')
class ClientRouter {
constructor(appRoot, clientPort = 3000, routerBasePath = '') {
this.appRoot = appRoot
this.clientPort = clientPort
this.routerBasePath = routerBasePath
this.client = null
this.router = express()
this.router.disable('x-powered-by')
this.init()
}
init () {
const target = `http://localhost:${this.clientPort}${this.routerBasePath || '/'}`
this.router.use(createProxyMiddleware({ target, changeOrigin: true }));
Logger.info(`[Client] Proxying requests to client on port :${this.clientPort}`)
}
start () {
const clientDir = Path.join(this.appRoot, '/client')
const clientPath = Path.join(clientDir, '/node_modules/@nuxt/cli/bin/nuxt-cli.js')
this.client = childProcess.fork(clientPath, ["start"], { cwd: clientDir, stdio: 'pipe' })
Logger.info(`[Client] Client started under port :${this.clientPort}`)
this.client.stdout.on('data', (data) => {
Logger.info('[Client]', data.toString().trim())
})
this.client.stderr.on('data', (data) => {
Logger.error('[Client]', data.toString().trim())
})
this.client.on('exit', () => {
Logger.info('[Client] Client exited unexpectedly, restarting...')
this.start()
})
}
stop () {
if (this.client) {
this.client.off('exit')
this.client.kill()
} else {
Logger.error('Client not running')
}
}
}
module.exports = ClientRouter
+5
View File
@@ -25,6 +25,11 @@ module.exports = function areEquivalent(value1, value2, stack = []) {
return true; return true;
} }
// Truthy check to handle value1=null, value2=Object
if ((value1 && !value2) || (!value1 && value2)) {
return false
}
const type1 = typeof value1; const type1 = typeof value1;
// Ensure types match // Ensure types match
-1
View File
@@ -92,7 +92,6 @@ module.exports.setDefault = (path, silent = false) => {
const gid = global.Gid const gid = global.Gid
return new Promise((resolve) => { return new Promise((resolve) => {
if (isNaN(uid) || isNaN(gid)) { if (isNaN(uid) || isNaN(gid)) {
if (!silent) Logger.debug('Not modifying permissions since no uid/gid is specified')
return resolve() return resolve()
} }
if (!silent) Logger.debug(`Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`) if (!silent) Logger.debug(`Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
+17 -13
View File
@@ -174,20 +174,24 @@ async function recurseFiles(path, relPathToReplace = null) {
} }
module.exports.recurseFiles = recurseFiles module.exports.recurseFiles = recurseFiles
module.exports.downloadFile = async (url, filepath) => { module.exports.downloadFile = (url, filepath) => {
Logger.debug(`[fileUtils] Downloading file to ${filepath}`) return new Promise(async (resolve, reject) => {
Logger.debug(`[fileUtils] Downloading file to ${filepath}`)
axios({
url,
method: 'GET',
responseType: 'stream',
timeout: 30000
}).then((response) => {
const writer = fs.createWriteStream(filepath)
response.data.pipe(writer)
const writer = fs.createWriteStream(filepath) writer.on('finish', resolve)
const response = await axios({ writer.on('error', reject)
url, }).catch((err) => {
method: 'GET', Logger.error(`[fileUtils] Failed to download file "${filepath}"`, err)
responseType: 'stream', reject(err)
timeout: 30000 })
})
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
}) })
} }
+2
View File
@@ -85,6 +85,8 @@ function extractEpisodeData(item) {
} }
} }
episode.enclosure.url = episode.enclosure.url.trim()
// Full description with html // Full description with html
if (item['content:encoded']) { if (item['content:encoded']) {
const rawDescription = (extractFirstArrayItem(item, 'content:encoded') || '').trim() const rawDescription = (extractFirstArrayItem(item, 'content:encoded') || '').trim()