mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-02 00:40:39 +02:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c58a6b9047 | |||
| b787fb18f3 | |||
| 17cce9c914 | |||
| 90299e348c | |||
| fe25a1bc54 | |||
| edbe1851b5 | |||
| ad6c5a4f00 | |||
| 4971787482 | |||
| 56d2ec9c22 | |||
| 106ddc9541 | |||
| 4d93e39fa9 | |||
| 54b41b15c2 | |||
| 54ca42a903 | |||
| d7cc8a052a | |||
| 5165f11460 | |||
| b47ce4fb24 | |||
| 9b1f7f566f | |||
| 10295b000a | |||
| c06d734d5e | |||
| 49a69193d8 | |||
| 7852804a9c | |||
| 415dda37a4 | |||
| 179d339afd | |||
| 858c1a7353 | |||
| 0b42b81558 | |||
| f9678dec2f | |||
| 82642b295c | |||
| ba3d84a924 | |||
| 96e2f934a3 | |||
| a68ade2b3d | |||
| 4fcdeda447 | |||
| dc03835742 | |||
| 50430e6b27 | |||
| d130dd6d5e | |||
| 793cc989de | |||
| 27d8c4d67c | |||
| 48f493a9f5 | |||
| 04992ee3fb | |||
| 4d8e2a1279 | |||
| 2af7b6b6f1 | |||
| e59351566d | |||
| 05d10b73c3 | |||
| 41e192c6a5 | |||
| ea42ab7624 | |||
| 2d9035d90b | |||
| 0ae853c119 | |||
| 3c0fdff7b4 | |||
| eede2bbd46 | |||
| 5c31687a0f | |||
| 6b654d3c2d | |||
| 91cbe45839 | |||
| 7883d4a97f | |||
| 9f4547cff8 | |||
| a98106593d | |||
| c625b3f08c | |||
| 9e7f09c21b | |||
| 616caecdf1 | |||
| cee19c5128 | |||
| 67db41a525 | |||
| 3ea3e55d17 | |||
| 4959a28485 | |||
| 6d2482a98e | |||
| 4b23b842bb | |||
| 07bebc8808 | |||
| 027d7f7a5b | |||
| 6baa0fa047 | |||
| 8425fac543 | |||
| 7b2ac7b9e9 | |||
| c9ab2a242d | |||
| 13532cba14 | |||
| 3fb2bd3362 | |||
| e80c3a1c5a | |||
| e04d26307e | |||
| b8f74e1c98 | |||
| 0851050392 | |||
| b84882d9d1 | |||
| cd37a7618e |
@@ -11,7 +11,6 @@ ExecReload=/bin/kill -HUP $MAINPID
|
|||||||
Restart=always
|
Restart=always
|
||||||
User=audiobookshelf
|
User=audiobookshelf
|
||||||
Group=audiobookshelf
|
Group=audiobookshelf
|
||||||
PermissionsStartOnly=true
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-16 bg-primary relative">
|
<div class="w-full h-16 bg-primary relative">
|
||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="~static/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
<img src="~static/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
||||||
@@ -25,15 +25,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||||
@@ -62,7 +68,7 @@
|
|||||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<template v-if="userCanUpdate">
|
<template v-if="userCanUpdate">
|
||||||
<ui-tooltip text="Edit" direction="bottom">
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
<ui-icon-btn :disabled="processingBatch" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@@ -116,7 +122,7 @@ export default {
|
|||||||
return this.$store.state.globals.selectedMediaItems
|
return this.$store.state.globals.selectedMediaItems
|
||||||
},
|
},
|
||||||
selectedMediaItemsArePlayable() {
|
selectedMediaItemsArePlayable() {
|
||||||
return !this.selectedMediaItems.some(i => !i.hasTracks)
|
return !this.selectedMediaItems.some((i) => !i.hasTracks)
|
||||||
},
|
},
|
||||||
userMediaProgress() {
|
userMediaProgress() {
|
||||||
return this.$store.state.user.user.mediaProgress || []
|
return this.$store.state.user.user.mediaProgress || []
|
||||||
@@ -158,12 +164,15 @@ export default {
|
|||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
|
|
||||||
const libraryItemIds = this.selectedMediaItems.map((i) => i.id)
|
const libraryItemIds = this.selectedMediaItems.map((i) => i.id)
|
||||||
const libraryItems = await this.$axios.$post(`/api/items/batch/get`, { libraryItemIds }).catch((error) => {
|
const libraryItems = await this.$axios
|
||||||
const errorMsg = error.response.data || 'Failed to get items'
|
.$post(`/api/items/batch/get`, { libraryItemIds })
|
||||||
console.error(errorMsg, error)
|
.then((res) => res.libraryItems)
|
||||||
this.$toast.error(errorMsg)
|
.catch((error) => {
|
||||||
return []
|
const errorMsg = error.response.data || 'Failed to get items'
|
||||||
})
|
console.error(errorMsg, error)
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
if (!libraryItems.length) {
|
if (!libraryItems.length) {
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
|
|||||||
@@ -405,8 +405,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeListeners() {
|
removeListeners() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('user_updated', this.userUpdated)
|
this.$root.socket.off('user_updated', this.userUpdated)
|
||||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
<!-- Series books page -->
|
<!-- Series books page -->
|
||||||
<template v-if="selectedSeries">
|
<template v-if="selectedSeries">
|
||||||
<p class="pl-2 font-book text-base md:text-lg">
|
<p class="pl-2 font-book text-base md:text-lg">
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||||
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
||||||
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
||||||
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="seriesSortBy" :descending.sync="seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||||
|
|
||||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -219,30 +219,6 @@ export default {
|
|||||||
},
|
},
|
||||||
isIssuesFilter() {
|
isIssuesFilter() {
|
||||||
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
||||||
},
|
|
||||||
seriesSortBy: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesSortBy
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesSortBy', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seriesSortDesc: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesSortDesc
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesSortDesc', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seriesFilterBy: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesFilterBy
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesFilterBy', val)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -339,10 +315,10 @@ export default {
|
|||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateSeriesSort() {
|
updateSeriesSort() {
|
||||||
this.$eventBus.$emit('series-sort-updated')
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateSeriesFilter() {
|
updateSeriesFilter() {
|
||||||
this.$eventBus.$emit('series-sort-updated')
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateCollapseSeries() {
|
updateCollapseSeries() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
@@ -367,11 +343,11 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ export default {
|
|||||||
id: 'config-notifications',
|
id: 'config-notifications',
|
||||||
title: this.$strings.HeaderNotifications,
|
title: this.$strings.HeaderNotifications,
|
||||||
path: '/config/notifications'
|
path: '/config/notifications'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'config-item-metadata-utils',
|
||||||
|
title: this.$strings.HeaderItemMetadataUtils,
|
||||||
|
path: '/config/item-metadata-utils'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -100,13 +100,13 @@ export default {
|
|||||||
return this.page
|
return this.page
|
||||||
},
|
},
|
||||||
seriesSortBy() {
|
seriesSortBy() {
|
||||||
return this.$store.state.libraries.seriesSortBy
|
return this.$store.getters['user/getUserSetting']('seriesSortBy')
|
||||||
},
|
},
|
||||||
seriesSortDesc() {
|
seriesSortDesc() {
|
||||||
return this.$store.state.libraries.seriesSortDesc
|
return this.$store.getters['user/getUserSetting']('seriesSortDesc')
|
||||||
},
|
},
|
||||||
seriesFilterBy() {
|
seriesFilterBy() {
|
||||||
return this.$store.state.libraries.seriesFilterBy
|
return this.$store.getters['user/getUserSetting']('seriesFilterBy')
|
||||||
},
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
@@ -163,7 +163,7 @@ export default {
|
|||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
bookHeight() {
|
bookHeight() {
|
||||||
@@ -230,7 +230,7 @@ export default {
|
|||||||
},
|
},
|
||||||
selectEntity(entity, shiftKey) {
|
selectEntity(entity, shiftKey) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
const indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
||||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||||
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||||
this.lastItemIndexSelected = indexOf
|
this.lastItemIndexSelected = indexOf
|
||||||
@@ -239,14 +239,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||||
var loopStart = indexOf
|
let loopStart = indexOf
|
||||||
var loopEnd = lastLastItemIndexSelected
|
let loopEnd = lastLastItemIndexSelected
|
||||||
if (indexOf > lastLastItemIndexSelected) {
|
if (indexOf > lastLastItemIndexSelected) {
|
||||||
loopStart = lastLastItemIndexSelected
|
loopStart = lastLastItemIndexSelected
|
||||||
loopEnd = indexOf
|
loopEnd = indexOf
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSelecting = false
|
let isSelecting = false
|
||||||
// If any items in this range is not selected then select all otherwise unselect all
|
// If any items in this range is not selected then select all otherwise unselect all
|
||||||
for (let i = loopStart; i <= loopEnd; i++) {
|
for (let i = loopStart; i <= loopEnd; i++) {
|
||||||
const thisEntity = this.entities[i]
|
const thisEntity = this.entities[i]
|
||||||
@@ -275,6 +275,7 @@ export default {
|
|||||||
mediaType: thisEntity.mediaType,
|
mediaType: thisEntity.mediaType,
|
||||||
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||||
}
|
}
|
||||||
|
console.log('Setting media item selected', mediaItem, 'Num Selected=', this.selectedMediaItems.length)
|
||||||
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid entity index', i)
|
console.error('Invalid entity index', i)
|
||||||
@@ -497,7 +498,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
var wasUpdated = this.checkUpdateSearchParams()
|
const wasUpdated = this.checkUpdateSearchParams()
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
||||||
@@ -666,11 +667,9 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$eventBus.$on('series-sort-updated', this.seriesSortUpdated)
|
|
||||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$on('socket_init', this.socketInit)
|
this.$eventBus.$on('socket_init', this.socketInit)
|
||||||
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
@@ -695,11 +694,9 @@ export default {
|
|||||||
bookshelf.removeEventListener('scroll', this.scroll)
|
bookshelf.removeEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$off('series-sort-updated', this.seriesSortUpdated)
|
|
||||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$off('socket_init', this.socketInit)
|
this.$eventBus.$off('socket_init', this.socketInit)
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- <div class="w-20 bg-bg h-full relative box-shadow-side z-40" style="min-width: 80px"> -->
|
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
||||||
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-40" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
|
||||||
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
||||||
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-50 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
|
|||||||
@@ -13,10 +13,14 @@
|
|||||||
|
|
||||||
<!-- Search icon btn -->
|
<!-- Search icon btn -->
|
||||||
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
||||||
<span class="material-icons text-lg">search</span>
|
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
|
<span class="material-icons text-lg">search</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
||||||
<span class="material-icons text-lg">edit</span>
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
|
<span class="material-icons text-lg">edit</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading spinner -->
|
<!-- Loading spinner -->
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -348,6 +348,10 @@ export default {
|
|||||||
{
|
{
|
||||||
id: 'language',
|
id: 'language',
|
||||||
name: this.$strings.LabelLanguage
|
name: this.$strings.LabelLanguage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cover',
|
||||||
|
name: this.$strings.LabelCover
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -201,8 +201,8 @@ export default {
|
|||||||
this.loadingTags = true
|
this.loadingTags = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/tags`)
|
.$get(`/api/tags`)
|
||||||
.then((tags) => {
|
.then((res) => {
|
||||||
this.tags = tags
|
this.tags = res.tags
|
||||||
this.loadingTags = false
|
this.loadingTags = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-2 p-1">
|
<div class="flex justify-end mt-2 p-1">
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default {
|
|||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 50
|
default: 60
|
||||||
},
|
},
|
||||||
bgOpacity: {
|
bgOpacity: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<div class="flex pt-2 px-2">
|
<div class="flex pt-2 px-2">
|
||||||
<ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
<ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -303,11 +303,14 @@ export default {
|
|||||||
this.persistProvider()
|
this.persistProvider()
|
||||||
|
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
var searchQuery = this.getSearchQuery()
|
const searchQuery = this.getSearchQuery()
|
||||||
var results = await this.$axios.$get(`/api/search/covers?${searchQuery}`).catch((error) => {
|
const results = await this.$axios
|
||||||
console.error('Failed', error)
|
.$get(`/api/search/covers?${searchQuery}`)
|
||||||
return []
|
.then((res) => res.results)
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.coversFound = results
|
this.coversFound = results
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.hasSearched = true
|
this.hasSearched = true
|
||||||
|
|||||||
@@ -306,13 +306,13 @@ export default {
|
|||||||
this.runSearch()
|
this.runSearch()
|
||||||
},
|
},
|
||||||
async runSearch() {
|
async runSearch() {
|
||||||
var searchQuery = this.getSearchQuery()
|
const searchQuery = this.getSearchQuery()
|
||||||
if (this.lastSearch === searchQuery) return
|
if (this.lastSearch === searchQuery) return
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.lastSearch = searchQuery
|
this.lastSearch = searchQuery
|
||||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
const searchEntity = this.isPodcast ? 'podcast' : 'books'
|
||||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
let results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -335,8 +335,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.hasSearched = true
|
this.hasSearched = true
|
||||||
},
|
},
|
||||||
init() {
|
initSelectedMatchUsage() {
|
||||||
this.clearSelectedMatch()
|
|
||||||
this.selectedMatchUsage = {
|
this.selectedMatchUsage = {
|
||||||
title: true,
|
title: true,
|
||||||
subtitle: true,
|
subtitle: true,
|
||||||
@@ -360,6 +359,27 @@ export default {
|
|||||||
releaseDate: true
|
releaseDate: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load saved selected match from local storage
|
||||||
|
try {
|
||||||
|
let savedSelectedMatchUsage = localStorage.getItem('selectedMatchUsage')
|
||||||
|
if (!savedSelectedMatchUsage) return
|
||||||
|
savedSelectedMatchUsage = JSON.parse(savedSelectedMatchUsage)
|
||||||
|
|
||||||
|
for (const key in savedSelectedMatchUsage) {
|
||||||
|
if (this.selectedMatchUsage[key] !== undefined) {
|
||||||
|
this.selectedMatchUsage[key] = !!savedSelectedMatchUsage[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load saved selectedMatchUsage', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkboxToggled()
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.clearSelectedMatch()
|
||||||
|
this.initSelectedMatchUsage()
|
||||||
|
|
||||||
if (this.libraryItem.id !== this.libraryItemId) {
|
if (this.libraryItem.id !== this.libraryItemId) {
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.hasSearched = false
|
this.hasSearched = false
|
||||||
@@ -465,11 +485,14 @@ export default {
|
|||||||
console.log('Match payload', updatePayload)
|
console.log('Match payload', updatePayload)
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
|
// Persist in local storage
|
||||||
|
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
|
||||||
|
|
||||||
if (updatePayload.metadata.cover) {
|
if (updatePayload.metadata.cover) {
|
||||||
var coverPayload = {
|
const coverPayload = {
|
||||||
url: updatePayload.metadata.cover
|
url: updatePayload.metadata.cover
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -483,8 +506,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
if (Object.keys(updatePayload).length) {
|
||||||
var mediaUpdatePayload = updatePayload
|
const mediaUpdatePayload = updatePayload
|
||||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -502,6 +525,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
},
|
},
|
||||||
clearSelectedMatch() {
|
clearSelectedMatch() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-1 md:px-4 py-2 mb-4">
|
<div class="w-full h-full md:px-4 py-2 mb-4">
|
||||||
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
|
<div v-if="!showDirectoryPicker" class="w-full h-full md:py-4">
|
||||||
<div class="flex flex-wrap md:flex-nowrap -mx-1">
|
<div class="flex flex-wrap md:flex-nowrap -mx-1 mb-2">
|
||||||
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full py-4">
|
<div class="folders-container overflow-y-auto w-full py-2 mb-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
@@ -140,3 +140,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.folders-container {
|
||||||
|
max-height: calc(80vh - 192px);
|
||||||
|
}
|
||||||
|
@media (max-device-width: 768px) {
|
||||||
|
.folders-container {
|
||||||
|
max-height: calc(80vh - 292px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-2 md:px-4 w-full text-sm pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
||||||
|
|||||||
@@ -234,13 +234,10 @@ export default {
|
|||||||
this.showChaptersModal = false
|
this.showChaptersModal = false
|
||||||
},
|
},
|
||||||
setUseChapterTrack() {
|
setUseChapterTrack() {
|
||||||
var useChapterTrack = !this.useChapterTrack
|
this.useChapterTrack = !this.useChapterTrack
|
||||||
this.useChapterTrack = useChapterTrack
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(useChapterTrack)
|
|
||||||
|
|
||||||
this.$store.dispatch('user/updateUserSettings', { useChapterTrack }).catch((err) => {
|
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
|
||||||
console.error('Failed to update settings', err)
|
|
||||||
})
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
},
|
},
|
||||||
checkUpdateChapterTrack() {
|
checkUpdateChapterTrack() {
|
||||||
@@ -311,7 +308,7 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
var _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||||
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
||||||
|
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
@@ -345,13 +342,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'audioplayer', meth: this.settingsUpdated })
|
|
||||||
this.init()
|
|
||||||
this.$eventBus.$on('player-hotkey', this.hotkey)
|
this.$eventBus.$on('player-hotkey', this.hotkey)
|
||||||
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
|
this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'audioplayer')
|
|
||||||
this.$eventBus.$off('player-hotkey', this.hotkey)
|
this.$eventBus.$off('player-hotkey', this.hotkey)
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-50 bg-primary text-white">
|
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 bg-primary text-white">
|
||||||
<div class="absolute top-4 right-4 z-20">
|
<div class="absolute top-4 right-4 z-20">
|
||||||
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -109,8 +109,8 @@ export default {
|
|||||||
loadUsers() {
|
loadUsers() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users')
|
||||||
.then((users) => {
|
.then((res) => {
|
||||||
this.users = users.sort((a, b) => {
|
this.users = res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="librariesTable">
|
<div>
|
||||||
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
||||||
<template v-for="library in libraryCopies">
|
<template v-for="library in libraryCopies">
|
||||||
<div :key="library.id" class="item">
|
<div :key="library.id" class="item">
|
||||||
@@ -82,10 +82,10 @@ export default {
|
|||||||
})
|
})
|
||||||
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
||||||
if (currOrder !== newOrder) {
|
if (currOrder !== newOrder) {
|
||||||
this.$axios.$post('/api/libraries/order', libraryOrderData).then((libraries) => {
|
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
|
||||||
if (libraries && libraries.length) {
|
if (response.libraries && response.libraries.length) {
|
||||||
this.$toast.success('Library order saved', { timeout: 1500 })
|
this.$toast.success('Library order saved', { timeout: 1500 })
|
||||||
this.$store.commit('libraries/set', libraries)
|
this.$store.commit('libraries/set', response.libraries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
|
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
|
<span class="material-icons">more_vert</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="menu">
|
||||||
|
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 w-48 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm">
|
||||||
|
<template v-for="(item, index) in items">
|
||||||
|
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||||
|
<p>{{ item.text }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
disabled: Boolean,
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clickOutsideObj: {
|
||||||
|
handler: this.clickedOutside,
|
||||||
|
events: ['mousedown'],
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
showMenu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
},
|
||||||
|
clickedOutside() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
clickAction(action) {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = false
|
||||||
|
this.$emit('action', action)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -113,10 +113,13 @@ export default {
|
|||||||
if (this.searching) return
|
if (this.searching) return
|
||||||
this.currentSearch = this.textInput
|
this.currentSearch = this.textInput
|
||||||
this.searching = true
|
this.searching = true
|
||||||
var results = await this.$axios.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15&token=${this.userToken}`).catch((error) => {
|
const results = await this.$axios
|
||||||
console.error('Failed to get search results', error)
|
.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15&token=${this.userToken}`)
|
||||||
return []
|
.then((res) => res.results || res)
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed to get search results', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.items = results || []
|
this.items = results || []
|
||||||
this.searching = false
|
this.searching = false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -137,16 +137,33 @@ export default {
|
|||||||
author: (this.details.authors || []).map((au) => au.name).join(', ')
|
author: (this.details.authors || []).map((au) => au.name).join(', ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mapBatchDetails(batchDetails) {
|
mapBatchDetails(batchDetails, mapType = 'overwrite') {
|
||||||
for (const key in batchDetails) {
|
for (const key in batchDetails) {
|
||||||
if (key === 'tags') {
|
if (mapType === 'append') {
|
||||||
this.newTags = [...batchDetails.tags]
|
if (key === 'tags') {
|
||||||
} else if (key === 'genres' || key === 'narrators') {
|
// Concat and remove dupes
|
||||||
this.details[key] = [...batchDetails[key]]
|
this.newTags = [...new Set(this.newTags.concat(batchDetails.tags))]
|
||||||
} else if (key === 'authors' || key === 'series') {
|
} else if (key === 'genres' || key === 'narrators') {
|
||||||
this.details[key] = batchDetails[key].map((i) => ({ ...i }))
|
// Concat and remove dupes
|
||||||
|
this.details[key] = [...new Set(this.details[key].concat(batchDetails[key]))]
|
||||||
|
} else if (key === 'authors' || key === 'series') {
|
||||||
|
batchDetails[key].forEach((detail) => {
|
||||||
|
const existingDetail = this.details[key].find((_d) => _d.name.toLowerCase() == detail.name.toLowerCase().trim() || _d.id == detail.id)
|
||||||
|
if (!existingDetail) {
|
||||||
|
this.details[key].push({ ...detail })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.details[key] = batchDetails[key]
|
if (key === 'tags') {
|
||||||
|
this.newTags = [...batchDetails.tags]
|
||||||
|
} else if (key === 'genres' || key === 'narrators') {
|
||||||
|
this.details[key] = [...batchDetails[key]]
|
||||||
|
} else if (key === 'authors' || key === 'series') {
|
||||||
|
this.details[key] = batchDetails[key].map((i) => ({ ...i }))
|
||||||
|
} else {
|
||||||
|
this.details[key] = batchDetails[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -107,14 +107,24 @@ export default {
|
|||||||
author: this.details.author
|
author: this.details.author
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mapBatchDetails(batchDetails) {
|
mapBatchDetails(batchDetails, mapType = 'overwrite') {
|
||||||
for (const key in batchDetails) {
|
for (const key in batchDetails) {
|
||||||
if (key === 'tags') {
|
if (mapType === 'append') {
|
||||||
this.newTags = [...batchDetails.tags]
|
if (key === 'tags') {
|
||||||
} else if (key === 'genres') {
|
// Concat and remove dupes
|
||||||
this.details[key] = [...batchDetails[key]]
|
this.newTags = [...new Set(this.newTags.concat(batchDetails.tags))]
|
||||||
|
} else if (key === 'genres') {
|
||||||
|
// Concat and remove dupes
|
||||||
|
this.details[key] = [...new Set(this.details[key].concat(batchDetails[key]))]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.details[key] = batchDetails[key]
|
if (key === 'tags') {
|
||||||
|
this.newTags = [...batchDetails.tags]
|
||||||
|
} else if (key === 'genres') {
|
||||||
|
this.details[key] = [...batchDetails[key]]
|
||||||
|
} else {
|
||||||
|
this.details[key] = batchDetails[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -280,7 +280,6 @@ export default {
|
|||||||
userUpdated(user) {
|
userUpdated(user) {
|
||||||
if (this.$store.state.user.user.id === user.id) {
|
if (this.$store.state.user.user.id === user.id) {
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
this.$store.commit('user/setSettings', user.settings)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userOnline(user) {
|
userOnline(user) {
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
|
offline: false,
|
||||||
|
cacheAssets: false,
|
||||||
preCaching: [],
|
preCaching: [],
|
||||||
runtimeCaching: []
|
runtimeCaching: []
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.7",
|
"version": "2.2.9",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.7",
|
"version": "2.2.9",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.7",
|
"version": "2.2.9",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ export default {
|
|||||||
for (let i = 0; i < this.newChapters.length; i++) {
|
for (let i = 0; i < this.newChapters.length; i++) {
|
||||||
this.newChapters[i].id = i
|
this.newChapters[i].id = i
|
||||||
this.newChapters[i].start = Number(this.newChapters[i].start)
|
this.newChapters[i].start = Number(this.newChapters[i].start)
|
||||||
|
this.newChapters[i].title = (this.newChapters[i].title || '').trim()
|
||||||
|
|
||||||
if (i === 0 && this.newChapters[i].start !== 0) {
|
if (i === 0 && this.newChapters[i].start !== 0) {
|
||||||
this.newChapters[i].error = this.$strings.MessageChapterErrorFirstNotZero
|
this.newChapters[i].error = this.$strings.MessageChapterErrorFirstNotZero
|
||||||
@@ -508,22 +509,25 @@ export default {
|
|||||||
|
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
applyChapterData() {
|
applyChapterData() {
|
||||||
var index = 0
|
let index = 0
|
||||||
this.newChapters = this.chapterData.chapters
|
this.newChapters = this.chapterData.chapters
|
||||||
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
||||||
.map((chap) => {
|
.map((chap) => {
|
||||||
var chapEnd = Math.min(this.mediaDuration, (chap.startOffsetMs + chap.lengthMs) / 1000)
|
|
||||||
return {
|
return {
|
||||||
id: index++,
|
id: index++,
|
||||||
start: chap.startOffsetMs / 1000,
|
start: chap.startOffsetMs / 1000,
|
||||||
end: chapEnd,
|
end: Math.min(this.mediaDuration, (chap.startOffsetMs + chap.lengthMs) / 1000),
|
||||||
title: chap.title
|
title: chap.title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
findChapters() {
|
findChapters() {
|
||||||
if (!this.asinInput) {
|
if (!this.asinInput) {
|
||||||
|
|||||||
@@ -4,12 +4,23 @@
|
|||||||
<div class="flex items-center px-4 py-4 cursor-pointer" @click="openMapOptions = !openMapOptions" @mousedown.prevent @mouseup.prevent>
|
<div class="flex items-center px-4 py-4 cursor-pointer" @click="openMapOptions = !openMapOptions" @mousedown.prevent @mouseup.prevent>
|
||||||
<span class="material-icons text-2xl">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
<span class="material-icons text-2xl">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
||||||
|
|
||||||
<p class="ml-4 text-gray-200 text-lg">Map details</p>
|
<p class="ml-4 text-gray-200 text-lg">{{ $strings.HeaderMapDetails }}</p>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<div class="w-64 flex">
|
||||||
|
<button class="w-32 h-8 rounded-l-md shadow-md border border-gray-600" :class="!isMapOverwrite ? 'bg-bg text-white/30' : 'bg-primary'" @click.stop.prevent="mapDetailsType = 'overwrite'">
|
||||||
|
<p class="text-sm">{{ $strings.LabelOverwrite }}</p>
|
||||||
|
</button>
|
||||||
|
<button class="w-32 h-8 rounded-r-md shadow-md border border-gray-600" :class="!isMapAppend ? 'bg-bg text-white/30' : 'bg-primary'" @click.stop.prevent="mapDetailsType = 'append'">
|
||||||
|
<p class="text-sm">{{ $strings.LabelAppend }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
<div v-if="openMapOptions" class="flex flex-wrap">
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
||||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
@@ -18,13 +29,13 @@
|
|||||||
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
||||||
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
|
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||||
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
|
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.genres" />
|
<ui-checkbox v-model="selectedBatchUsage.genres" />
|
||||||
@@ -38,15 +49,15 @@
|
|||||||
<ui-checkbox v-model="selectedBatchUsage.narrators" />
|
<ui-checkbox v-model="selectedBatchUsage.narrators" />
|
||||||
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
|
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
||||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.language" />
|
<ui-checkbox v-model="selectedBatchUsage.language" />
|
||||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<ui-checkbox
|
<ui-checkbox
|
||||||
@@ -96,11 +107,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const libraryItemIds = store.state.globals.selectedMediaItems.map((i) => i.id)
|
const libraryItemIds = store.state.globals.selectedMediaItems.map((i) => i.id)
|
||||||
const libraryItems = await app.$axios.$post(`/api/items/batch/get`, { libraryItemIds }).catch((error) => {
|
const libraryItems = await app.$axios
|
||||||
const errorMsg = error.response.data || 'Failed to get items'
|
.$post(`/api/items/batch/get`, { libraryItemIds })
|
||||||
console.error(errorMsg, error)
|
.then((res) => res.libraryItems)
|
||||||
return []
|
.catch((error) => {
|
||||||
})
|
const errorMsg = error.response.data || 'Failed to get items'
|
||||||
|
console.error(errorMsg, error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
mediaType: libraryItems[0].mediaType,
|
mediaType: libraryItems[0].mediaType,
|
||||||
libraryItems
|
libraryItems
|
||||||
@@ -111,10 +125,10 @@ export default {
|
|||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
libraryItemCopies: [],
|
libraryItemCopies: [],
|
||||||
isScrollable: false,
|
isScrollable: false,
|
||||||
newSeriesNames: [],
|
|
||||||
newTagItems: [],
|
newTagItems: [],
|
||||||
newGenreItems: [],
|
newGenreItems: [],
|
||||||
newNarratorItems: [],
|
newNarratorItems: [],
|
||||||
|
mapDetailsType: 'overwrite',
|
||||||
batchDetails: {
|
batchDetails: {
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
authors: null,
|
authors: null,
|
||||||
@@ -139,10 +153,17 @@ export default {
|
|||||||
language: false,
|
language: false,
|
||||||
explicit: false
|
explicit: false
|
||||||
},
|
},
|
||||||
|
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
||||||
openMapOptions: false
|
openMapOptions: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMapOverwrite() {
|
||||||
|
return this.mapDetailsType === 'overwrite'
|
||||||
|
},
|
||||||
|
isMapAppend() {
|
||||||
|
return this.mapDetailsType === 'append'
|
||||||
|
},
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
@@ -155,9 +176,6 @@ export default {
|
|||||||
tagItems() {
|
tagItems() {
|
||||||
return this.tags.concat(this.newTagItems)
|
return this.tags.concat(this.newTagItems)
|
||||||
},
|
},
|
||||||
seriesItems() {
|
|
||||||
return [...this.existingSeriesNames, ...this.newSeriesNames]
|
|
||||||
},
|
|
||||||
narratorItems() {
|
narratorItems() {
|
||||||
return [...this.narrators, ...this.newNarratorItems]
|
return [...this.narrators, ...this.newNarratorItems]
|
||||||
},
|
},
|
||||||
@@ -216,31 +234,32 @@ export default {
|
|||||||
mapBatchDetails() {
|
mapBatchDetails() {
|
||||||
this.blurBatchForm()
|
this.blurBatchForm()
|
||||||
|
|
||||||
var batchMapPayload = {}
|
const batchMapPayload = {}
|
||||||
for (const key in this.selectedBatchUsage) {
|
for (const key in this.selectedBatchUsage) {
|
||||||
if (this.selectedBatchUsage[key]) {
|
if (!this.selectedBatchUsage[key]) continue
|
||||||
if (key === 'series') {
|
if (this.isMapAppend && !this.appendableKeys.includes(key)) continue
|
||||||
// Map string of series to series objects
|
|
||||||
batchMapPayload[key] = this.batchDetails[key].map((seItem) => {
|
if (key === 'series') {
|
||||||
var existingSeries = this.series.find((se) => se.name.toLowerCase() === seItem.toLowerCase().trim())
|
// Map string of series to series objects
|
||||||
if (existingSeries) {
|
batchMapPayload[key] = this.batchDetails[key].map((seItem) => {
|
||||||
return existingSeries
|
const existingSeries = this.series.find((se) => se.name.toLowerCase() === seItem.toLowerCase().trim())
|
||||||
} else {
|
if (existingSeries) {
|
||||||
return {
|
return existingSeries
|
||||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
} else {
|
||||||
name: seItem
|
return {
|
||||||
}
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
|
name: seItem
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
} else {
|
})
|
||||||
batchMapPayload[key] = this.batchDetails[key]
|
} else {
|
||||||
}
|
batchMapPayload[key] = this.batchDetails[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.libraryItemCopies.forEach((li) => {
|
this.libraryItemCopies.forEach((li) => {
|
||||||
var ref = this.getEditFormRef(li.id)
|
const ref = this.getEditFormRef(li.id)
|
||||||
ref.mapBatchDetails(batchMapPayload)
|
ref.mapBatchDetails(batchMapPayload, this.mapDetailsType)
|
||||||
})
|
})
|
||||||
this.$toast.success('Details mapped')
|
this.$toast.success('Details mapped')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,9 +19,11 @@
|
|||||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
<button type="button" class="h-9 w-9 flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5 mx-px" @click.stop.prevent="editClick">
|
||||||
|
<span class="material-icons text-xl">edit</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
<ui-context-menu-dropdown :items="contextMenuItems" class="mx-px" @action="contextMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-8 max-w-2xl">
|
<div class="my-8 max-w-2xl">
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
<div v-show="processing" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,7 +66,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processingRemove: false
|
processing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -102,15 +104,55 @@ export default {
|
|||||||
},
|
},
|
||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
},
|
||||||
|
contextMenuItems() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
text: this.$strings.MessagePlaylistCreateFromCollection,
|
||||||
|
action: 'create-playlist'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (this.userCanDelete) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.ButtonDelete,
|
||||||
|
action: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'delete') {
|
||||||
|
this.removeClick()
|
||||||
|
} else if (action === 'create-playlist') {
|
||||||
|
this.createPlaylistFromCollection()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createPlaylistFromCollection() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/playlists/collection/${this.collectionId}`)
|
||||||
|
.then((playlist) => {
|
||||||
|
if (playlist) {
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess)
|
||||||
|
this.$router.push(`/playlist/${playlist.id}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || this.$strings.ToastPlaylistCreateFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
this.$store.commit('globals/setEditCollection', this.collection)
|
this.$store.commit('globals/setEditCollection', this.collection)
|
||||||
},
|
},
|
||||||
removeClick() {
|
removeClick() {
|
||||||
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
||||||
this.processingRemove = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/collections/${this.collection.id}`)
|
.$delete(`/api/collections/${this.collection.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -121,7 +163,7 @@ export default {
|
|||||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingRemove = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default {
|
|||||||
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
||||||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
}
|
}
|
||||||
return this.$strings.HeaderSettings
|
return this.$strings.HeaderSettings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8 relative" style="min-height: 200px">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<h1 class="text-xl mx-2">{{ $strings.HeaderManageGenres }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!genres.length && !loading" class="text-center py-8 text-lg">{{ $strings.MessageNoGenres }}</p>
|
||||||
|
|
||||||
|
<div class="border border-white/10">
|
||||||
|
<template v-for="(genre, index) in genres">
|
||||||
|
<div :key="genre" class="w-full p-2 flex items-center text-gray-400 hover:text-white" :class="{ 'bg-primary/20': index % 2 === 0 }">
|
||||||
|
<p v-if="editingGenre !== genre" class="text-sm md:text-base text-gray-100">{{ genre }}</p>
|
||||||
|
<ui-text-input v-else v-model="newGenreName" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<template v-if="editingGenre !== genre">
|
||||||
|
<ui-icon-btn v-if="editingGenre !== genre" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editClick(genre)" />
|
||||||
|
<ui-icon-btn v-if="editingGenre !== genre" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeClick(genre)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-btn color="success" small class="mx-2" @click.stop="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
genres: [],
|
||||||
|
editingGenre: null,
|
||||||
|
newGenreName: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
cancelEditClick() {
|
||||||
|
this.newGenreName = ''
|
||||||
|
this.editingGenre = null
|
||||||
|
},
|
||||||
|
removeClick(genre) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to remove genre "${genre}" from all items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.removeGenre(genre)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
editClick(genre) {
|
||||||
|
this.newGenreName = genre
|
||||||
|
this.editingGenre = genre
|
||||||
|
},
|
||||||
|
saveClick() {
|
||||||
|
this.newGenreName = this.newGenreName.trim()
|
||||||
|
if (!this.newGenreName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingGenre === this.newGenreName) {
|
||||||
|
this.cancelEditClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const genreNameExists = this.genres.find((g) => g !== this.editingGenre && g === this.newGenreName)
|
||||||
|
const genreNameExistsOfDifferentCase = !genreNameExists ? this.genres.find((g) => g !== this.editingGenre && g.toLowerCase() === this.newGenreName.toLowerCase()) : null
|
||||||
|
|
||||||
|
let message = this.$getString('MessageConfirmRenameGenre', [this.editingGenre, this.newGenreName])
|
||||||
|
if (genreNameExists) {
|
||||||
|
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameGenreMergeNote}</span>`
|
||||||
|
} else if (genreNameExistsOfDifferentCase) {
|
||||||
|
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.renameGenre()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
renameGenre() {
|
||||||
|
this.loading = true
|
||||||
|
let _newGenreName = this.newGenreName
|
||||||
|
let _editingGenre = this.editingGenre
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
genre: _editingGenre,
|
||||||
|
newGenre: _newGenreName
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/genres/rename', payload)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
if (res.genreMerged) {
|
||||||
|
this.genres = this.genres.filter((g) => g !== _newGenreName)
|
||||||
|
}
|
||||||
|
this.genres = this.genres.map((g) => {
|
||||||
|
if (g === _editingGenre) return _newGenreName
|
||||||
|
return g
|
||||||
|
})
|
||||||
|
this.cancelEditClick()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to rename genre', error)
|
||||||
|
this.$toast.error('Failed to rename genre')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeGenre(genre) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/genres/${this.$encode(genre)}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
this.genres = this.genres.filter((g) => g !== genre)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove genre', error)
|
||||||
|
this.$toast.error('Failed to remove genre')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/genres')
|
||||||
|
.then((data) => {
|
||||||
|
this.genres = (data.genres || []).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load genres', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<app-settings-content :header-text="'Item Metadata Utils'">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils/tags" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 mt-6 mb-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p>{{ $strings.HeaderManageTags }}</p>
|
||||||
|
<span class="material-icons">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link to="/config/item-metadata-utils/genres" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 my-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p>{{ $strings.HeaderManageGenres }}</p>
|
||||||
|
<span class="material-icons">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</app-settings-content>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
init() {}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8 relative" style="min-height: 200px">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<h1 class="text-xl mx-2">{{ $strings.HeaderManageTags }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!tags.length && !loading" class="text-center py-8 text-lg">{{ $strings.MessageNoTags }}</p>
|
||||||
|
|
||||||
|
<div class="border border-white/10">
|
||||||
|
<template v-for="(tag, index) in tags">
|
||||||
|
<div :key="tag" class="w-full p-2 flex items-center text-gray-400 hover:text-white" :class="{ 'bg-primary/20': index % 2 === 0 }">
|
||||||
|
<p v-if="editingTag !== tag" class="text-sm md:text-base text-gray-100">{{ tag }}</p>
|
||||||
|
<ui-text-input v-else v-model="newTagName" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<template v-if="editingTag !== tag">
|
||||||
|
<ui-icon-btn v-if="editingTag !== tag" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editTagClick(tag)" />
|
||||||
|
<ui-icon-btn v-if="editingTag !== tag" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeTagClick(tag)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-btn color="success" small class="mx-2" @click.stop="saveTagClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
tags: [],
|
||||||
|
editingTag: null,
|
||||||
|
newTagName: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
cancelEditClick() {
|
||||||
|
this.newTagName = ''
|
||||||
|
this.editingTag = null
|
||||||
|
},
|
||||||
|
removeTagClick(tag) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to remove tag "${tag}" from all items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.removeTag(tag)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
saveTagClick() {
|
||||||
|
this.newTagName = this.newTagName.trim()
|
||||||
|
if (!this.newTagName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingTag === this.newTagName) {
|
||||||
|
this.cancelEditClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagNameExists = this.tags.find((t) => t !== this.editingTag && t === this.newTagName)
|
||||||
|
const tagNameExistsOfDifferentCase = !tagNameExists ? this.tags.find((t) => t !== this.editingTag && t.toLowerCase() === this.newTagName.toLowerCase()) : null
|
||||||
|
|
||||||
|
let message = this.$getString('MessageConfirmRenameTag', [this.editingTag, this.newTagName])
|
||||||
|
if (tagNameExists) {
|
||||||
|
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameTagMergeNote}</span>`
|
||||||
|
} else if (tagNameExistsOfDifferentCase) {
|
||||||
|
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.renameTag()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
renameTag() {
|
||||||
|
this.loading = true
|
||||||
|
let _newTagName = this.newTagName
|
||||||
|
let _editingTag = this.editingTag
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
tag: _editingTag,
|
||||||
|
newTag: _newTagName
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/tags/rename', payload)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
if (res.tagMerged) {
|
||||||
|
this.tags = this.tags.filter((t) => t !== _newTagName)
|
||||||
|
}
|
||||||
|
this.tags = this.tags.map((t) => {
|
||||||
|
if (t === _editingTag) return _newTagName
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
this.cancelEditClick()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to rename tag', error)
|
||||||
|
this.$toast.error('Failed to rename tag')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeTag(tag) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/tags/${this.$encode(tag)}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
this.tags = this.tags.filter((t) => t !== tag)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove tag', error)
|
||||||
|
this.$toast.error('Failed to remove tag')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
editTagClick(tag) {
|
||||||
|
this.newTagName = tag
|
||||||
|
this.editingTag = tag
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/tags')
|
||||||
|
.then((data) => {
|
||||||
|
this.tags = (data.tags || []).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load tags', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -61,10 +61,10 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, redirect, app }) {
|
async asyncData({ params, redirect, app }) {
|
||||||
var users = await app.$axios
|
const users = await app.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users')
|
||||||
.then((users) => {
|
.then((res) => {
|
||||||
return users.sort((a, b) => {
|
return res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,10 +48,13 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
async init() {
|
||||||
this.authors = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors`).catch((error) => {
|
this.authors = await this.$axios
|
||||||
console.error('Failed to load authors', error)
|
.$get(`/api/libraries/${this.currentLibraryId}/authors`)
|
||||||
return []
|
.then((response) => response.authors)
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed to load authors', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.loading = false
|
this.loading = false
|
||||||
},
|
},
|
||||||
authorAdded(author) {
|
authorAdded(author) {
|
||||||
|
|||||||
@@ -15,17 +15,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set series sort by
|
// Set series sort by
|
||||||
if (params.id === 'series') {
|
if (query.filter || query.sort || query.desc) {
|
||||||
if (query.sort) {
|
const isSeries = params.id === 'series'
|
||||||
store.commit('libraries/setSeriesSortBy', query.sort)
|
const settingsUpdate = {
|
||||||
store.commit('libraries/setSeriesSortDesc', !!query.desc)
|
[isSeries ? 'seriesFilterBy' : 'filterBy']: query.filter || undefined,
|
||||||
|
[isSeries ? 'seriesSortBy' : 'orderBy']: query.sort || undefined,
|
||||||
|
[isSeries ? 'seriesSortDesc' : 'orderDesc']: query.desc == '0' ? false : query.desc == '1' ? true : undefined
|
||||||
}
|
}
|
||||||
if (query.filter) {
|
store.dispatch('user/updateUserSettings', settingsUpdate)
|
||||||
console.log('has filter', query.filter)
|
|
||||||
store.commit('libraries/setSeriesFilterBy', query.filter)
|
|
||||||
}
|
|
||||||
} else if (query.filter) {
|
|
||||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect podcast libraries
|
// Redirect podcast libraries
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export default {
|
|||||||
|
|
||||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
|
|
||||||
|
this.$store.dispatch('user/loadUserSettings')
|
||||||
},
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { formatDistance, format, addDays, isDate } from 'date-fns'
|
|||||||
|
|
||||||
Vue.directive('click-outside', vClickOutside.directive)
|
Vue.directive('click-outside', vClickOutside.directive)
|
||||||
|
|
||||||
Vue.prototype.$eventBus = new Vue()
|
|
||||||
|
|
||||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
if (!unixms) return ''
|
if (!unixms) return ''
|
||||||
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||||
@@ -30,23 +28,26 @@ Vue.prototype.$addDaysToDate = (jsdate, daysToAdd) => {
|
|||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
||||||
if (typeof input !== 'string') {
|
if (typeof filename !== 'string') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet
|
// Most file systems use number of bytes for max filename
|
||||||
const MAX_FILENAME_LEN = 240
|
// to support most filesystems we will use max of 255 bytes in utf-16
|
||||||
|
// Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html
|
||||||
|
// Issue: https://github.com/advplyr/audiobookshelf/issues/1261
|
||||||
|
const MAX_FILENAME_BYTES = 255
|
||||||
|
|
||||||
var replacement = ''
|
const replacement = ''
|
||||||
var illegalRe = /[\/\?<>\\:\*\|"]/g
|
const illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g
|
const controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||||
var reservedRe = /^\.+$/
|
const reservedRe = /^\.+$/
|
||||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||||
var windowsTrailingRe = /[\. ]+$/
|
const windowsTrailingRe = /[\. ]+$/
|
||||||
var lineBreaks = /[\n\r]/g
|
const lineBreaks = /[\n\r]/g
|
||||||
|
|
||||||
var sanitized = input
|
sanitized = filename
|
||||||
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
||||||
.replace(illegalRe, replacement)
|
.replace(illegalRe, replacement)
|
||||||
.replace(controlRe, replacement)
|
.replace(controlRe, replacement)
|
||||||
@@ -55,13 +56,25 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
|||||||
.replace(windowsReservedRe, replacement)
|
.replace(windowsReservedRe, replacement)
|
||||||
.replace(windowsTrailingRe, replacement)
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
|
||||||
|
// Check if basename is too many bytes
|
||||||
|
const ext = Path.extname(sanitized) // separate out file extension
|
||||||
|
const basename = Path.basename(sanitized, ext)
|
||||||
|
const extByteLength = Buffer.byteLength(ext, 'utf16le')
|
||||||
|
const basenameByteLength = Buffer.byteLength(basename, 'utf16le')
|
||||||
|
if (basenameByteLength + extByteLength > MAX_FILENAME_BYTES) {
|
||||||
|
const MaxBytesForBasename = MAX_FILENAME_BYTES - extByteLength
|
||||||
|
let totalBytes = 0
|
||||||
|
let trimmedBasename = ''
|
||||||
|
|
||||||
if (sanitized.length > MAX_FILENAME_LEN) {
|
// Add chars until max bytes is reached
|
||||||
var lenToRemove = sanitized.length - MAX_FILENAME_LEN
|
for (const char of basename) {
|
||||||
var ext = Path.extname(sanitized)
|
totalBytes += Buffer.byteLength(char, 'utf16le')
|
||||||
var basename = Path.basename(sanitized, ext)
|
if (totalBytes > MaxBytesForBasename) break
|
||||||
basename = basename.slice(0, basename.length - lenToRemove)
|
else trimmedBasename += char
|
||||||
sanitized = basename + ext
|
}
|
||||||
|
|
||||||
|
trimmedBasename = trimmedBasename.trim()
|
||||||
|
sanitized = trimmedBasename + ext
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
@@ -144,6 +157,7 @@ export {
|
|||||||
export default ({ app, store }, inject) => {
|
export default ({ app, store }, inject) => {
|
||||||
app.$decode = decode
|
app.$decode = decode
|
||||||
app.$encode = encode
|
app.$encode = encode
|
||||||
|
inject('eventBus', new Vue())
|
||||||
inject('isDev', process.env.NODE_ENV !== 'production')
|
inject('isDev', process.env.NODE_ENV !== 'production')
|
||||||
|
|
||||||
store.commit('setRouterBasePath', app.$config.routerBasePath)
|
store.commit('setRouterBasePath', app.$config.routerBasePath)
|
||||||
|
|||||||
@@ -140,26 +140,22 @@ export const mutations = {
|
|||||||
state.showBatchQuickMatchModal = val
|
state.showBatchQuickMatchModal = val
|
||||||
},
|
},
|
||||||
resetSelectedMediaItems(state) {
|
resetSelectedMediaItems(state) {
|
||||||
// Vue.set(state, 'selectedMediaItems', [])
|
|
||||||
state.selectedMediaItems = []
|
state.selectedMediaItems = []
|
||||||
},
|
},
|
||||||
toggleMediaItemSelected(state, item) {
|
toggleMediaItemSelected(state, item) {
|
||||||
if (state.selectedMediaItems.some(i => i.id === item.id)) {
|
if (state.selectedMediaItems.some(i => i.id === item.id)) {
|
||||||
state.selectedMediaItems = state.selectedMediaItems.filter(i => i.id !== item.id)
|
state.selectedMediaItems = state.selectedMediaItems.filter(i => i.id !== item.id)
|
||||||
} else {
|
} else {
|
||||||
// const newSel = state.selectedMediaItems.concat([{...item}])
|
|
||||||
// Vue.set(state, 'selectedMediaItems', newSel)
|
|
||||||
state.selectedMediaItems.push(item)
|
state.selectedMediaItems.push(item)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setMediaItemSelected(state, { item, selected }) {
|
setMediaItemSelected(state, { item, selected }) {
|
||||||
const index = state.selectedMediaItems.findIndex(i => i.id === item.id)
|
const isAlreadySelected = state.selectedMediaItems.some(i => i.id === item.id)
|
||||||
if (index && !selected) {
|
if (isAlreadySelected && !selected) {
|
||||||
state.selectedMediaItems = state.selectedMediaItems.filter(i => i.id !== item.id)
|
state.selectedMediaItems = state.selectedMediaItems.filter(i => i.id !== item.id)
|
||||||
} else if (selected && !index) {
|
|
||||||
state.selectedMediaItems.splice(index, 1, item)
|
} else if (selected && !isAlreadySelected) {
|
||||||
// var newSel = state.selectedMediaItems.concat([libraryItemId])
|
state.selectedMediaItems.push(item)
|
||||||
// Vue.set(state, 'selectedMediaItems', newSel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,6 @@ export const state = () => ({
|
|||||||
folderLastUpdate: 0,
|
folderLastUpdate: 0,
|
||||||
filterData: null,
|
filterData: null,
|
||||||
numUserPlaylists: 0,
|
numUserPlaylists: 0,
|
||||||
seriesSortBy: 'name',
|
|
||||||
seriesSortDesc: false,
|
|
||||||
seriesFilterBy: 'all',
|
|
||||||
collections: [],
|
collections: [],
|
||||||
userPlaylists: []
|
userPlaylists: []
|
||||||
})
|
})
|
||||||
@@ -86,8 +83,8 @@ export const actions = {
|
|||||||
.$get('/api/filesystem')
|
.$get('/api/filesystem')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log('Settings folders', res)
|
console.log('Settings folders', res)
|
||||||
commit('setFolders', res)
|
commit('setFolders', res.directories)
|
||||||
return res
|
return res.directories
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to load dirs', error)
|
console.error('Failed to load dirs', error)
|
||||||
@@ -151,7 +148,7 @@ export const actions = {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/libraries`)
|
.$get(`/api/libraries`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
commit('set', data)
|
commit('set', data.libraries)
|
||||||
commit('setLastLoad')
|
commit('setLastLoad')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -312,15 +309,6 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setSeriesSortBy(state, sortBy) {
|
|
||||||
state.seriesSortBy = sortBy
|
|
||||||
},
|
|
||||||
setSeriesSortDesc(state, sortDesc) {
|
|
||||||
state.seriesSortDesc = sortDesc
|
|
||||||
},
|
|
||||||
setSeriesFilterBy(state, filterBy) {
|
|
||||||
state.seriesFilterBy = filterBy
|
|
||||||
},
|
|
||||||
setCollections(state, collections) {
|
setCollections(state, collections) {
|
||||||
state.collections = collections
|
state.collections = collections
|
||||||
},
|
},
|
||||||
|
|||||||
+44
-43
@@ -7,9 +7,12 @@ export const state = () => ({
|
|||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
bookshelfCoverSize: 120,
|
bookshelfCoverSize: 120,
|
||||||
collapseSeries: false,
|
collapseSeries: false,
|
||||||
collapseBookSeries: false
|
collapseBookSeries: false,
|
||||||
},
|
useChapterTrack: false,
|
||||||
settingsListeners: []
|
seriesSortBy: 'name',
|
||||||
|
seriesSortDesc: false,
|
||||||
|
seriesFilterBy: 'all'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -66,7 +69,7 @@ export const getters = {
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
// When changing libraries make sure sort and filter is still valid
|
// When changing libraries make sure sort and filter is still valid
|
||||||
checkUpdateLibrarySortFilter({ state, dispatch, commit }, mediaType) {
|
checkUpdateLibrarySortFilter({ state, dispatch, commit }, mediaType) {
|
||||||
var settingsUpdate = {}
|
const settingsUpdate = {}
|
||||||
if (mediaType == 'podcast') {
|
if (mediaType == 'podcast') {
|
||||||
if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') {
|
if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.author'
|
settingsUpdate.orderBy = 'media.metadata.author'
|
||||||
@@ -77,8 +80,8 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.title'
|
settingsUpdate.orderBy = 'media.metadata.title'
|
||||||
}
|
}
|
||||||
var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
const invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
||||||
var filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||||
if (invalidFilters.includes(filterByFirstPart)) {
|
if (invalidFilters.includes(filterByFirstPart)) {
|
||||||
settingsUpdate.filterBy = 'all'
|
settingsUpdate.filterBy = 'all'
|
||||||
}
|
}
|
||||||
@@ -94,30 +97,46 @@ export const actions = {
|
|||||||
dispatch('updateUserSettings', settingsUpdate)
|
dispatch('updateUserSettings', settingsUpdate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateUserSettings({ commit }, payload) {
|
updateUserSettings({ state, commit }, payload) {
|
||||||
var updatePayload = {
|
if (!payload) return false
|
||||||
...payload
|
|
||||||
}
|
let hasChanges = false
|
||||||
// Immediately update
|
const existingSettings = { ...state.settings }
|
||||||
commit('setSettings', updatePayload)
|
for (const key in existingSettings) {
|
||||||
return this.$axios.$patch('/api/me/settings', updatePayload).then((result) => {
|
if (payload[key] !== undefined && existingSettings[key] !== payload[key]) {
|
||||||
if (result.success) {
|
hasChanges = true
|
||||||
commit('setSettings', result.settings)
|
existingSettings[key] = payload[key]
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}
|
||||||
console.error('Failed to update settings', error)
|
if (hasChanges) {
|
||||||
return false
|
commit('setSettings', existingSettings)
|
||||||
})
|
this.$eventBus.$emit('user-settings', state.settings)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadUserSettings({ state, commit }) {
|
||||||
|
// Load settings from local storage
|
||||||
|
try {
|
||||||
|
let userSettingsFromLocal = localStorage.getItem('userSettings')
|
||||||
|
if (userSettingsFromLocal) {
|
||||||
|
userSettingsFromLocal = JSON.parse(userSettingsFromLocal)
|
||||||
|
const userSettings = { ...state.settings }
|
||||||
|
for (const key in userSettings) {
|
||||||
|
if (userSettingsFromLocal[key] !== undefined) {
|
||||||
|
userSettings[key] = userSettingsFromLocal[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commit('setSettings', userSettings)
|
||||||
|
this.$eventBus.$emit('user-settings', state.settings)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load userSettings from local storage', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
state.user = user
|
state.user = user
|
||||||
state.settings = user.settings
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.token) localStorage.setItem('token', user.token)
|
if (user.token) localStorage.setItem('token', user.token)
|
||||||
} else {
|
} else {
|
||||||
@@ -143,25 +162,7 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
setSettings(state, settings) {
|
setSettings(state, settings) {
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
var hasChanges = false
|
localStorage.setItem('userSettings', JSON.stringify(settings))
|
||||||
for (const key in settings) {
|
state.settings = settings
|
||||||
if (state.settings[key] !== settings[key]) {
|
|
||||||
hasChanges = true
|
|
||||||
state.settings[key] = settings[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasChanges) {
|
|
||||||
state.settingsListeners.forEach((listener) => {
|
|
||||||
listener.meth(state.settings)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addSettingsListener(state, listener) {
|
|
||||||
var index = state.settingsListeners.findIndex(l => l.id === listener.id)
|
|
||||||
if (index >= 0) state.settingsListeners.splice(index, 1, listener)
|
|
||||||
else state.settingsListeners.push(listener)
|
|
||||||
},
|
|
||||||
removeSettingsListener(state, listenerId) {
|
|
||||||
state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+54
-37
@@ -22,7 +22,7 @@
|
|||||||
"ButtonDelete": "Löschen",
|
"ButtonDelete": "Löschen",
|
||||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||||
"ButtonForceReScan": "Erzwinge einen Neu-Scan",
|
"ButtonForceReScan": "Erzwinge kompletten Neu-Scan",
|
||||||
"ButtonFullPath": "Vollständiger Pfad",
|
"ButtonFullPath": "Vollständiger Pfad",
|
||||||
"ButtonHide": "Ausblenden",
|
"ButtonHide": "Ausblenden",
|
||||||
"ButtonHome": "Startseite",
|
"ButtonHome": "Startseite",
|
||||||
@@ -30,11 +30,11 @@
|
|||||||
"ButtonLatest": "Neuste",
|
"ButtonLatest": "Neuste",
|
||||||
"ButtonLibrary": "Bibliothek",
|
"ButtonLibrary": "Bibliothek",
|
||||||
"ButtonLogout": "Abmelden",
|
"ButtonLogout": "Abmelden",
|
||||||
"ButtonLookup": "Nachschlagen",
|
"ButtonLookup": "Online-Suche",
|
||||||
"ButtonManageTracks": "Tracks verwalten",
|
"ButtonManageTracks": "Tracks verwalten",
|
||||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||||
"ButtonMatchAllAuthors": "Online-Abgleich aller Autoren",
|
"ButtonMatchAllAuthors": "Online-Suche für alle Autoren",
|
||||||
"ButtonMatchBooks": "Online-Abgleich aller Hörbücher",
|
"ButtonMatchBooks": "Online-Suche für alle Hörbücher",
|
||||||
"ButtonNevermind": "Vergiss es",
|
"ButtonNevermind": "Vergiss es",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
"ButtonSave": "Speichern",
|
"ButtonSave": "Speichern",
|
||||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||||
"ButtonScan": "Durchsuchen",
|
"ButtonScan": "Scan",
|
||||||
"ButtonScanLibrary": "Bibliothek durchsuchen",
|
"ButtonScanLibrary": "Bibliothek scannen",
|
||||||
"ButtonSearch": "Suchen",
|
"ButtonSearch": "Suchen",
|
||||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
||||||
"ButtonSeries": "Serien",
|
"ButtonSeries": "Serien",
|
||||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
|
||||||
"ButtonShiftTimes": "Arbeitszeiten",
|
"ButtonShiftTimes": "Zeitverschiebung",
|
||||||
"ButtonShow": "Anzeigen",
|
"ButtonShow": "Anzeigen",
|
||||||
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
||||||
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"HeaderAdvanced": "Erweitert",
|
"HeaderAdvanced": "Erweitert",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
|
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
|
||||||
"HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools",
|
"HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools",
|
||||||
"HeaderAudioTracks": "Audio-Tracks",
|
"HeaderAudioTracks": "Audiodateien",
|
||||||
"HeaderBackups": "Sicherungen",
|
"HeaderBackups": "Sicherungen",
|
||||||
"HeaderChangePassword": "Passwort ändern",
|
"HeaderChangePassword": "Passwort ändern",
|
||||||
"HeaderChapters": "Kapitel",
|
"HeaderChapters": "Kapitel",
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
"HeaderItemFiles": "Objekt-Dateien",
|
"HeaderItemFiles": "Objekt-Dateien",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
||||||
"HeaderLatestEpisodes": "Letzte Episoden",
|
"HeaderLatestEpisodes": "Letzte Episoden",
|
||||||
"HeaderLibraries": "Bibliotheken",
|
"HeaderLibraries": "Bibliotheken",
|
||||||
@@ -104,7 +105,10 @@
|
|||||||
"HeaderListeningStats": "Hörstatistiken",
|
"HeaderListeningStats": "Hörstatistiken",
|
||||||
"HeaderLogin": "Anmeldung",
|
"HeaderLogin": "Anmeldung",
|
||||||
"HeaderLogs": "Protokolle",
|
"HeaderLogs": "Protokolle",
|
||||||
"HeaderMatch": "Online-Abgleich",
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Stapelverarbeitung",
|
||||||
|
"HeaderMatch": "Online-Suche",
|
||||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||||
"HeaderNewAccount": "Neues Konto",
|
"HeaderNewAccount": "Neues Konto",
|
||||||
"HeaderNewLibrary": "Neue Bibliothek",
|
"HeaderNewLibrary": "Neue Bibliothek",
|
||||||
@@ -155,6 +159,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||||
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
|
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
|
||||||
@@ -201,7 +206,7 @@
|
|||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episodentitel",
|
"LabelEpisodeTitle": "Episodentitel",
|
||||||
"LabelEpisodeType": "Episodentyp",
|
"LabelEpisodeType": "Episodentyp",
|
||||||
"LabelExplicit": "Explizit <br />(Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
"LabelFileBirthtime": "Datei Geburtsdatum",
|
"LabelFileBirthtime": "Datei Geburtsdatum",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
"LabelNumberOfBooks": "Anzahl der Hörbücher",
|
||||||
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
"LabelNumberOfEpisodes": "Anzahl der Episoden",
|
||||||
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
"LabelOpenRSSFeed": "Öffne RSS Feed",
|
||||||
|
"LabelOverwrite": "Überschreiben",
|
||||||
"LabelPassword": "Passwort",
|
"LabelPassword": "Passwort",
|
||||||
"LabelPath": "Pfad",
|
"LabelPath": "Pfad",
|
||||||
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
|
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
|
||||||
@@ -292,7 +298,7 @@
|
|||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPrefixesToIgnore": "Zu ignorierende Vorwort/Artikel (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
"LabelProvider": "Anbieter",
|
"LabelProvider": "Anbieter",
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
@@ -315,7 +321,7 @@
|
|||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
"LabelSeriesProgress": "Serienfortschritt",
|
"LabelSeriesProgress": "Serienfortschritt",
|
||||||
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
|
||||||
"LabelSettingsChromecastSupport": "Chromecast-unterstützung",
|
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
|
||||||
"LabelSettingsDateFormat": "Datumsformat",
|
"LabelSettingsDateFormat": "Datumsformat",
|
||||||
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
|
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
|
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
|
||||||
@@ -336,8 +342,8 @@
|
|||||||
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Metadaten werden für die Metadaten eines Hörbuchs anstelle der Ordnernamen verwendet. Wenn keine ID3 Metadaten zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Metadaten werden für die Metadaten eines Hörbuchs anstelle der Ordnernamen verwendet. Wenn keine ID3 Metadaten zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben neu abgestimmte online Metadaten alle schon vorhandenen Metadaten eines Hörbuchs. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben neu abgestimmte online Metadaten alle schon vorhandenen Metadaten eines Hörbuchs. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
|
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten aus dem Hörbuchordner",
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "In OPF Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Metadaten eines Hörbuchs verwendet. OPF Datein sind seperate \"Textdateien \" mit der Endung \".abs\" in denen verschiedene Matadaten gespiechert sind. Wenn keine OPF Dateien zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
"LabelSettingsPreferOPFMetadataHelp": "In OPF-Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Bereitstellung der Metadaten eines Hörbuchs verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Hörbuch selber. In dieser sind verschiedene Matadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird standardmäßig der Ordnername verwendet.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
||||||
@@ -345,9 +351,9 @@
|
|||||||
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Titelbilder im Hörbuchordner speichern",
|
"LabelSettingsStoreCoversWithItem": "Titelbilder im Hörbuchordner speichern",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert wird, werden die Titelbilder in dem selben Ordner, in welchem auch das zugehörige Hörbuch gespeichert ist, gespeichert. Es wird nur eine Datei mit dem Namen \"cover\" 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 Hörbuch befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Hörbuchordner speichern",
|
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Hörbuchordner speichern",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert wird, werden die Metadaten in dem selben Ordner, in welchem auch das zugehörige Hörbuch gespeichert ist, gespeichert. Es wird eine Datei mit der Endung \".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 Hörbuch befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
@@ -390,9 +396,9 @@
|
|||||||
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
||||||
"LabelTrackFromFilename": "Titel von Dateiname",
|
"LabelTrackFromFilename": "Titel von Dateiname",
|
||||||
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
"LabelTrackFromMetadata": "Titel aus Metadaten",
|
||||||
"LabelTracks": "Tracks",
|
"LabelTracks": "Dateien",
|
||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Mehrfachdatei",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Einzeldatei",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
"LabelUnknown": "Unbekannt",
|
"LabelUnknown": "Unbekannt",
|
||||||
"LabelUpdateCover": "Titelbild aktualisieren",
|
"LabelUpdateCover": "Titelbild aktualisieren",
|
||||||
@@ -402,8 +408,8 @@
|
|||||||
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
|
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
|
||||||
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
|
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
|
||||||
"LabelUploaderDropFiles": "Dateien löschen",
|
"LabelUploaderDropFiles": "Dateien löschen",
|
||||||
"LabelUseChapterTrack": "Kapitelverfolgung verwenden",
|
"LabelUseChapterTrack": "Kapiteldatei verwenden",
|
||||||
"LabelUseFullTrack": "Gesamten Track verwenden",
|
"LabelUseFullTrack": "Gesamte Datei verwenden",
|
||||||
"LabelUser": "Benutzer",
|
"LabelUser": "Benutzer",
|
||||||
"LabelUsername": "Benutzername",
|
"LabelUsername": "Benutzername",
|
||||||
"LabelValue": "Wert",
|
"LabelValue": "Wert",
|
||||||
@@ -415,20 +421,20 @@
|
|||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "Eigene Playlists",
|
||||||
"LabelYourProgress": "Fortschritt",
|
"LabelYourProgress": "Fortschritt",
|
||||||
"MessageAddToPlayerQueue": "Add to player queue",
|
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||||
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
||||||
"MessageBackupsDescription": "In Sicherungen werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder gespeichert <code>/metadata/items</code> & <code>/metadata/authors</code>. Die Sicherungen enthalten keine Dateien welche in Ihren Bibliotheksordnern gespeichert sind.",
|
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Hörbuch-/Podcastordnern) gespeichert sind.",
|
||||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||||
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
||||||
"MessageChapterErrorFirstNotZero": "First chapter must start at 0",
|
"MessageChapterErrorFirstNotZero": "Das erste Kapitel muss bei 0 beginnen",
|
||||||
"MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
|
"MessageChapterErrorStartGteDuration": "Die ungültige Startzeit darf nicht größer als die gesamte Hörbuchdauer sein",
|
||||||
"MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
|
"MessageChapterErrorStartLtPrev": "Die ungültige Startzeit darf nicht größer oder gleich der Startzeit des vorherigen Kapitels sein",
|
||||||
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
||||||
"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?",
|
||||||
@@ -439,6 +445,12 @@
|
|||||||
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
||||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Wichtiger Hinweis!",
|
"MessageImportantNotice": "Wichtiger Hinweis!",
|
||||||
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
||||||
"MessageItemsSelected": "{0} ausgewählte Elemente",
|
"MessageItemsSelected": "{0} ausgewählte Elemente",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
||||||
"MessageLoading": "Laden...",
|
"MessageLoading": "Laden...",
|
||||||
@@ -459,7 +472,7 @@
|
|||||||
"MessageMarkAsFinished": "Als beendet markieren",
|
"MessageMarkAsFinished": "Als beendet markieren",
|
||||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
||||||
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
||||||
"MessageNoAudioTracks": "Keine Audiotracks",
|
"MessageNoAudioTracks": "Keine Audiodateien",
|
||||||
"MessageNoAuthors": "Keine Autoren",
|
"MessageNoAuthors": "Keine Autoren",
|
||||||
"MessageNoBackups": "Keine Sicherungen",
|
"MessageNoBackups": "Keine Sicherungen",
|
||||||
"MessageNoBookmarks": "Keine Lesezeichen",
|
"MessageNoBookmarks": "Keine Lesezeichen",
|
||||||
@@ -481,28 +494,30 @@
|
|||||||
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
|
||||||
"MessageNoResults": "Keine Ergebnisse",
|
"MessageNoResults": "Keine Ergebnisse",
|
||||||
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "Keine Serien",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"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",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
|
||||||
"MessageOr": "oder",
|
"MessageOr": "oder",
|
||||||
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
|
||||||
"MessagePlayChapter": "Kapitelanfang anhören",
|
"MessagePlayChapter": "Kapitelanfang anhören",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
||||||
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
||||||
"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": "Remove from player queue",
|
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen Remove from player queue",
|
||||||
"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": "Are you sure you want to reset chapters and undo the changes you made?",
|
"MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?",
|
||||||
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am",
|
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, welche am",
|
||||||
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
|
||||||
"MessageSearchResultsFor": "Suchergebnisse für",
|
"MessageSearchResultsFor": "Suchergebnisse für",
|
||||||
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
|
||||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
|
||||||
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
|
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
|
||||||
"MessageThinking": "Nachdenken...",
|
"MessageThinking": "Nachdenken...",
|
||||||
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
||||||
@@ -549,8 +564,8 @@
|
|||||||
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
||||||
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
||||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
|
||||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
|
||||||
"ToastCollectionItemsRemoveFailed": "Element(e) konnte(n) nicht aus der Sammlung entfernt werden",
|
"ToastCollectionItemsRemoveFailed": "Element(e) konnte(n) nicht aus der Sammlung entfernt werden",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Element(e) wurde(n) aus der Sammlung entfernt",
|
"ToastCollectionItemsRemoveSuccess": "Element(e) wurde(n) aus der Sammlung entfernt",
|
||||||
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
||||||
@@ -574,10 +589,12 @@
|
|||||||
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
"ToastLibraryScanStarted": "Bibliotheksscan gestartet",
|
||||||
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
|
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
"ToastPlaylistUpdateSuccess": "Playlist aktualisieren",
|
||||||
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
||||||
"ToastPodcastCreateSuccess": "Podcast erfolgreich erstellt",
|
"ToastPodcastCreateSuccess": "Podcast erfolgreich erstellt",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Element/Eintrag konnte nicht aus der Sammlung entfernt werden",
|
"ToastRemoveItemFromCollectionFailed": "Element/Eintrag konnte nicht aus der Sammlung entfernt werden",
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
"HeaderIgnoredFiles": "Ignored Files",
|
"HeaderIgnoredFiles": "Ignored Files",
|
||||||
"HeaderItemFiles": "Item Files",
|
"HeaderItemFiles": "Item Files",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Last Listening Session",
|
"HeaderLastListeningSession": "Last Listening Session",
|
||||||
"HeaderLatestEpisodes": "Latest episodes",
|
"HeaderLatestEpisodes": "Latest episodes",
|
||||||
"HeaderLibraries": "Libraries",
|
"HeaderLibraries": "Libraries",
|
||||||
@@ -104,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "Listening Stats",
|
"HeaderListeningStats": "Listening Stats",
|
||||||
"HeaderLogin": "Login",
|
"HeaderLogin": "Login",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Match",
|
"HeaderMatch": "Match",
|
||||||
"HeaderMetadataToEmbed": "Metadata to embed",
|
"HeaderMetadataToEmbed": "Metadata to embed",
|
||||||
"HeaderNewAccount": "New Account",
|
"HeaderNewAccount": "New Account",
|
||||||
@@ -155,6 +159,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "All Users",
|
"LabelAllUsers": "All Users",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Author",
|
"LabelAuthor": "Author",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Author (First Last)",
|
||||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "Number of Books",
|
"LabelNumberOfBooks": "Number of Books",
|
||||||
"LabelNumberOfEpisodes": "# of Episodes",
|
"LabelNumberOfEpisodes": "# of Episodes",
|
||||||
"LabelOpenRSSFeed": "Open RSS Feed",
|
"LabelOpenRSSFeed": "Open RSS Feed",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Path",
|
"LabelPath": "Path",
|
||||||
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
|
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
|
||||||
@@ -342,7 +348,7 @@
|
|||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
||||||
"LabelSettingsSquareBookCovers": "User square book covers",
|
"LabelSettingsSquareBookCovers": "Use square book covers",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
||||||
"LabelSettingsStoreCoversWithItem": "Store covers with item",
|
"LabelSettingsStoreCoversWithItem": "Store covers with item",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
|
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
|
||||||
@@ -439,6 +445,12 @@
|
|||||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Downloading episode",
|
"MessageDownloadingEpisode": "Downloading episode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Important Notice!",
|
"MessageImportantNotice": "Important Notice!",
|
||||||
"MessageInsertChapterBelow": "Insert chapter below",
|
"MessageInsertChapterBelow": "Insert chapter below",
|
||||||
"MessageItemsSelected": "{0} Items Selected",
|
"MessageItemsSelected": "{0} Items Selected",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Join us on",
|
"MessageJoinUsOn": "Join us on",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
||||||
"MessageLoading": "Loading...",
|
"MessageLoading": "Loading...",
|
||||||
@@ -482,6 +495,7 @@
|
|||||||
"MessageNoResults": "No Results",
|
"MessageNoResults": "No Results",
|
||||||
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "No Series",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNotYetImplemented": "Not yet implemented",
|
||||||
"MessageNoUpdateNecessary": "No update necessary",
|
"MessageNoUpdateNecessary": "No update necessary",
|
||||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||||
@@ -489,6 +503,7 @@
|
|||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
"MessagePauseChapter": "Pause chapter playback",
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
||||||
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
||||||
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
||||||
@@ -574,6 +589,8 @@
|
|||||||
"ToastLibraryScanStarted": "Library scan started",
|
"ToastLibraryScanStarted": "Library scan started",
|
||||||
"ToastLibraryUpdateFailed": "Failed to update library",
|
"ToastLibraryUpdateFailed": "Failed to update library",
|
||||||
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
|
|||||||
+18
-1
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
"HeaderIgnoredFiles": "Ignored Files",
|
"HeaderIgnoredFiles": "Ignored Files",
|
||||||
"HeaderItemFiles": "Item Files",
|
"HeaderItemFiles": "Item Files",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Last Listening Session",
|
"HeaderLastListeningSession": "Last Listening Session",
|
||||||
"HeaderLatestEpisodes": "Latest episodes",
|
"HeaderLatestEpisodes": "Latest episodes",
|
||||||
"HeaderLibraries": "Libraries",
|
"HeaderLibraries": "Libraries",
|
||||||
@@ -104,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "Listening Stats",
|
"HeaderListeningStats": "Listening Stats",
|
||||||
"HeaderLogin": "Login",
|
"HeaderLogin": "Login",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Match",
|
"HeaderMatch": "Match",
|
||||||
"HeaderMetadataToEmbed": "Metadata to embed",
|
"HeaderMetadataToEmbed": "Metadata to embed",
|
||||||
"HeaderNewAccount": "New Account",
|
"HeaderNewAccount": "New Account",
|
||||||
@@ -155,6 +159,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "All Users",
|
"LabelAllUsers": "All Users",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Author",
|
"LabelAuthor": "Author",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Author (First Last)",
|
||||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "Number of Books",
|
"LabelNumberOfBooks": "Number of Books",
|
||||||
"LabelNumberOfEpisodes": "# of Episodes",
|
"LabelNumberOfEpisodes": "# of Episodes",
|
||||||
"LabelOpenRSSFeed": "Open RSS Feed",
|
"LabelOpenRSSFeed": "Open RSS Feed",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Path",
|
"LabelPath": "Path",
|
||||||
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
|
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
|
||||||
@@ -342,7 +348,7 @@
|
|||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. for prefix \"the\" book title \"The Book Title\" would sort as \"Book Title, The\"",
|
||||||
"LabelSettingsSquareBookCovers": "User square book covers",
|
"LabelSettingsSquareBookCovers": "Use square book covers",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
"LabelSettingsSquareBookCoversHelp": "Prefer to use square covers over standard 1.6:1 book covers",
|
||||||
"LabelSettingsStoreCoversWithItem": "Store covers with item",
|
"LabelSettingsStoreCoversWithItem": "Store covers with item",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
|
"LabelSettingsStoreCoversWithItemHelp": "By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named \"cover\" will be kept",
|
||||||
@@ -439,6 +445,12 @@
|
|||||||
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Downloading episode",
|
"MessageDownloadingEpisode": "Downloading episode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Important Notice!",
|
"MessageImportantNotice": "Important Notice!",
|
||||||
"MessageInsertChapterBelow": "Insert chapter below",
|
"MessageInsertChapterBelow": "Insert chapter below",
|
||||||
"MessageItemsSelected": "{0} Items Selected",
|
"MessageItemsSelected": "{0} Items Selected",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Join us on",
|
"MessageJoinUsOn": "Join us on",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
"MessageListeningSessionsInTheLastYear": "{0} listening sessions in the last year",
|
||||||
"MessageLoading": "Loading...",
|
"MessageLoading": "Loading...",
|
||||||
@@ -482,6 +495,7 @@
|
|||||||
"MessageNoResults": "No Results",
|
"MessageNoResults": "No Results",
|
||||||
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "No Series",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNotYetImplemented": "Not yet implemented",
|
||||||
"MessageNoUpdateNecessary": "No update necessary",
|
"MessageNoUpdateNecessary": "No update necessary",
|
||||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||||
@@ -489,6 +503,7 @@
|
|||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
"MessagePauseChapter": "Pause chapter playback",
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
|
||||||
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
|
||||||
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
"MessageRemoveAllItemsWarning": "WARNING! This action will remove all library items from the database including any updates or matches you have made. This does not do anything to your actual files. Are you sure?",
|
||||||
@@ -574,6 +589,8 @@
|
|||||||
"ToastLibraryScanStarted": "Library scan started",
|
"ToastLibraryScanStarted": "Library scan started",
|
||||||
"ToastLibraryUpdateFailed": "Failed to update library",
|
"ToastLibraryUpdateFailed": "Failed to update library",
|
||||||
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
|
|||||||
+18
-1
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Trouver les Chapitres",
|
"HeaderFindChapters": "Trouver les Chapitres",
|
||||||
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
||||||
"HeaderItemFiles": "Fichiers des Articles",
|
"HeaderItemFiles": "Fichiers des Articles",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Dernière Session d'Ecoute",
|
"HeaderLastListeningSession": "Dernière Session d'Ecoute",
|
||||||
"HeaderLatestEpisodes": "Dernier Episodes",
|
"HeaderLatestEpisodes": "Dernier Episodes",
|
||||||
"HeaderLibraries": "Bibliothèque",
|
"HeaderLibraries": "Bibliothèque",
|
||||||
@@ -104,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "Statistiques d'Ecoute",
|
"HeaderListeningStats": "Statistiques d'Ecoute",
|
||||||
"HeaderLogin": "Connexion",
|
"HeaderLogin": "Connexion",
|
||||||
"HeaderLogs": "Fichiers Journaux",
|
"HeaderLogs": "Fichiers Journaux",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Edition en Masse",
|
||||||
"HeaderMatch": "Rechercher",
|
"HeaderMatch": "Rechercher",
|
||||||
"HeaderMetadataToEmbed": "Métadonnée à Intégrer",
|
"HeaderMetadataToEmbed": "Métadonnée à Intégrer",
|
||||||
"HeaderNewAccount": "Nouveau Compte",
|
"HeaderNewAccount": "Nouveau Compte",
|
||||||
@@ -155,6 +159,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "{0} Elements Ajoutés à la Liste de Lecture",
|
"LabelAddToPlaylistBatch": "{0} Elements Ajoutés à la Liste de Lecture",
|
||||||
"LabelAll": "Tout",
|
"LabelAll": "Tout",
|
||||||
"LabelAllUsers": "Tous les Utilisateurs",
|
"LabelAllUsers": "Tous les Utilisateurs",
|
||||||
|
"LabelAppend": "Ajouter",
|
||||||
"LabelAuthor": "Auteur",
|
"LabelAuthor": "Auteur",
|
||||||
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
||||||
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
|
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "Nombre de Livres",
|
"LabelNumberOfBooks": "Nombre de Livres",
|
||||||
"LabelNumberOfEpisodes": "Nombre d'Episodes",
|
"LabelNumberOfEpisodes": "Nombre d'Episodes",
|
||||||
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
|
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
|
||||||
|
"LabelOverwrite": "Ecraser",
|
||||||
"LabelPassword": "Mot de Passe",
|
"LabelPassword": "Mot de Passe",
|
||||||
"LabelPath": "Chemin",
|
"LabelPath": "Chemin",
|
||||||
"LabelPermissionsAccessAllLibraries": "Peut Acceder à Toutes les Bibliothèque",
|
"LabelPermissionsAccessAllLibraries": "Peut Acceder à Toutes les Bibliothèque",
|
||||||
@@ -439,6 +445,12 @@
|
|||||||
"MessageConfirmRemoveEpisode": "Etes vous certain de vouloir supprimer l'épisode \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Etes vous certain de vouloir supprimer l'épisode \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Etes vous certain de vouloir supprimer {0} épisodes?",
|
"MessageConfirmRemoveEpisodes": "Etes vous certain de vouloir supprimer {0} épisodes?",
|
||||||
"MessageConfirmRemovePlaylist": "Etes vous certain de vouloir supprimer la liste de lecture \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Etes vous certain de vouloir supprimer la liste de lecture \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Téléchargement de l'épisode",
|
"MessageDownloadingEpisode": "Téléchargement de l'épisode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l'ordre correct",
|
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l'ordre correct",
|
||||||
"MessageEmbedFinished": "Intégration Terminée!",
|
"MessageEmbedFinished": "Intégration Terminée!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Information Importante!",
|
"MessageImportantNotice": "Information Importante!",
|
||||||
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
|
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
|
||||||
"MessageItemsSelected": "{0} Articles Sélectionnés",
|
"MessageItemsSelected": "{0} Articles Sélectionnés",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Rejoignez-nous sur",
|
"MessageJoinUsOn": "Rejoignez-nous sur",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sessions d'écoute l'an dernier",
|
"MessageListeningSessionsInTheLastYear": "{0} sessions d'écoute l'an dernier",
|
||||||
"MessageLoading": "Chargement...",
|
"MessageLoading": "Chargement...",
|
||||||
@@ -482,6 +495,7 @@
|
|||||||
"MessageNoResults": "Pas de Résultats",
|
"MessageNoResults": "Pas de Résultats",
|
||||||
"MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"",
|
"MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"",
|
||||||
"MessageNoSeries": "Pas de Séries",
|
"MessageNoSeries": "Pas de Séries",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Non implémenté",
|
"MessageNotYetImplemented": "Non implémenté",
|
||||||
"MessageNoUpdateNecessary": "Pas de mise à jour nécessaire",
|
"MessageNoUpdateNecessary": "Pas de mise à jour nécessaire",
|
||||||
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire",
|
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire",
|
||||||
@@ -489,6 +503,7 @@
|
|||||||
"MessageOr": "ou",
|
"MessageOr": "ou",
|
||||||
"MessagePauseChapter": "Suspendre la lecture du chapitre",
|
"MessagePauseChapter": "Suspendre la lecture du chapitre",
|
||||||
"MessagePlayChapter": "Ecouter depuis le début du chapitre",
|
"MessagePlayChapter": "Ecouter depuis le début du chapitre",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n'a pas d'URL de flux RSS à utiliser pour la correspondance",
|
"MessagePodcastHasNoRSSFeedForMatching": "Le Podcast n'a pas d'URL de flux RSS à utiliser pour la correspondance",
|
||||||
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de '{0}'. N'écrase pas les données présentes à moins que le paramètre 'Préférer les Métadonnées par correspondance' soit activé.",
|
"MessageQuickMatchDescription": "Renseigne les détails manquants ainsi que la couverture avec la première correspondance de '{0}'. N'écrase pas les données présentes à moins que le paramètre 'Préférer les Métadonnées par correspondance' soit activé.",
|
||||||
"MessageRemoveAllItemsWarning": "ATTENTION! Cette action supprimera toute la base de données de la bibliothèque ainsi que les mises à jour ou correspondances qui auraient été effectuées. Cela n'a aucune incidence sur les fichiers de la bibliothèque. Voulez-vous continuer?",
|
"MessageRemoveAllItemsWarning": "ATTENTION! Cette action supprimera toute la base de données de la bibliothèque ainsi que les mises à jour ou correspondances qui auraient été effectuées. Cela n'a aucune incidence sur les fichiers de la bibliothèque. Voulez-vous continuer?",
|
||||||
@@ -574,6 +589,8 @@
|
|||||||
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
||||||
"ToastLibraryUpdateFailed": "Echec de la mise à jour de la bibliothèque",
|
"ToastLibraryUpdateFailed": "Echec de la mise à jour de la bibliothèque",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
||||||
|
"ToastPlaylistCreateFailed": "Echec de la création de la liste de lecture",
|
||||||
|
"ToastPlaylistCreateSuccess": "Liste de lecture créée",
|
||||||
"ToastPlaylistRemoveFailed": "Echec de la suppression de la liste de lecture",
|
"ToastPlaylistRemoveFailed": "Echec de la suppression de la liste de lecture",
|
||||||
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
|
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
|
||||||
"ToastPlaylistUpdateFailed": "Echec de la mise à jour de la liste de lecture",
|
"ToastPlaylistUpdateFailed": "Echec de la mise à jour de la liste de lecture",
|
||||||
@@ -598,4 +615,4 @@
|
|||||||
"WeekdayThursday": "Jeudi",
|
"WeekdayThursday": "Jeudi",
|
||||||
"WeekdayTuesday": "Mardi",
|
"WeekdayTuesday": "Mardi",
|
||||||
"WeekdayWednesday": "Mercredi"
|
"WeekdayWednesday": "Mercredi"
|
||||||
}
|
}
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Pronađi poglavlja",
|
"HeaderFindChapters": "Pronađi poglavlja",
|
||||||
"HeaderIgnoredFiles": "Zanemarene datoteke",
|
"HeaderIgnoredFiles": "Zanemarene datoteke",
|
||||||
"HeaderItemFiles": "Item Files",
|
"HeaderItemFiles": "Item Files",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Posljednja Listening Session",
|
"HeaderLastListeningSession": "Posljednja Listening Session",
|
||||||
"HeaderLatestEpisodes": "Najnovije epizode",
|
"HeaderLatestEpisodes": "Najnovije epizode",
|
||||||
"HeaderLibraries": "Biblioteke",
|
"HeaderLibraries": "Biblioteke",
|
||||||
@@ -104,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "Listening Stats",
|
"HeaderListeningStats": "Listening Stats",
|
||||||
"HeaderLogin": "Prijavljivanje",
|
"HeaderLogin": "Prijavljivanje",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Match",
|
"HeaderMatch": "Match",
|
||||||
"HeaderMetadataToEmbed": "Metapodatci za ugradnju",
|
"HeaderMetadataToEmbed": "Metapodatci za ugradnju",
|
||||||
"HeaderNewAccount": "Novi korisnički račun",
|
"HeaderNewAccount": "Novi korisnički račun",
|
||||||
@@ -155,6 +159,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "Svi korisnici",
|
"LabelAllUsers": "Svi korisnici",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Author (First Last)",
|
||||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "Number of Books",
|
"LabelNumberOfBooks": "Number of Books",
|
||||||
"LabelNumberOfEpisodes": "# of Episodes",
|
"LabelNumberOfEpisodes": "# of Episodes",
|
||||||
"LabelOpenRSSFeed": "Otvori RSS Feed",
|
"LabelOpenRSSFeed": "Otvori RSS Feed",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Lozinka",
|
"LabelPassword": "Lozinka",
|
||||||
"LabelPath": "Putanja",
|
"LabelPath": "Putanja",
|
||||||
"LabelPermissionsAccessAllLibraries": "Ima pristup svim bibliotekama",
|
"LabelPermissionsAccessAllLibraries": "Ima pristup svim bibliotekama",
|
||||||
@@ -439,6 +445,12 @@
|
|||||||
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Preuzimam epizodu",
|
"MessageDownloadingEpisode": "Preuzimam epizodu",
|
||||||
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
||||||
"MessageEmbedFinished": "Embed završen!",
|
"MessageEmbedFinished": "Embed završen!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Važna obavijest!",
|
"MessageImportantNotice": "Važna obavijest!",
|
||||||
"MessageInsertChapterBelow": "Unesi poglavlje ispod",
|
"MessageInsertChapterBelow": "Unesi poglavlje ispod",
|
||||||
"MessageItemsSelected": "{0} odabranih stavki",
|
"MessageItemsSelected": "{0} odabranih stavki",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Pridruži nam se na",
|
"MessageJoinUsOn": "Pridruži nam se na",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} slušanja u prošloj godini",
|
"MessageListeningSessionsInTheLastYear": "{0} slušanja u prošloj godini",
|
||||||
"MessageLoading": "Učitavam...",
|
"MessageLoading": "Učitavam...",
|
||||||
@@ -482,6 +495,7 @@
|
|||||||
"MessageNoResults": "Nema rezultata",
|
"MessageNoResults": "Nema rezultata",
|
||||||
"MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
|
"MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "No Series",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNotYetImplemented": "Not yet implemented",
|
||||||
"MessageNoUpdateNecessary": "Aktualiziranje nije potrebno",
|
"MessageNoUpdateNecessary": "Aktualiziranje nije potrebno",
|
||||||
"MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno",
|
"MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno",
|
||||||
@@ -489,6 +503,7 @@
|
|||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
"MessagePauseChapter": "Pause chapter playback",
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nema RSS feed url za matchanje",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nema RSS feed url za matchanje",
|
||||||
"MessageQuickMatchDescription": "Popuni prazne detalje stavki i cover sa prvim match rezultato iz '{0}'. Ne briše detalje osim ako 'Prefer matched metadata' server postavka nije uključena.",
|
"MessageQuickMatchDescription": "Popuni prazne detalje stavki i cover sa prvim match rezultato iz '{0}'. Ne briše detalje osim ako 'Prefer matched metadata' server postavka nije uključena.",
|
||||||
"MessageRemoveAllItemsWarning": "UPOZORENJE! Ova radnja briše sve stavke iz biblioteke uključujući bilokakve aktualizacije ili matcheve. Ovo ne mjenja vaše lokalne datoteke. Jeste li sigurni?",
|
"MessageRemoveAllItemsWarning": "UPOZORENJE! Ova radnja briše sve stavke iz biblioteke uključujući bilokakve aktualizacije ili matcheve. Ovo ne mjenja vaše lokalne datoteke. Jeste li sigurni?",
|
||||||
@@ -574,6 +589,8 @@
|
|||||||
"ToastLibraryScanStarted": "Sken biblioteke pokrenut",
|
"ToastLibraryScanStarted": "Sken biblioteke pokrenut",
|
||||||
"ToastLibraryUpdateFailed": "Aktualiziranje biblioteke neuspješno",
|
"ToastLibraryUpdateFailed": "Aktualiziranje biblioteke neuspješno",
|
||||||
"ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" aktualizirana",
|
"ToastLibraryUpdateSuccess": "Biblioteka \"{0}\" aktualizirana",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
|
|||||||
+44
-27
@@ -54,7 +54,7 @@
|
|||||||
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
||||||
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
||||||
"ButtonReScan": "Riscansiona",
|
"ButtonReScan": "Ri-scansiona",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
"ButtonRestore": "Ripristina",
|
"ButtonRestore": "Ripristina",
|
||||||
"ButtonSave": "Salva",
|
"ButtonSave": "Salva",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"ButtonSearch": "Cerca",
|
"ButtonSearch": "Cerca",
|
||||||
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
"ButtonSelectFolderPath": "Seleziona percorso cartella",
|
||||||
"ButtonSeries": "Serie",
|
"ButtonSeries": "Serie",
|
||||||
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
|
"ButtonSetChaptersFromTracks": "Impostare i capitoli dalle tracce",
|
||||||
"ButtonShiftTimes": "Ricerca veloce",
|
"ButtonShiftTimes": "Ricerca veloce",
|
||||||
"ButtonShow": "Mostra",
|
"ButtonShow": "Mostra",
|
||||||
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
|
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Trova Capitoli",
|
"HeaderFindChapters": "Trova Capitoli",
|
||||||
"HeaderIgnoredFiles": "File Ignorati",
|
"HeaderIgnoredFiles": "File Ignorati",
|
||||||
"HeaderItemFiles": "Files",
|
"HeaderItemFiles": "Files",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Ultima sessione di Ascolto",
|
"HeaderLastListeningSession": "Ultima sessione di Ascolto",
|
||||||
"HeaderLatestEpisodes": "Ultimi Episodi",
|
"HeaderLatestEpisodes": "Ultimi Episodi",
|
||||||
"HeaderLibraries": "Librerie",
|
"HeaderLibraries": "Librerie",
|
||||||
@@ -104,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "Statistiche di Ascolto",
|
"HeaderListeningStats": "Statistiche di Ascolto",
|
||||||
"HeaderLogin": "Login",
|
"HeaderLogin": "Login",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Trova Corrispondenza",
|
"HeaderMatch": "Trova Corrispondenza",
|
||||||
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
||||||
"HeaderNewAccount": "Nuovo Account",
|
"HeaderNewAccount": "Nuovo Account",
|
||||||
@@ -114,7 +118,7 @@
|
|||||||
"HeaderPermissions": "Permessi",
|
"HeaderPermissions": "Permessi",
|
||||||
"HeaderPlayerQueue": "Coda Riproduzione",
|
"HeaderPlayerQueue": "Coda Riproduzione",
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Playlist",
|
||||||
"HeaderPlaylistItems": "Playlist Items",
|
"HeaderPlaylistItems": "Elementi della playlist",
|
||||||
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
||||||
"HeaderPreviewCover": "Anteprima Cover",
|
"HeaderPreviewCover": "Anteprima Cover",
|
||||||
"HeaderRemoveEpisode": "Rimuovi Episodi",
|
"HeaderRemoveEpisode": "Rimuovi Episodi",
|
||||||
@@ -151,10 +155,11 @@
|
|||||||
"LabelAddedAt": "Aggiunto il",
|
"LabelAddedAt": "Aggiunto il",
|
||||||
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
"LabelAddToCollection": "Aggiungi alla Raccolta",
|
||||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
||||||
"LabelAddToPlaylist": "Add to Playlist",
|
"LabelAddToPlaylist": "aggiungi alla Playlist",
|
||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "Tutti gli Utenti",
|
"LabelAllUsers": "Tutti gli Utenti",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Autore",
|
"LabelAuthor": "Autore",
|
||||||
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
||||||
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "Numero di libri",
|
"LabelNumberOfBooks": "Numero di libri",
|
||||||
"LabelNumberOfEpisodes": "# degli episodi",
|
"LabelNumberOfEpisodes": "# degli episodi",
|
||||||
"LabelOpenRSSFeed": "Apri RSS Feed",
|
"LabelOpenRSSFeed": "Apri RSS Feed",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Percorso",
|
"LabelPath": "Percorso",
|
||||||
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
||||||
@@ -390,9 +396,9 @@
|
|||||||
"LabelTotalTimeListened": "Tempo totale di Ascolto",
|
"LabelTotalTimeListened": "Tempo totale di Ascolto",
|
||||||
"LabelTrackFromFilename": "Traccia da nome file",
|
"LabelTrackFromFilename": "Traccia da nome file",
|
||||||
"LabelTrackFromMetadata": "Traccia da Metadata",
|
"LabelTrackFromMetadata": "Traccia da Metadata",
|
||||||
"LabelTracks": "Tracks",
|
"LabelTracks": "Traccia",
|
||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Multi-traccia",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Traccia-singola",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnknown": "Sconosciuto",
|
"LabelUnknown": "Sconosciuto",
|
||||||
"LabelUpdateCover": "Aggiornamento Cover",
|
"LabelUpdateCover": "Aggiornamento Cover",
|
||||||
@@ -415,20 +421,20 @@
|
|||||||
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
"LabelWeekdaysToRun": "Giorni feriali da eseguire",
|
||||||
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
"LabelYourAudiobookDuration": "La durata dell'audiolibro",
|
||||||
"LabelYourBookmarks": "I tuoi Preferiti",
|
"LabelYourBookmarks": "I tuoi Preferiti",
|
||||||
"LabelYourPlaylists": "Your Playlists",
|
"LabelYourPlaylists": "le tue Playlist",
|
||||||
"LabelYourProgress": "Completato al",
|
"LabelYourProgress": "Completato al",
|
||||||
"MessageAddToPlayerQueue": "Add to player queue",
|
"MessageAddToPlayerQueue": "Aggiungi alla coda di riproduzione",
|
||||||
"MessageAppriseDescription": "Per utilizzare questa funzione è necessario disporre di un'istanza di <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> in esecuzione o un'API che gestirà quelle stesse richieste. <br />L'API Url dovrebbe essere il percorso URL completo per inviare la notifica, ad esempio se la tua istanza API è servita cosi .<code>http://192.168.1.1:8337</code> Allora dovrai mettere <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Per utilizzare questa funzione è necessario disporre di un'istanza di <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> in esecuzione o un'API che gestirà quelle stesse richieste. <br />L'API Url dovrebbe essere il percorso URL completo per inviare la notifica, ad esempio se la tua istanza API è servita cosi .<code>http://192.168.1.1:8337</code> Allora dovrai mettere <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
||||||
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
||||||
"MessageBookshelfNoResultsForFilter": "Nessul risultato per il filtro \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Nessul risultato per il filtro \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||||
"MessageBookshelfNoSeries": "You have no series",
|
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
||||||
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
||||||
"MessageChapterErrorFirstNotZero": "First chapter must start at 0",
|
"MessageChapterErrorFirstNotZero": "Il primo capitolo deve iniziare da 0",
|
||||||
"MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
|
"MessageChapterErrorStartGteDuration": "L'ora di inizio non valida deve essere inferiore alla durata dell'audiolibro",
|
||||||
"MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
|
"MessageChapterErrorStartLtPrev": "L'ora di inizio non valida deve essere maggiore o uguale all'ora di inizio del capitolo precedente",
|
||||||
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
"MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro",
|
||||||
"MessageCheckingCron": "Controllo cron...",
|
"MessageCheckingCron": "Controllo cron...",
|
||||||
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
"MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?",
|
||||||
@@ -438,7 +444,13 @@
|
|||||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
"MessageEmbedFinished": "Incorporamento finito!",
|
"MessageEmbedFinished": "Incorporamento finito!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Avviso Importante!",
|
"MessageImportantNotice": "Avviso Importante!",
|
||||||
"MessageInsertChapterBelow": "Inserisci capitolo sotto",
|
"MessageInsertChapterBelow": "Inserisci capitolo sotto",
|
||||||
"MessageItemsSelected": "{0} oggetti Selezionati",
|
"MessageItemsSelected": "{0} oggetti Selezionati",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Unisciti a noi su",
|
"MessageJoinUsOn": "Unisciti a noi su",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sessioni di ascolto nell'ultimo anno",
|
"MessageListeningSessionsInTheLastYear": "{0} sessioni di ascolto nell'ultimo anno",
|
||||||
"MessageLoading": "Caricamento...",
|
"MessageLoading": "Caricamento...",
|
||||||
@@ -481,28 +494,30 @@
|
|||||||
"MessageNoPodcastsFound": "Nessun podcasts trovato",
|
"MessageNoPodcastsFound": "Nessun podcasts trovato",
|
||||||
"MessageNoResults": "Nessun Risultato",
|
"MessageNoResults": "Nessun Risultato",
|
||||||
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "Nessuna Serie",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Non Ancora Implementato",
|
"MessageNotYetImplemented": "Non Ancora Implementato",
|
||||||
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "non hai nessuna Playlist",
|
||||||
"MessageOr": "o",
|
"MessageOr": "o",
|
||||||
"MessagePauseChapter": "Metti in Pausa Capitolo",
|
"MessagePauseChapter": "Metti in Pausa Capitolo",
|
||||||
"MessagePlayChapter": "Ascolta dall'inizio del capitolo",
|
"MessagePlayChapter": "Ascolta dall'inizio del capitolo",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
|
||||||
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
|
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
|
||||||
"MessageRemoveAllItemsWarning": "AVVERTIMENTO! Questa azione rimuoverà tutti gli elementi della libreria dal database, inclusi eventuali aggiornamenti o corrispondenze apportate. Questo non fa nulla ai tuoi file effettivi. Sei sicuro?",
|
"MessageRemoveAllItemsWarning": "AVVERTIMENTO! Questa azione rimuoverà tutti gli elementi della libreria dal database, inclusi eventuali aggiornamenti o corrispondenze apportate. Questo non fa nulla ai tuoi file effettivi. Sei sicuro?",
|
||||||
"MessageRemoveChapter": "Rimuovi Capitolo",
|
"MessageRemoveChapter": "Rimuovi Capitolo",
|
||||||
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
|
"MessageRemoveEpisodes": "rimuovi {0} episodio(i)",
|
||||||
"MessageRemoveFromPlayerQueue": "Remove from player queue",
|
"MessageRemoveFromPlayerQueue": "Rimuovi dalla coda di riproduzione",
|
||||||
"MessageRemoveUserWarning": "Sei sicuro di voler eliminare definitivamente l'utente \"{0}\"?",
|
"MessageRemoveUserWarning": "Sei sicuro di voler eliminare definitivamente l'utente \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Segnala bug, richiedi funzionalità e contribuisci",
|
"MessageReportBugsAndContribute": "Segnala bug, richiedi funzionalità e contribuisci",
|
||||||
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
"MessageResetChaptersConfirm": "Sei sicuro di voler reimpostare i capitoli e annullare le modifiche ?",
|
||||||
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
"MessageRestoreBackupConfirm": "Sei sicuro di voler ripristinare il backup creato su",
|
||||||
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
"MessageRestoreBackupWarning": "Il ripristino di un backup sovrascriverà l'intero database situato in /config e sovrascrive le immagini in /metadata/items & /metadata/authors.<br /><br />I backup non modificano alcun file nelle cartelle della libreria. Se hai abilitato le impostazioni del server per archiviare copertine e metadati nelle cartelle della libreria, questi non vengono sottoposti a backup o sovrascritti.<br /><br />Tutti i client che utilizzano il tuo server verranno aggiornati automaticamente.",
|
||||||
"MessageSearchResultsFor": "cerca risultati per",
|
"MessageSearchResultsFor": "cerca risultati per",
|
||||||
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
"MessageServerCouldNotBeReached": "Impossibile raggiungere il server",
|
||||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
"MessageSetChaptersFromTracksDescription": "Impostare i capitoli utilizzando ciascun file audio come capitolo e il titolo del capitolo come nome del file audio",
|
||||||
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
"MessageStartPlaybackAtTime": "Avvia la riproduzione per \"{0}\" a {1}?",
|
||||||
"MessageThinking": "Elaborazione...",
|
"MessageThinking": "Elaborazione...",
|
||||||
"MessageUploaderItemFailed": "Caricamento Fallito",
|
"MessageUploaderItemFailed": "Caricamento Fallito",
|
||||||
@@ -524,7 +539,7 @@
|
|||||||
"NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.",
|
"NoteUploaderUnsupportedFiles": "I file non supportati vengono ignorati. Quando si sceglie o si elimina una cartella, gli altri file che non si trovano in una cartella di elementi vengono ignorati.",
|
||||||
"PlaceholderNewCollection": "Nome Nuova Raccolta",
|
"PlaceholderNewCollection": "Nome Nuova Raccolta",
|
||||||
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
||||||
"PlaceholderNewPlaylist": "New playlist name",
|
"PlaceholderNewPlaylist": "Nome nuova playlist",
|
||||||
"PlaceholderSearch": "Cerca..",
|
"PlaceholderSearch": "Cerca..",
|
||||||
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
||||||
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
||||||
@@ -549,8 +564,8 @@
|
|||||||
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
|
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
|
||||||
"ToastBookmarkUpdateFailed": "Aggiornamento Segnalibro fallito",
|
"ToastBookmarkUpdateFailed": "Aggiornamento Segnalibro fallito",
|
||||||
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
|
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
|
||||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
"ToastChaptersHaveErrors": "I capitoli contengono errori",
|
||||||
"ToastChaptersMustHaveTitles": "Chapters must have titles",
|
"ToastChaptersMustHaveTitles": "I capitoli devono avere titoli",
|
||||||
"ToastCollectionItemsRemoveFailed": "Rimozione oggetti dalla Raccolta fallita",
|
"ToastCollectionItemsRemoveFailed": "Rimozione oggetti dalla Raccolta fallita",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla Raccolta",
|
"ToastCollectionItemsRemoveSuccess": "Oggetto(i) rimossi dalla Raccolta",
|
||||||
"ToastCollectionRemoveFailed": "Rimozione Raccolta fallita",
|
"ToastCollectionRemoveFailed": "Rimozione Raccolta fallita",
|
||||||
@@ -574,10 +589,12 @@
|
|||||||
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
||||||
"ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
|
"ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
|
||||||
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistRemoveFailed": "Rimozione Playlist Fallita",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
"ToastPlaylistRemoveSuccess": "Playlist rimossa",
|
||||||
|
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
|
||||||
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamwnte",
|
"ToastPodcastCreateSuccess": "Podcast creato Correttamwnte",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "Wyszukaj rozdziały",
|
"HeaderFindChapters": "Wyszukaj rozdziały",
|
||||||
"HeaderIgnoredFiles": "Zignoruj pliki",
|
"HeaderIgnoredFiles": "Zignoruj pliki",
|
||||||
"HeaderItemFiles": "Pliki",
|
"HeaderItemFiles": "Pliki",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Ostatnio odtwarzana sesja",
|
"HeaderLastListeningSession": "Ostatnio odtwarzana sesja",
|
||||||
"HeaderLatestEpisodes": "Najnowsze odcinki",
|
"HeaderLatestEpisodes": "Najnowsze odcinki",
|
||||||
"HeaderLibraries": "Biblioteki",
|
"HeaderLibraries": "Biblioteki",
|
||||||
@@ -104,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "Statystyki odtwarzania",
|
"HeaderListeningStats": "Statystyki odtwarzania",
|
||||||
"HeaderLogin": "Zaloguj się",
|
"HeaderLogin": "Zaloguj się",
|
||||||
"HeaderLogs": "Logi",
|
"HeaderLogs": "Logi",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "Dopasuj",
|
"HeaderMatch": "Dopasuj",
|
||||||
"HeaderMetadataToEmbed": "Osadź metadane",
|
"HeaderMetadataToEmbed": "Osadź metadane",
|
||||||
"HeaderNewAccount": "Nowe konto",
|
"HeaderNewAccount": "Nowe konto",
|
||||||
@@ -155,6 +159,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "Wszyscy użytkownicy",
|
"LabelAllUsers": "Wszyscy użytkownicy",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Rosnąco)",
|
"LabelAuthorFirstLast": "Autor (Rosnąco)",
|
||||||
"LabelAuthorLastFirst": "Author (Malejąco)",
|
"LabelAuthorLastFirst": "Author (Malejąco)",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "Liczba książek",
|
"LabelNumberOfBooks": "Liczba książek",
|
||||||
"LabelNumberOfEpisodes": "# odcinków",
|
"LabelNumberOfEpisodes": "# odcinków",
|
||||||
"LabelOpenRSSFeed": "Otwórz kanał RSS",
|
"LabelOpenRSSFeed": "Otwórz kanał RSS",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "Hasło",
|
"LabelPassword": "Hasło",
|
||||||
"LabelPath": "Ścieżka",
|
"LabelPath": "Ścieżka",
|
||||||
"LabelPermissionsAccessAllLibraries": "Ma dostęp do wszystkich bibliotek",
|
"LabelPermissionsAccessAllLibraries": "Ma dostęp do wszystkich bibliotek",
|
||||||
@@ -439,6 +445,12 @@
|
|||||||
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
||||||
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
||||||
"MessageEmbedFinished": "Osadzanie zakończone!",
|
"MessageEmbedFinished": "Osadzanie zakończone!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "Ważna informacja!",
|
"MessageImportantNotice": "Ważna informacja!",
|
||||||
"MessageInsertChapterBelow": "Wstaw rozdział poniżej",
|
"MessageInsertChapterBelow": "Wstaw rozdział poniżej",
|
||||||
"MessageItemsSelected": "{0} zaznaczone elementy",
|
"MessageItemsSelected": "{0} zaznaczone elementy",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Dołącz do nas na",
|
"MessageJoinUsOn": "Dołącz do nas na",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sesje odsłuchowe w ostatnim roku",
|
"MessageListeningSessionsInTheLastYear": "{0} sesje odsłuchowe w ostatnim roku",
|
||||||
"MessageLoading": "Ładowanie...",
|
"MessageLoading": "Ładowanie...",
|
||||||
@@ -482,6 +495,7 @@
|
|||||||
"MessageNoResults": "Brak wyników",
|
"MessageNoResults": "Brak wyników",
|
||||||
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
|
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "No Series",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
|
"MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
|
||||||
"MessageNoUpdateNecessary": "Brak konieczności aktualizacji",
|
"MessageNoUpdateNecessary": "Brak konieczności aktualizacji",
|
||||||
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
|
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
|
||||||
@@ -489,6 +503,7 @@
|
|||||||
"MessageOr": "lub",
|
"MessageOr": "lub",
|
||||||
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
|
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
|
||||||
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
|
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
|
||||||
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
|
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
|
||||||
"MessageRemoveAllItemsWarning": "UWAGA! Ta akcja usunie wszystkie elementy biblioteki z bazy danych, w tym wszystkie aktualizacje lub dopasowania, które zostały wykonane. Pliki pozostaną niezmienione. Czy jesteś pewien?",
|
"MessageRemoveAllItemsWarning": "UWAGA! Ta akcja usunie wszystkie elementy biblioteki z bazy danych, w tym wszystkie aktualizacje lub dopasowania, które zostały wykonane. Pliki pozostaną niezmienione. Czy jesteś pewien?",
|
||||||
@@ -574,6 +589,8 @@
|
|||||||
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
|
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
|
||||||
"ToastLibraryUpdateFailed": "Nie udało się zaktualizować biblioteki",
|
"ToastLibraryUpdateFailed": "Nie udało się zaktualizować biblioteki",
|
||||||
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
|
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
"HeaderFindChapters": "查找章节",
|
"HeaderFindChapters": "查找章节",
|
||||||
"HeaderIgnoredFiles": "忽略的文件",
|
"HeaderIgnoredFiles": "忽略的文件",
|
||||||
"HeaderItemFiles": "项目文件",
|
"HeaderItemFiles": "项目文件",
|
||||||
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "最后一次收听会话",
|
"HeaderLastListeningSession": "最后一次收听会话",
|
||||||
"HeaderLatestEpisodes": "最新剧集",
|
"HeaderLatestEpisodes": "最新剧集",
|
||||||
"HeaderLibraries": "媒体库",
|
"HeaderLibraries": "媒体库",
|
||||||
@@ -104,6 +105,9 @@
|
|||||||
"HeaderListeningStats": "收听统计数据",
|
"HeaderListeningStats": "收听统计数据",
|
||||||
"HeaderLogin": "登录",
|
"HeaderLogin": "登录",
|
||||||
"HeaderLogs": "日志",
|
"HeaderLogs": "日志",
|
||||||
|
"HeaderManageGenres": "Manage Genres",
|
||||||
|
"HeaderManageTags": "Manage Tags",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
"HeaderMatch": "匹配",
|
"HeaderMatch": "匹配",
|
||||||
"HeaderMetadataToEmbed": "嵌入元数据",
|
"HeaderMetadataToEmbed": "嵌入元数据",
|
||||||
"HeaderNewAccount": "新建帐户",
|
"HeaderNewAccount": "新建帐户",
|
||||||
@@ -155,6 +159,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
|
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
|
||||||
"LabelAll": "全部",
|
"LabelAll": "全部",
|
||||||
"LabelAllUsers": "所有用户",
|
"LabelAllUsers": "所有用户",
|
||||||
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "作者",
|
"LabelAuthor": "作者",
|
||||||
"LabelAuthorFirstLast": "作者 (姓 名)",
|
"LabelAuthorFirstLast": "作者 (姓 名)",
|
||||||
"LabelAuthorLastFirst": "作者 (名, 姓)",
|
"LabelAuthorLastFirst": "作者 (名, 姓)",
|
||||||
@@ -278,6 +283,7 @@
|
|||||||
"LabelNumberOfBooks": "图书数量",
|
"LabelNumberOfBooks": "图书数量",
|
||||||
"LabelNumberOfEpisodes": "# 集",
|
"LabelNumberOfEpisodes": "# 集",
|
||||||
"LabelOpenRSSFeed": "打开 RSS 源",
|
"LabelOpenRSSFeed": "打开 RSS 源",
|
||||||
|
"LabelOverwrite": "Overwrite",
|
||||||
"LabelPassword": "密码",
|
"LabelPassword": "密码",
|
||||||
"LabelPath": "路径",
|
"LabelPath": "路径",
|
||||||
"LabelPermissionsAccessAllLibraries": "可以访问所有媒体库",
|
"LabelPermissionsAccessAllLibraries": "可以访问所有媒体库",
|
||||||
@@ -439,6 +445,12 @@
|
|||||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||||
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "正在下载剧集",
|
"MessageDownloadingEpisode": "正在下载剧集",
|
||||||
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
||||||
"MessageEmbedFinished": "嵌入完成!",
|
"MessageEmbedFinished": "嵌入完成!",
|
||||||
@@ -449,6 +461,7 @@
|
|||||||
"MessageImportantNotice": "重要通知!",
|
"MessageImportantNotice": "重要通知!",
|
||||||
"MessageInsertChapterBelow": "在下面插入章节",
|
"MessageInsertChapterBelow": "在下面插入章节",
|
||||||
"MessageItemsSelected": "已选定 {0} 个项目",
|
"MessageItemsSelected": "已选定 {0} 个项目",
|
||||||
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "加入我们",
|
"MessageJoinUsOn": "加入我们",
|
||||||
"MessageListeningSessionsInTheLastYear": "去年收听 {0} 个会话",
|
"MessageListeningSessionsInTheLastYear": "去年收听 {0} 个会话",
|
||||||
"MessageLoading": "加载...",
|
"MessageLoading": "加载...",
|
||||||
@@ -482,6 +495,7 @@
|
|||||||
"MessageNoResults": "无结果",
|
"MessageNoResults": "无结果",
|
||||||
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
|
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
|
||||||
"MessageNoSeries": "无系列",
|
"MessageNoSeries": "无系列",
|
||||||
|
"MessageNoTags": "No Tags",
|
||||||
"MessageNotYetImplemented": "尚未实施",
|
"MessageNotYetImplemented": "尚未实施",
|
||||||
"MessageNoUpdateNecessary": "无需更新",
|
"MessageNoUpdateNecessary": "无需更新",
|
||||||
"MessageNoUpdatesWereNecessary": "无需更新",
|
"MessageNoUpdatesWereNecessary": "无需更新",
|
||||||
@@ -489,6 +503,7 @@
|
|||||||
"MessageOr": "或",
|
"MessageOr": "或",
|
||||||
"MessagePauseChapter": "暂停章节播放",
|
"MessagePauseChapter": "暂停章节播放",
|
||||||
"MessagePlayChapter": "开始章节播放",
|
"MessagePlayChapter": "开始章节播放",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
|
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
|
||||||
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
|
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
|
||||||
"MessageRemoveAllItemsWarning": "警告! 此操作将从数据库中删除所有的媒体库项, 包括您所做的任何更新或匹配. 这不会对实际文件产生任何影响. 你确定吗?",
|
"MessageRemoveAllItemsWarning": "警告! 此操作将从数据库中删除所有的媒体库项, 包括您所做的任何更新或匹配. 这不会对实际文件产生任何影响. 你确定吗?",
|
||||||
@@ -574,6 +589,8 @@
|
|||||||
"ToastLibraryScanStarted": "媒体库扫描已启动",
|
"ToastLibraryScanStarted": "媒体库扫描已启动",
|
||||||
"ToastLibraryUpdateFailed": "更新图书库失败",
|
"ToastLibraryUpdateFailed": "更新图书库失败",
|
||||||
"ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新",
|
"ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新",
|
||||||
|
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
||||||
|
"ToastPlaylistCreateSuccess": "Playlist created",
|
||||||
"ToastPlaylistRemoveFailed": "删除播放列表失败",
|
"ToastPlaylistRemoveFailed": "删除播放列表失败",
|
||||||
"ToastPlaylistRemoveSuccess": "播放列表已删除",
|
"ToastPlaylistRemoveSuccess": "播放列表已删除",
|
||||||
"ToastPlaylistUpdateFailed": "更新播放列表失败",
|
"ToastPlaylistUpdateFailed": "更新播放列表失败",
|
||||||
@@ -598,4 +615,4 @@
|
|||||||
"WeekdayThursday": "星期四",
|
"WeekdayThursday": "星期四",
|
||||||
"WeekdayTuesday": "星期二",
|
"WeekdayTuesday": "星期二",
|
||||||
"WeekdayWednesday": "星期三"
|
"WeekdayWednesday": "星期三"
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ if (isDev) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PORT = process.env.PORT || 80
|
const PORT = process.env.PORT || 80
|
||||||
const HOST = process.env.HOST || '0.0.0.0'
|
const HOST = process.env.HOST
|
||||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
||||||
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
||||||
const UID = process.env.AUDIOBOOKSHELF_UID || 99
|
const UID = process.env.AUDIOBOOKSHELF_UID || 99
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.7",
|
"version": "2.2.9",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.7",
|
"version": "2.2.9",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.7",
|
"version": "2.2.9",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+5
-1
@@ -2,6 +2,7 @@ const Path = require('path')
|
|||||||
const njodb = require('./libs/njodb')
|
const njodb = require('./libs/njodb')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
const { version } = require('../package.json')
|
const { version } = require('../package.json')
|
||||||
|
const filePerms = require('./utils/filePerms')
|
||||||
const LibraryItem = require('./objects/LibraryItem')
|
const LibraryItem = require('./objects/LibraryItem')
|
||||||
const User = require('./objects/user/User')
|
const User = require('./objects/user/User')
|
||||||
const Collection = require('./objects/Collection')
|
const Collection = require('./objects/Collection')
|
||||||
@@ -131,6 +132,9 @@ class Db {
|
|||||||
async init() {
|
async init() {
|
||||||
await this.load()
|
await this.load()
|
||||||
|
|
||||||
|
// Set file ownership for all files created by db
|
||||||
|
await filePerms.setDefault(global.ConfigPath, true)
|
||||||
|
|
||||||
if (!this.serverSettings) { // Create first load server settings
|
if (!this.serverSettings) { // Create first load server settings
|
||||||
this.serverSettings = new ServerSettings()
|
this.serverSettings = new ServerSettings()
|
||||||
await this.insertEntity('settings', this.serverSettings)
|
await this.insertEntity('settings', this.serverSettings)
|
||||||
@@ -229,7 +233,7 @@ class Db {
|
|||||||
return null
|
return null
|
||||||
}))
|
}))
|
||||||
|
|
||||||
var libraryItemIds = libraryItems.map(li => li.id)
|
const libraryItemIds = libraryItems.map(li => li.id)
|
||||||
return this.libraryItemsDb.update((record) => libraryItemIds.includes(record.id), (record) => {
|
return this.libraryItemsDb.update((record) => libraryItemIds.includes(record.id), (record) => {
|
||||||
return libraryItems.find(li => li.id === record.id)
|
return libraryItems.find(li => li.id === record.id)
|
||||||
}).then((results) => {
|
}).then((results) => {
|
||||||
|
|||||||
+3
-1
@@ -206,6 +206,7 @@ class Server {
|
|||||||
'/library/:library/podcast/latest',
|
'/library/:library/podcast/latest',
|
||||||
'/config/users/:id',
|
'/config/users/:id',
|
||||||
'/config/users/:id/sessions',
|
'/config/users/:id/sessions',
|
||||||
|
'/config/item-metadata-utils/:id',
|
||||||
'/collection/:id',
|
'/collection/:id',
|
||||||
'/playlist/:id'
|
'/playlist/:id'
|
||||||
]
|
]
|
||||||
@@ -240,7 +241,8 @@ class Server {
|
|||||||
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
||||||
|
|
||||||
this.server.listen(this.Port, this.Host, () => {
|
this.server.listen(this.Port, 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}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start listening for socket connections
|
// Start listening for socket connections
|
||||||
|
|||||||
@@ -148,7 +148,9 @@ class AuthorController {
|
|||||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||||
var authors = this.db.authors.filter(au => au.name.toLowerCase().includes(q))
|
var authors = this.db.authors.filter(au => au.name.toLowerCase().includes(q))
|
||||||
authors = authors.slice(0, limit)
|
authors = authors.slice(0, limit)
|
||||||
res.json(authors)
|
res.json({
|
||||||
|
results: authors
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async match(req, res) {
|
async match(req, res) {
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ class CollectionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findAll(req, res) {
|
findAll(req, res) {
|
||||||
var expandedCollections = this.db.collections.map(c => c.toJSONExpanded(this.db.libraryItems))
|
res.json({
|
||||||
res.json(expandedCollections)
|
collections: this.db.collections.map(c => c.toJSONExpanded(this.db.libraryItems))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
findOne(req, res) {
|
||||||
@@ -122,7 +123,7 @@ class CollectionController {
|
|||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
const collection = this.db.collections.find(c => c.id === req.params.id)
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return res.status(404).send('Collection not found')
|
return res.status(404).send('Collection not found')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ class FileSystemController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Logger.debug(`[Server] get file system paths, excluded: ${excludedDirs.join(', ')}`)
|
Logger.debug(`[Server] get file system paths, excluded: ${excludedDirs.join(', ')}`)
|
||||||
var dirs = await this.getDirectories(global.appRoot, '/', excludedDirs)
|
res.json({
|
||||||
res.json(dirs)
|
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new FileSystemController()
|
module.exports = new FileSystemController()
|
||||||
@@ -62,7 +62,9 @@ class LibraryController {
|
|||||||
return res.json(this.db.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON()))
|
return res.json(this.db.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON()))
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(this.db.libraries.map(lib => lib.toJSON()))
|
res.json({
|
||||||
|
libraries: this.db.libraries.map(lib => lib.toJSON())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
@@ -496,8 +498,9 @@ class LibraryController {
|
|||||||
Logger.debug(`[LibraryController] Library orders were up to date`)
|
Logger.debug(`[LibraryController] Library orders were up to date`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraries = this.db.libraries.map(lib => lib.toJSON())
|
res.json({
|
||||||
res.json(libraries)
|
libraries: this.db.libraries.map(lib => lib.toJSON())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: Global library search
|
// GET: Global library search
|
||||||
@@ -603,7 +606,9 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json(naturalSort(Object.values(authors)).asc(au => au.name))
|
res.json({
|
||||||
|
authors: naturalSort(Object.values(authors)).asc(au => au.name)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async matchAll(req, res) {
|
async matchAll(req, res) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
@@ -178,7 +179,15 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// GET api/items/:id/cover
|
// GET api/items/:id/cover
|
||||||
async getCover(req, res) {
|
async getCover(req, res) {
|
||||||
let { query: { width, height, format }, libraryItem } = req
|
const { query: { width, height, format, raw }, libraryItem } = req
|
||||||
|
|
||||||
|
if (raw) { // any value
|
||||||
|
if (!libraryItem.media.coverPath || !await fs.pathExists(libraryItem.media.coverPath)) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.sendFile(libraryItem.media.coverPath)
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||||
@@ -299,16 +308,18 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// POST: api/items/batch/get
|
// POST: api/items/batch/get
|
||||||
async batchGet(req, res) {
|
async batchGet(req, res) {
|
||||||
var libraryItemIds = req.body.libraryItemIds || []
|
const libraryItemIds = req.body.libraryItemIds || []
|
||||||
if (!libraryItemIds.length) {
|
if (!libraryItemIds.length) {
|
||||||
return res.status(403).send('Invalid payload')
|
return res.status(403).send('Invalid payload')
|
||||||
}
|
}
|
||||||
var libraryItems = []
|
const libraryItems = []
|
||||||
libraryItemIds.forEach((lid) => {
|
libraryItemIds.forEach((lid) => {
|
||||||
const li = this.db.libraryItems.find(_li => _li.id === lid)
|
const li = this.db.libraryItems.find(_li => _li.id === lid)
|
||||||
if (li) libraryItems.push(li.toJSONExpanded())
|
if (li) libraryItems.push(li.toJSONExpanded())
|
||||||
})
|
})
|
||||||
res.json(libraryItems)
|
res.json({
|
||||||
|
libraryItems
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/items/batch/quickmatch
|
// POST: api/items/batch/quickmatch
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ class MeController {
|
|||||||
this.auth.userChangePassword(req, res)
|
this.auth.userChangePassword(req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove after mobile release v0.9.61-beta
|
||||||
// PATCH: api/me/settings
|
// PATCH: api/me/settings
|
||||||
async updateSettings(req, res) {
|
async updateSettings(req, res) {
|
||||||
var settingsUpdate = req.body
|
var settingsUpdate = req.body
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||||
const { isObject } = require('../utils/index')
|
const { isObject } = require('../utils/index')
|
||||||
@@ -124,12 +126,13 @@ class MiscController {
|
|||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/tags
|
||||||
getAllTags(req, res) {
|
getAllTags(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
|
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
var tags = []
|
const tags = []
|
||||||
this.db.libraryItems.forEach((li) => {
|
this.db.libraryItems.forEach((li) => {
|
||||||
if (li.media.tags && li.media.tags.length) {
|
if (li.media.tags && li.media.tags.length) {
|
||||||
li.media.tags.forEach((tag) => {
|
li.media.tags.forEach((tag) => {
|
||||||
@@ -137,7 +140,162 @@ class MiscController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
res.json(tags)
|
res.json({
|
||||||
|
tags: tags
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/tags/rename
|
||||||
|
async renameTag(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[MiscController] Non-admin user attempted to renameTag`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = req.body.tag
|
||||||
|
const newTag = req.body.newTag
|
||||||
|
if (!tag || !newTag) {
|
||||||
|
Logger.error(`[MiscController] Invalid request body for renameTag`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
let tagMerged = false
|
||||||
|
let numItemsUpdated = 0
|
||||||
|
|
||||||
|
for (const li of this.db.libraryItems) {
|
||||||
|
if (!li.media.tags || !li.media.tags.length) continue
|
||||||
|
|
||||||
|
if (li.media.tags.includes(newTag)) tagMerged = true // new tag is an existing tag so this is a merge
|
||||||
|
|
||||||
|
if (li.media.tags.includes(tag)) {
|
||||||
|
li.media.tags = li.media.tags.filter(t => t !== tag) // Remove old tag
|
||||||
|
if (!li.media.tags.includes(newTag)) {
|
||||||
|
li.media.tags.push(newTag) // Add new tag
|
||||||
|
}
|
||||||
|
Logger.debug(`[MiscController] Rename tag "${tag}" to "${newTag}" for item "${li.media.metadata.title}"`)
|
||||||
|
await this.db.updateLibraryItem(li)
|
||||||
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
|
numItemsUpdated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
tagMerged,
|
||||||
|
numItemsUpdated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: api/tags/:tag
|
||||||
|
async deleteTag(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[MiscController] Non-admin user attempted to deleteTag`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString()
|
||||||
|
|
||||||
|
let numItemsUpdated = 0
|
||||||
|
for (const li of this.db.libraryItems) {
|
||||||
|
if (!li.media.tags || !li.media.tags.length) continue
|
||||||
|
|
||||||
|
if (li.media.tags.includes(tag)) {
|
||||||
|
li.media.tags = li.media.tags.filter(t => t !== tag)
|
||||||
|
Logger.debug(`[MiscController] Remove tag "${tag}" from item "${li.media.metadata.title}"`)
|
||||||
|
await this.db.updateLibraryItem(li)
|
||||||
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
|
numItemsUpdated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
numItemsUpdated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/genres
|
||||||
|
getAllGenres(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
const genres = []
|
||||||
|
this.db.libraryItems.forEach((li) => {
|
||||||
|
if (li.media.metadata.genres && li.media.metadata.genres.length) {
|
||||||
|
li.media.metadata.genres.forEach((genre) => {
|
||||||
|
if (!genres.includes(genre)) genres.push(genre)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
res.json({
|
||||||
|
genres
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/genres/rename
|
||||||
|
async renameGenre(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[MiscController] Non-admin user attempted to renameGenre`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const genre = req.body.genre
|
||||||
|
const newGenre = req.body.newGenre
|
||||||
|
if (!genre || !newGenre) {
|
||||||
|
Logger.error(`[MiscController] Invalid request body for renameGenre`)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
let genreMerged = false
|
||||||
|
let numItemsUpdated = 0
|
||||||
|
|
||||||
|
for (const li of this.db.libraryItems) {
|
||||||
|
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||||
|
|
||||||
|
if (li.media.metadata.genres.includes(newGenre)) genreMerged = true // new genre is an existing genre so this is a merge
|
||||||
|
|
||||||
|
if (li.media.metadata.genres.includes(genre)) {
|
||||||
|
li.media.metadata.genres = li.media.metadata.genres.filter(g => g !== genre) // Remove old genre
|
||||||
|
if (!li.media.metadata.genres.includes(newGenre)) {
|
||||||
|
li.media.metadata.genres.push(newGenre) // Add new genre
|
||||||
|
}
|
||||||
|
Logger.debug(`[MiscController] Rename genre "${genre}" to "${newGenre}" for item "${li.media.metadata.title}"`)
|
||||||
|
await this.db.updateLibraryItem(li)
|
||||||
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
|
numItemsUpdated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
genreMerged,
|
||||||
|
numItemsUpdated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: api/genres/:genre
|
||||||
|
async deleteGenre(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString()
|
||||||
|
|
||||||
|
let numItemsUpdated = 0
|
||||||
|
for (const li of this.db.libraryItems) {
|
||||||
|
if (!li.media.metadata.genres || !li.media.metadata.genres.length) continue
|
||||||
|
|
||||||
|
if (li.media.metadata.genres.includes(genre)) {
|
||||||
|
li.media.metadata.genres = li.media.metadata.genres.filter(t => t !== genre)
|
||||||
|
Logger.debug(`[MiscController] Remove genre "${genre}" from item "${li.media.metadata.title}"`)
|
||||||
|
await this.db.updateLibraryItem(li)
|
||||||
|
SocketAuthority.emitter('item_updated', li.toJSONExpanded())
|
||||||
|
numItemsUpdated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
numItemsUpdated
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
validateCronExpression(req, res) {
|
validateCronExpression(req, res) {
|
||||||
|
|||||||
@@ -174,9 +174,42 @@ class PlaylistController {
|
|||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/playlists/collection/:collectionId
|
||||||
|
async createFromCollection(req, res) {
|
||||||
|
let collection = this.db.collections.find(c => c.id === req.params.collectionId)
|
||||||
|
if (!collection) {
|
||||||
|
return res.status(404).send('Collection not found')
|
||||||
|
}
|
||||||
|
// Expand collection to get library items
|
||||||
|
collection = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
|
|
||||||
|
// Filter out library items not accessible to user
|
||||||
|
const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
|
||||||
|
|
||||||
|
if (!libraryItems.length) {
|
||||||
|
return res.status(400).send('Collection has no books accessible to user')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPlaylist = new Playlist()
|
||||||
|
|
||||||
|
const newPlaylistData = {
|
||||||
|
userId: req.user.id,
|
||||||
|
libraryId: collection.libraryId,
|
||||||
|
name: collection.name,
|
||||||
|
description: collection.description || null,
|
||||||
|
items: libraryItems.map(li => ({ libraryItemId: li.id }))
|
||||||
|
}
|
||||||
|
newPlaylist.setData(newPlaylistData)
|
||||||
|
|
||||||
|
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
||||||
|
await this.db.insertEntity('playlist', newPlaylist)
|
||||||
|
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||||
|
res.json(jsonExpanded)
|
||||||
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
if (req.params.id) {
|
if (req.params.id) {
|
||||||
var playlist = this.db.playlists.find(p => p.id === req.params.id)
|
const playlist = this.db.playlists.find(p => p.id === req.params.id)
|
||||||
if (!playlist) {
|
if (!playlist) {
|
||||||
return res.status(404).send('Playlist not found')
|
return res.status(404).send('Playlist not found')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ class SearchController {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async findBooks(req, res) {
|
async findBooks(req, res) {
|
||||||
var provider = req.query.provider || 'google'
|
const provider = req.query.provider || 'google'
|
||||||
var title = req.query.title || ''
|
const title = req.query.title || ''
|
||||||
var author = req.query.author || ''
|
const author = req.query.author || ''
|
||||||
var results = await this.bookFinder.search(provider, title, author)
|
const results = await this.bookFinder.search(provider, title, author)
|
||||||
res.json(results)
|
res.json(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findCovers(req, res) {
|
async findCovers(req, res) {
|
||||||
var query = req.query
|
const query = req.query
|
||||||
const podcast = query.podcast == 1
|
const podcast = query.podcast == 1
|
||||||
|
|
||||||
if (!query.title) {
|
if (!query.title) {
|
||||||
@@ -20,28 +20,30 @@ class SearchController {
|
|||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = null
|
let results = null
|
||||||
if (podcast) result = await this.podcastFinder.findCovers(query.title)
|
if (podcast) results = await this.podcastFinder.findCovers(query.title)
|
||||||
else result = await this.bookFinder.findCovers(query.provider || 'google', query.title, query.author || null)
|
else results = await this.bookFinder.findCovers(query.provider || 'google', query.title, query.author || null)
|
||||||
res.json(result)
|
res.json({
|
||||||
|
results
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async findPodcasts(req, res) {
|
async findPodcasts(req, res) {
|
||||||
var term = req.query.term
|
const term = req.query.term
|
||||||
var results = await this.podcastFinder.search(term)
|
const results = await this.podcastFinder.search(term)
|
||||||
res.json(results)
|
res.json(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAuthor(req, res) {
|
async findAuthor(req, res) {
|
||||||
var query = req.query.q
|
const query = req.query.q
|
||||||
var author = await this.authorFinder.findAuthorByName(query)
|
const author = await this.authorFinder.findAuthorByName(query)
|
||||||
res.json(author)
|
res.json(author)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findChapters(req, res) {
|
async findChapters(req, res) {
|
||||||
var asin = req.query.asin
|
const asin = req.query.asin
|
||||||
var region = (req.query.region || 'us').toLowerCase()
|
const region = (req.query.region || 'us').toLowerCase()
|
||||||
var chapterData = await this.bookFinder.findChapters(asin, region)
|
const chapterData = await this.bookFinder.findChapters(asin, region)
|
||||||
if (!chapterData) {
|
if (!chapterData) {
|
||||||
return res.json({ error: 'Chapters not found' })
|
return res.json({ error: 'Chapters not found' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ class SeriesController {
|
|||||||
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
var limit = (req.query.limit && !isNaN(req.query.limit)) ? Number(req.query.limit) : 25
|
||||||
var series = this.db.series.filter(se => se.name.toLowerCase().includes(q))
|
var series = this.db.series.filter(se => se.name.toLowerCase().includes(q))
|
||||||
series = series.slice(0, limit)
|
series = series.slice(0, limit)
|
||||||
res.json(series)
|
res.json({
|
||||||
|
results: series
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ class UserController {
|
|||||||
if (!req.user.isAdminOrUp) return res.sendStatus(403)
|
if (!req.user.isAdminOrUp) return res.sendStatus(403)
|
||||||
const hideRootToken = !req.user.isRoot
|
const hideRootToken = !req.user.isRoot
|
||||||
const users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u, hideRootToken))
|
const users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u, hideRootToken))
|
||||||
res.json(users)
|
res.json({
|
||||||
|
users: users
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
findOne(req, res) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class CacheManager {
|
|||||||
|
|
||||||
res.type(`image/${format}`)
|
res.type(`image/${format}`)
|
||||||
|
|
||||||
var path = Path.join(this.CoverCachePath, `${libraryItem.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
|
const path = Path.join(this.CoverCachePath, `${libraryItem.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
|
||||||
|
|
||||||
// Cache exists
|
// Cache exists
|
||||||
if (await fs.pathExists(path)) {
|
if (await fs.pathExists(path)) {
|
||||||
@@ -66,7 +66,7 @@ class CacheManager {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
let writtenFile = await resizeImage(libraryItem.media.coverPath, path, width, height)
|
const writtenFile = await resizeImage(libraryItem.media.coverPath, path, width, height)
|
||||||
if (!writtenFile) return res.sendStatus(500)
|
if (!writtenFile) return res.sendStatus(500)
|
||||||
|
|
||||||
// Set owner and permissions of cache image
|
// Set owner and permissions of cache image
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
const filePerms = require('../utils/filePerms')
|
||||||
|
|
||||||
const DailyLog = require('../objects/DailyLog')
|
const DailyLog = require('../objects/DailyLog')
|
||||||
|
|
||||||
@@ -11,8 +12,8 @@ class LogManager {
|
|||||||
constructor(db) {
|
constructor(db) {
|
||||||
this.db = db
|
this.db = db
|
||||||
|
|
||||||
this.logDirPath = Path.join(global.MetadataPath, 'logs')
|
this.DailyLogPath = Path.posix.join(global.MetadataPath, 'logs', 'daily')
|
||||||
this.dailyLogDirPath = Path.join(this.logDirPath, 'daily')
|
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
||||||
|
|
||||||
this.currentDailyLog = null
|
this.currentDailyLog = null
|
||||||
this.dailyLogBuffer = []
|
this.dailyLogBuffer = []
|
||||||
@@ -27,24 +28,38 @@ class LogManager {
|
|||||||
return this.serverSettings.loggerDailyLogsToKeep || 7
|
return this.serverSettings.loggerDailyLogsToKeep || 7
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ensureLogDirs() {
|
||||||
|
await fs.ensureDir(this.DailyLogPath)
|
||||||
|
await fs.ensureDir(this.ScanLogPath)
|
||||||
|
await filePerms.setDefault(Path.posix.join(global.MetadataPath, 'logs'), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureScanLogDir() {
|
||||||
|
if (!(await fs.pathExists(this.ScanLogPath))) {
|
||||||
|
await fs.mkdir(this.ScanLogPath)
|
||||||
|
await filePerms.setDefault(this.ScanLogPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
await this.ensureLogDirs()
|
||||||
|
|
||||||
// Load daily logs
|
// Load daily logs
|
||||||
await this.scanLogFiles()
|
await this.scanLogFiles()
|
||||||
|
|
||||||
// Check remove extra daily logs
|
// Check remove extra daily logs
|
||||||
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
||||||
var dailyLogFilesCopy = [...this.dailyLogFiles]
|
const dailyLogFilesCopy = [...this.dailyLogFiles]
|
||||||
for (let i = 0; i < dailyLogFilesCopy.length - this.loggerDailyLogsToKeep; i++) {
|
for (let i = 0; i < dailyLogFilesCopy.length - this.loggerDailyLogsToKeep; i++) {
|
||||||
var logFileToRemove = dailyLogFilesCopy[i]
|
await this.removeLogFile(dailyLogFilesCopy[i])
|
||||||
await this.removeLogFile(logFileToRemove)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename()
|
const currentDailyLogFilename = DailyLog.getCurrentDailyLogFilename()
|
||||||
Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`)
|
Logger.info(TAG, `Init current daily log filename: ${currentDailyLogFilename}`)
|
||||||
|
|
||||||
this.currentDailyLog = new DailyLog()
|
this.currentDailyLog = new DailyLog()
|
||||||
this.currentDailyLog.setData({ dailyLogDirPath: this.dailyLogDirPath })
|
this.currentDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
|
||||||
|
|
||||||
if (this.dailyLogFiles.includes(currentDailyLogFilename)) {
|
if (this.dailyLogFiles.includes(currentDailyLogFilename)) {
|
||||||
Logger.debug(TAG, `Daily log file already exists - set in Logger`)
|
Logger.debug(TAG, `Daily log file already exists - set in Logger`)
|
||||||
@@ -63,8 +78,7 @@ class LogManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanLogFiles() {
|
async scanLogFiles() {
|
||||||
await fs.ensureDir(this.dailyLogDirPath)
|
const dailyFiles = await fs.readdir(this.DailyLogPath)
|
||||||
var dailyFiles = await fs.readdir(this.dailyLogDirPath)
|
|
||||||
if (dailyFiles && dailyFiles.length) {
|
if (dailyFiles && dailyFiles.length) {
|
||||||
dailyFiles.forEach((logFile) => {
|
dailyFiles.forEach((logFile) => {
|
||||||
if (Path.extname(logFile) === '.txt') {
|
if (Path.extname(logFile) === '.txt') {
|
||||||
@@ -80,13 +94,13 @@ class LogManager {
|
|||||||
|
|
||||||
async removeOldestLog() {
|
async removeOldestLog() {
|
||||||
if (!this.dailyLogFiles.length) return
|
if (!this.dailyLogFiles.length) return
|
||||||
var oldestLog = this.dailyLogFiles[0]
|
const oldestLog = this.dailyLogFiles[0]
|
||||||
return this.removeLogFile(oldestLog)
|
return this.removeLogFile(oldestLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeLogFile(filename) {
|
async removeLogFile(filename) {
|
||||||
var fullPath = Path.join(this.dailyLogDirPath, filename)
|
const fullPath = Path.join(this.DailyLogPath, filename)
|
||||||
var exists = await fs.pathExists(fullPath)
|
const exists = await fs.pathExists(fullPath)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
Logger.error(TAG, 'Invalid log dne ' + fullPath)
|
Logger.error(TAG, 'Invalid log dne ' + fullPath)
|
||||||
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename)
|
this.dailyLogFiles = this.dailyLogFiles.filter(dlf => dlf.filename !== filename)
|
||||||
@@ -109,8 +123,8 @@ class LogManager {
|
|||||||
|
|
||||||
// Check log rolls to next day
|
// Check log rolls to next day
|
||||||
if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) {
|
if (this.currentDailyLog.id !== DailyLog.getCurrentDateString()) {
|
||||||
var newDailyLog = new DailyLog()
|
const newDailyLog = new DailyLog()
|
||||||
newDailyLog.setData({ dailyLogDirPath: this.dailyLogDirPath })
|
newDailyLog.setData({ dailyLogDirPath: this.DailyLogPath })
|
||||||
this.currentDailyLog = newDailyLog
|
this.currentDailyLog = newDailyLog
|
||||||
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
if (this.dailyLogFiles.length > this.loggerDailyLogsToKeep) {
|
||||||
this.removeOldestLog()
|
this.removeOldestLog()
|
||||||
@@ -126,7 +140,7 @@ class LogManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastLogs = this.currentDailyLog.logs.slice(-5000)
|
const lastLogs = this.currentDailyLog.logs.slice(-5000)
|
||||||
socket.emit('daily_logs', lastLogs)
|
socket.emit('daily_logs', lastLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class PlaybackSessionManager {
|
|||||||
return this.sessions.find(s => s.userId === userId)
|
return this.sessions.find(s => s.userId === userId)
|
||||||
}
|
}
|
||||||
getStream(sessionId) {
|
getStream(sessionId) {
|
||||||
var session = this.getSession(sessionId)
|
const session = this.getSession(sessionId)
|
||||||
return session ? session.stream : null
|
return session ? session.stream : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncSessionRequest(user, session, payload, res) {
|
async syncSessionRequest(user, session, payload, res) {
|
||||||
var result = await this.syncSession(user, session, payload)
|
const result = await this.syncSession(user, session, payload)
|
||||||
if (result) {
|
if (result) {
|
||||||
res.json(session.toJSONForClient(result.libraryItem))
|
res.json(session.toJSONForClient(result.libraryItem))
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ class PlaybackSessionManager {
|
|||||||
return res.status(500).send('Local session is locked and already syncing')
|
return res.status(500).send('Local session is locked and already syncing')
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
|
const libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session "${sessionJson.libraryItemId}"`)
|
Logger.error(`[PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session "${sessionJson.libraryItemId}"`)
|
||||||
return res.status(500).send('Library item not found')
|
return res.status(500).send('Library item not found')
|
||||||
@@ -74,7 +74,7 @@ class PlaybackSessionManager {
|
|||||||
|
|
||||||
this.localSessionLock[sessionJson.id] = true // Lock local session
|
this.localSessionLock[sessionJson.id] = true // Lock local session
|
||||||
|
|
||||||
var session = await this.db.getPlaybackSession(sessionJson.id)
|
let session = await this.db.getPlaybackSession(sessionJson.id)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
// New session from local
|
// New session from local
|
||||||
session = new PlaybackSession(sessionJson)
|
session = new PlaybackSession(sessionJson)
|
||||||
@@ -96,10 +96,10 @@ class PlaybackSessionManager {
|
|||||||
progress: session.progress,
|
progress: session.progress,
|
||||||
lastUpdate: session.updatedAt // Keep media progress update times the same as local
|
lastUpdate: session.updatedAt // Keep media progress update times the same as local
|
||||||
}
|
}
|
||||||
var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
@@ -118,18 +118,25 @@ class PlaybackSessionManager {
|
|||||||
|
|
||||||
async startSession(user, deviceInfo, libraryItem, episodeId, options) {
|
async startSession(user, deviceInfo, libraryItem, episodeId, options) {
|
||||||
// Close any sessions already open for user
|
// Close any sessions already open for user
|
||||||
var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id)
|
const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id)
|
||||||
for (const session of userSessions) {
|
for (const session of userSessions) {
|
||||||
Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}"`)
|
Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}"`)
|
||||||
await this.closeSession(user, session, null)
|
await this.closeSession(user, session, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
|
const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
|
||||||
var mediaPlayer = options.mediaPlayer || 'unknown'
|
const mediaPlayer = options.mediaPlayer || 'unknown'
|
||||||
|
|
||||||
const userProgress = user.getMediaProgress(libraryItem.id, episodeId)
|
const userProgress = user.getMediaProgress(libraryItem.id, episodeId)
|
||||||
var userStartTime = 0
|
let userStartTime = 0
|
||||||
if (userProgress) userStartTime = Number.parseFloat(userProgress.currentTime) || 0
|
if (userProgress) {
|
||||||
|
if (userProgress.isFinished) {
|
||||||
|
Logger.info(`[PlaybackSessionManager] Starting session for user "${user.username}" and resetting progress for finished item "${libraryItem.media.metadata.title}"`)
|
||||||
|
// Keep userStartTime as 0 so the client restarts the media
|
||||||
|
} else {
|
||||||
|
userStartTime = Number.parseFloat(userProgress.currentTime) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
const newPlaybackSession = new PlaybackSession()
|
const newPlaybackSession = new PlaybackSession()
|
||||||
newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId)
|
newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId)
|
||||||
|
|
||||||
@@ -142,14 +149,14 @@ class PlaybackSessionManager {
|
|||||||
// HLS not supported for video yet
|
// HLS not supported for video yet
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var audioTracks = []
|
let audioTracks = []
|
||||||
if (shouldDirectPlay) {
|
if (shouldDirectPlay) {
|
||||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||||
audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
|
audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
|
||||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
||||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime)
|
const stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime)
|
||||||
await stream.generatePlaylist()
|
await stream.generatePlaylist()
|
||||||
stream.start() // Start transcode
|
stream.start() // Start transcode
|
||||||
|
|
||||||
@@ -175,7 +182,7 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async syncSession(user, session, syncData) {
|
async syncSession(user, session, syncData) {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
const libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
||||||
return null
|
return null
|
||||||
@@ -190,11 +197,11 @@ class PlaybackSessionManager {
|
|||||||
currentTime: syncData.currentTime,
|
currentTime: syncData.currentTime,
|
||||||
progress: session.progress
|
progress: session.progress
|
||||||
}
|
}
|
||||||
var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
|
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
@@ -229,7 +236,7 @@ class PlaybackSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeSession(sessionId) {
|
async removeSession(sessionId) {
|
||||||
var session = this.sessions.find(s => s.id === sessionId)
|
const session = this.sessions.find(s => s.id === sessionId)
|
||||||
if (!session) return
|
if (!session) return
|
||||||
if (session.stream) {
|
if (session.stream) {
|
||||||
await session.stream.close()
|
await session.stream.close()
|
||||||
@@ -242,13 +249,13 @@ class PlaybackSessionManager {
|
|||||||
async removeOrphanStreams() {
|
async removeOrphanStreams() {
|
||||||
await fs.ensureDir(this.StreamsPath)
|
await fs.ensureDir(this.StreamsPath)
|
||||||
try {
|
try {
|
||||||
var streamsInPath = await fs.readdir(this.StreamsPath)
|
const streamsInPath = await fs.readdir(this.StreamsPath)
|
||||||
for (let i = 0; i < streamsInPath.length; i++) {
|
for (let i = 0; i < streamsInPath.length; i++) {
|
||||||
var streamId = streamsInPath[i]
|
const streamId = streamsInPath[i]
|
||||||
if (streamId.startsWith('play_')) { // Make sure to only remove folders that are a stream
|
if (streamId.startsWith('play_')) { // Make sure to only remove folders that are a stream
|
||||||
var session = this.sessions.find(se => se.id === streamId)
|
const session = this.sessions.find(se => se.id === streamId)
|
||||||
if (!session) {
|
if (!session) {
|
||||||
var streamPath = Path.join(this.StreamsPath, streamId)
|
const streamPath = Path.join(this.StreamsPath, streamId)
|
||||||
Logger.debug(`[PlaybackSessionManager] Removing orphan stream "${streamPath}"`)
|
Logger.debug(`[PlaybackSessionManager] Removing orphan stream "${streamPath}"`)
|
||||||
await fs.remove(streamPath)
|
await fs.remove(streamPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ class Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJSONExpanded(libraryItems, minifiedBooks = false) {
|
toJSONExpanded(libraryItems, minifiedBooks = false) {
|
||||||
var json = this.toJSON()
|
const json = this.toJSON()
|
||||||
json.books = json.books.map(bookId => {
|
json.books = json.books.map(bookId => {
|
||||||
var _ab = libraryItems.find(li => li.id === bookId)
|
const book = libraryItems.find(li => li.id === bookId)
|
||||||
return _ab ? minifiedBooks ? _ab.toJSONMinified() : _ab.toJSONExpanded() : null
|
return book ? minifiedBooks ? book.toJSONMinified() : book.toJSONExpanded() : null
|
||||||
}).filter(b => !!b)
|
}).filter(b => !!b)
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ class FeedEpisode {
|
|||||||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
||||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||||
const timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
const timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
||||||
const audiobookPubDate = date.format(new Date(libraryItem.addedAt - timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
// e.g. Track 1 will have a pub date before Track 2
|
||||||
|
const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||||
|
|
||||||
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
|
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
|
||||||
const media = libraryItem.media
|
const media = libraryItem.media
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class User {
|
|||||||
this.seriesHideFromContinueListening = [] // Series IDs that should not show on home page continue listening
|
this.seriesHideFromContinueListening = [] // Series IDs that should not show on home page continue listening
|
||||||
this.bookmarks = []
|
this.bookmarks = []
|
||||||
|
|
||||||
this.settings = {}
|
this.settings = {} // TODO: Remove after mobile release v0.9.61-beta
|
||||||
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.itemTagsAccessible = [] // Empty if ALL item tags accessible
|
||||||
@@ -59,17 +59,12 @@ class User {
|
|||||||
return !!this.pash && !!this.pash.length
|
return !!this.pash && !!this.pash.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove after mobile release v0.9.61-beta
|
||||||
getDefaultUserSettings() {
|
getDefaultUserSettings() {
|
||||||
return {
|
return {
|
||||||
mobileOrderBy: 'recent',
|
mobileOrderBy: 'recent',
|
||||||
mobileOrderDesc: true,
|
mobileOrderDesc: true,
|
||||||
mobileFilterBy: 'all',
|
mobileFilterBy: 'all'
|
||||||
orderBy: 'media.metadata.title',
|
|
||||||
orderDesc: false,
|
|
||||||
filterBy: 'all',
|
|
||||||
playbackRate: 1,
|
|
||||||
bookshelfCoverSize: 120,
|
|
||||||
collapseSeries: false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +94,7 @@ class User {
|
|||||||
isLocked: this.isLocked,
|
isLocked: this.isLocked,
|
||||||
lastSeen: this.lastSeen,
|
lastSeen: this.lastSeen,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
settings: this.settings,
|
settings: this.settings, // TODO: Remove after mobile release v0.9.61-beta
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsAccessible: [...this.itemTagsAccessible]
|
itemTagsAccessible: [...this.itemTagsAccessible]
|
||||||
@@ -119,7 +114,7 @@ class User {
|
|||||||
isLocked: this.isLocked,
|
isLocked: this.isLocked,
|
||||||
lastSeen: this.lastSeen,
|
lastSeen: this.lastSeen,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
settings: this.settings,
|
settings: this.settings, // TODO: Remove after mobile release v0.9.61-beta
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsAccessible: [...this.itemTagsAccessible]
|
itemTagsAccessible: [...this.itemTagsAccessible]
|
||||||
@@ -171,7 +166,7 @@ class User {
|
|||||||
this.isLocked = user.type === 'root' ? false : !!user.isLocked
|
this.isLocked = user.type === 'root' ? false : !!user.isLocked
|
||||||
this.lastSeen = user.lastSeen || null
|
this.lastSeen = user.lastSeen || null
|
||||||
this.createdAt = user.createdAt || Date.now()
|
this.createdAt = user.createdAt || Date.now()
|
||||||
this.settings = user.settings || this.getDefaultUserSettings()
|
this.settings = user.settings || this.getDefaultUserSettings() // TODO: Remove after mobile release v0.9.61-beta
|
||||||
this.permissions = user.permissions || this.getDefaultUserPermissions()
|
this.permissions = user.permissions || this.getDefaultUserPermissions()
|
||||||
// Upload permission added v1.1.13, make sure root user has upload permissions
|
// Upload permission added v1.1.13, make sure root user has upload permissions
|
||||||
if (this.type === 'root' && !this.permissions.upload) this.permissions.upload = true
|
if (this.type === 'root' && !this.permissions.upload) this.permissions.upload = true
|
||||||
@@ -348,6 +343,7 @@ class User {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove after mobile release v0.9.61-beta
|
||||||
// Returns Boolean If update was made
|
// Returns Boolean If update was made
|
||||||
updateSettings(settings) {
|
updateSettings(settings) {
|
||||||
if (!this.settings) {
|
if (!this.settings) {
|
||||||
|
|||||||
@@ -15,7 +15,14 @@ class GoogleBooks {
|
|||||||
cleanResult(item) {
|
cleanResult(item) {
|
||||||
var { id, volumeInfo } = item
|
var { id, volumeInfo } = item
|
||||||
if (!volumeInfo) return null
|
if (!volumeInfo) return null
|
||||||
var { title, subtitle, authors, publisher, publisherDate, description, industryIdentifiers, categories, imageLinks } = volumeInfo
|
const { title, subtitle, authors, publisher, publisherDate, description, industryIdentifiers, categories, imageLinks } = volumeInfo
|
||||||
|
|
||||||
|
let cover = null
|
||||||
|
// Selects the largest cover assuming the largest is the last key in the object
|
||||||
|
if (imageLinks && Object.keys(imageLinks).length) {
|
||||||
|
cover = imageLinks[Object.keys(imageLinks).pop()]
|
||||||
|
cover = cover?.replace(/^http:/, 'https:') || null
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -25,7 +32,7 @@ class GoogleBooks {
|
|||||||
publisher,
|
publisher,
|
||||||
publishedYear: publisherDate ? publisherDate.split('-')[0] : null,
|
publishedYear: publisherDate ? publisherDate.split('-')[0] : null,
|
||||||
description,
|
description,
|
||||||
cover: imageLinks && imageLinks.thumbnail ? imageLinks.thumbnail : null,
|
cover,
|
||||||
genres: categories && Array.isArray(categories) ? [...categories] : null,
|
genres: categories && Array.isArray(categories) ? [...categories] : null,
|
||||||
isbn: this.extractIsbn(industryIdentifiers)
|
isbn: this.extractIsbn(industryIdentifiers)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// Playlist Routes
|
// Playlist Routes
|
||||||
//
|
//
|
||||||
this.router.post('/playlists', PlaylistController.middleware.bind(this), PlaylistController.create.bind(this))
|
this.router.post('/playlists', PlaylistController.create.bind(this))
|
||||||
this.router.get('/playlists', PlaylistController.findAllForUser.bind(this))
|
this.router.get('/playlists', PlaylistController.findAllForUser.bind(this))
|
||||||
this.router.get('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.findOne.bind(this))
|
this.router.get('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.findOne.bind(this))
|
||||||
this.router.patch('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.update.bind(this))
|
this.router.patch('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.update.bind(this))
|
||||||
@@ -152,6 +152,7 @@ class ApiRouter {
|
|||||||
this.router.delete('/playlists/:id/item/:libraryItemId/:episodeId?', PlaylistController.middleware.bind(this), PlaylistController.removeItem.bind(this))
|
this.router.delete('/playlists/:id/item/:libraryItemId/:episodeId?', PlaylistController.middleware.bind(this), PlaylistController.removeItem.bind(this))
|
||||||
this.router.post('/playlists/:id/batch/add', PlaylistController.middleware.bind(this), PlaylistController.addBatch.bind(this))
|
this.router.post('/playlists/:id/batch/add', PlaylistController.middleware.bind(this), PlaylistController.addBatch.bind(this))
|
||||||
this.router.post('/playlists/:id/batch/remove', PlaylistController.middleware.bind(this), PlaylistController.removeBatch.bind(this))
|
this.router.post('/playlists/:id/batch/remove', PlaylistController.middleware.bind(this), PlaylistController.removeBatch.bind(this))
|
||||||
|
this.router.post('/playlists/collection/:collectionId', PlaylistController.createFromCollection.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Current User Routes (Me)
|
// Current User Routes (Me)
|
||||||
@@ -168,7 +169,7 @@ class ApiRouter {
|
|||||||
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
|
||||||
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
|
||||||
this.router.patch('/me/password', MeController.updatePassword.bind(this))
|
this.router.patch('/me/password', MeController.updatePassword.bind(this))
|
||||||
this.router.patch('/me/settings', MeController.updateSettings.bind(this))
|
this.router.patch('/me/settings', MeController.updateSettings.bind(this)) // TODO: Remove after mobile release v0.9.61-beta
|
||||||
this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this))
|
this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this))
|
||||||
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this))
|
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this))
|
||||||
this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))
|
this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))
|
||||||
@@ -271,6 +272,11 @@ class ApiRouter {
|
|||||||
this.router.patch('/settings', MiscController.updateServerSettings.bind(this))
|
this.router.patch('/settings', MiscController.updateServerSettings.bind(this))
|
||||||
this.router.post('/authorize', MiscController.authorize.bind(this))
|
this.router.post('/authorize', MiscController.authorize.bind(this))
|
||||||
this.router.get('/tags', MiscController.getAllTags.bind(this))
|
this.router.get('/tags', MiscController.getAllTags.bind(this))
|
||||||
|
this.router.post('/tags/rename', MiscController.renameTag.bind(this))
|
||||||
|
this.router.delete('/tags/:tag', MiscController.deleteTag.bind(this))
|
||||||
|
this.router.get('/genres', MiscController.getAllGenres.bind(this))
|
||||||
|
this.router.post('/genres/rename', MiscController.renameGenre.bind(this))
|
||||||
|
this.router.delete('/genres/:genre', MiscController.deleteGenre.bind(this))
|
||||||
this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this))
|
this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const date = require('../libs/dateAndTime')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Folder = require('../objects/Folder')
|
const Folder = require('../objects/Folder')
|
||||||
const { LogLevel } = require('../utils/constants')
|
const { LogLevel } = require('../utils/constants')
|
||||||
|
const filePerms = require('../utils/filePerms')
|
||||||
const { getId, secondsToTimestamp } = require('../utils/index')
|
const { getId, secondsToTimestamp } = require('../utils/index')
|
||||||
|
|
||||||
class LibraryScan {
|
class LibraryScan {
|
||||||
@@ -61,7 +62,7 @@ class LibraryScan {
|
|||||||
get totalResults() {
|
get totalResults() {
|
||||||
return this.resultsAdded + this.resultsUpdated + this.resultsMissing
|
return this.resultsAdded + this.resultsUpdated + this.resultsMissing
|
||||||
}
|
}
|
||||||
get getLogFilename() {
|
get logFilename() {
|
||||||
return date.format(new Date(), 'YYYY-MM-DD') + '_' + this.id + '.txt'
|
return date.format(new Date(), 'YYYY-MM-DD') + '_' + this.id + '.txt'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,14 +125,18 @@ class LibraryScan {
|
|||||||
this.logs.push(logObj)
|
this.logs.push(logObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveLog(logDir) {
|
async saveLog() {
|
||||||
await fs.ensureDir(logDir)
|
await Logger.logManager.ensureScanLogDir()
|
||||||
var outputPath = Path.join(logDir, this.getLogFilename)
|
|
||||||
var logLines = [JSON.stringify(this.toJSON())]
|
const logDir = Path.join(global.MetadataPath, 'logs', 'scans')
|
||||||
|
const outputPath = Path.join(logDir, this.logFilename)
|
||||||
|
const logLines = [JSON.stringify(this.toJSON())]
|
||||||
this.logs.forEach(l => {
|
this.logs.forEach(l => {
|
||||||
logLines.push(JSON.stringify(l))
|
logLines.push(JSON.stringify(l))
|
||||||
})
|
})
|
||||||
await fs.writeFile(outputPath, logLines.join('\n') + '\n')
|
await fs.writeFile(outputPath, logLines.join('\n') + '\n')
|
||||||
|
await filePerms.setDefault(outputPath)
|
||||||
|
|
||||||
Logger.info(`[LibraryScan] Scan log saved "${outputPath}"`)
|
Logger.info(`[LibraryScan] Scan log saved "${outputPath}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ const Series = require('../objects/entities/Series')
|
|||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(db, coverManager) {
|
constructor(db, coverManager) {
|
||||||
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
|
|
||||||
|
|
||||||
this.db = db
|
this.db = db
|
||||||
this.coverManager = coverManager
|
this.coverManager = coverManager
|
||||||
|
|
||||||
@@ -165,7 +163,7 @@ class Scanner {
|
|||||||
SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData)
|
SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData)
|
||||||
|
|
||||||
if (libraryScan.totalResults) {
|
if (libraryScan.totalResults) {
|
||||||
libraryScan.saveLog(this.ScanLogPath)
|
libraryScan.saveLog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +614,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a library item is a subdirectory of this dir
|
// Check if a library item is a subdirectory of this dir
|
||||||
var childItem = this.db.libraryItems.find(li => li.path.startsWith(fullPath))
|
var childItem = this.db.libraryItems.find(li => (li.path + '/').startsWith(fullPath + '/'))
|
||||||
if (childItem) {
|
if (childItem) {
|
||||||
Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`)
|
Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`)
|
||||||
itemGroupingResults[itemDir] = ScanResult.NOTHING
|
itemGroupingResults[itemDir] = ScanResult.NOTHING
|
||||||
|
|||||||
+31
-15
@@ -183,16 +183,19 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet
|
// Most file systems use number of bytes for max filename
|
||||||
const MAX_FILENAME_LEN = 240
|
// to support most filesystems we will use max of 255 bytes in utf-16
|
||||||
|
// Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html
|
||||||
|
// Issue: https://github.com/advplyr/audiobookshelf/issues/1261
|
||||||
|
const MAX_FILENAME_BYTES = 255
|
||||||
|
|
||||||
var replacement = ''
|
const replacement = ''
|
||||||
var illegalRe = /[\/\?<>\\:\*\|"]/g
|
const illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g
|
const controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||||
var reservedRe = /^\.+$/
|
const reservedRe = /^\.+$/
|
||||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||||
var windowsTrailingRe = /[\. ]+$/
|
const windowsTrailingRe = /[\. ]+$/
|
||||||
var lineBreaks = /[\n\r]/g
|
const lineBreaks = /[\n\r]/g
|
||||||
|
|
||||||
sanitized = filename
|
sanitized = filename
|
||||||
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
||||||
@@ -203,12 +206,25 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
|||||||
.replace(windowsReservedRe, replacement)
|
.replace(windowsReservedRe, replacement)
|
||||||
.replace(windowsTrailingRe, replacement)
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
|
||||||
if (sanitized.length > MAX_FILENAME_LEN) {
|
// Check if basename is too many bytes
|
||||||
var lenToRemove = sanitized.length - MAX_FILENAME_LEN
|
const ext = Path.extname(sanitized) // separate out file extension
|
||||||
var ext = Path.extname(sanitized)
|
const basename = Path.basename(sanitized, ext)
|
||||||
var basename = Path.basename(sanitized, ext)
|
const extByteLength = Buffer.byteLength(ext, 'utf16le')
|
||||||
basename = basename.slice(0, basename.length - lenToRemove)
|
const basenameByteLength = Buffer.byteLength(basename, 'utf16le')
|
||||||
sanitized = basename + ext
|
if (basenameByteLength + extByteLength > MAX_FILENAME_BYTES) {
|
||||||
|
const MaxBytesForBasename = MAX_FILENAME_BYTES - extByteLength
|
||||||
|
let totalBytes = 0
|
||||||
|
let trimmedBasename = ''
|
||||||
|
|
||||||
|
// Add chars until max bytes is reached
|
||||||
|
for (const char of basename) {
|
||||||
|
totalBytes += Buffer.byteLength(char, 'utf16le')
|
||||||
|
if (totalBytes > MaxBytesForBasename) break
|
||||||
|
else trimmedBasename += char
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedBasename = trimmedBasename.trim()
|
||||||
|
sanitized = trimmedBasename + ext
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
|
|||||||
@@ -39,18 +39,19 @@ module.exports = {
|
|||||||
} else if (group == 'missing') {
|
} else if (group == 'missing') {
|
||||||
filtered = filtered.filter(li => {
|
filtered = filtered.filter(li => {
|
||||||
if (li.isBook) {
|
if (li.isBook) {
|
||||||
if (filter === 'asin' && li.media.metadata.asin === null) return true
|
if (filter === 'asin' && !li.media.metadata.asin) return true
|
||||||
if (filter === 'isbn' && li.media.metadata.isbn === null) return true
|
if (filter === 'isbn' && !li.media.metadata.isbn) return true
|
||||||
if (filter === 'subtitle' && li.media.metadata.subtitle === null) return true
|
if (filter === 'subtitle' && !li.media.metadata.subtitle) return true
|
||||||
if (filter === 'authors' && li.media.metadata.authors.length === 0) return true
|
if (filter === 'authors' && !li.media.metadata.authors.length) return true
|
||||||
if (filter === 'publishedYear' && li.media.metadata.publishedYear === null) return true
|
if (filter === 'publishedYear' && !li.media.metadata.publishedYear) return true
|
||||||
if (filter === 'series' && li.media.metadata.series.length === 0) return true
|
if (filter === 'series' && !li.media.metadata.series.length) return true
|
||||||
if (filter === 'description' && li.media.metadata.description === null) return true
|
if (filter === 'description' && !li.media.metadata.description) return true
|
||||||
if (filter === 'genres' && li.media.metadata.genres.length === 0) return true
|
if (filter === 'genres' && !li.media.metadata.genres.length) return true
|
||||||
if (filter === 'tags' && li.media.tags.length === 0) return true
|
if (filter === 'tags' && !li.media.tags.length) return true
|
||||||
if (filter === 'narrators' && li.media.metadata.narrators.length === 0) return true
|
if (filter === 'narrators' && !li.media.metadata.narrators.length) return true
|
||||||
if (filter === 'publisher' && li.media.metadata.publisher === null) return true
|
if (filter === 'publisher' && !li.media.metadata.publisher) return true
|
||||||
if (filter === 'language' && li.media.metadata.language === null) return true
|
if (filter === 'language' && !li.media.metadata.language) return true
|
||||||
|
if (filter === 'cover' && !li.media.coverPath) return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user