mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-05 10:12:44 +02:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14ee17de47 | |||
| 034b8956a2 | |||
| 1a3f0e332e | |||
| fc36e86db7 | |||
| 60b4bc1a7e | |||
| 9fdc8df8bc | |||
| 212b97fa20 | |||
| 704fbaced8 | |||
| 575a162f8b | |||
| d2e0844493 | |||
| f2baf3fafd | |||
| 916fd039ca | |||
| e248b6d8d8 | |||
| 936de68622 | |||
| a99257e758 | |||
| c89d77dd06 | |||
| 3138865d69 | |||
| 4d29ebd647 | |||
| fd58df4729 | |||
| 5078818295 | |||
| 7181df0479 | |||
| 6c618d7760 | |||
| 17b8cf19b7 | |||
| e018f8341e | |||
| 59b5f8cbbe | |||
| d6108a0722 | |||
| 1af7e59d88 | |||
| 7b425e9a9d | |||
| 596a03900b | |||
| b283644d95 | |||
| 808690c137 | |||
| 136c347586 | |||
| e81238038e | |||
| fcf6964d7d | |||
| bd75ad4576 | |||
| f970d8e539 | |||
| c49010b4e1 | |||
| 146093d81e | |||
| 11ccbf1913 | |||
| a4a334a18a | |||
| 387a37e4da | |||
| ebad304aa9 | |||
| 8b557a0cb9 | |||
| 40b808e73d | |||
| a8b57a1ce9 | |||
| 35315843f2 | |||
| 27b9d3b94f | |||
| 0010ac5a40 | |||
| 884808f34e | |||
| f75ed07497 | |||
| b707d6f3c9 | |||
| a2d4a4a906 | |||
| 434d743d99 | |||
| 30f16b05fe | |||
| 92a88f4416 | |||
| 5c9c122af2 | |||
| 620d5ce578 | |||
| 363e1cee4b | |||
| 93f576772a | |||
| d4612bae92 | |||
| e01af27008 | |||
| 657fe0a650 | |||
| 9a6ec5548e | |||
| 0807509ea7 | |||
| d9d1c4e360 | |||
| 2135e5b066 | |||
| b69eb10ae0 | |||
| e1512b6f54 | |||
| 1b8e8215d6 | |||
| 9b44e36e7b | |||
| db1ca08c2e | |||
| 557d3243c3 | |||
| 785942b94f | |||
| 3df7caa838 | |||
| aef2c52630 | |||
| dccad3055b | |||
| c629923a80 | |||
| b4f1fd5b25 | |||
| 267897ce74 | |||
| 022bf9d0ef | |||
| 61c759e0c4 | |||
| cfb3ce0c60 | |||
| 72396c5a98 | |||
| 12f231b886 | |||
| 6aeed24296 | |||
| d8b6e09bc0 | |||
| d95975cade | |||
| c4208a4690 | |||
| 7c7a6df6e4 | |||
| 791c058ef8 | |||
| c847aea0a4 | |||
| e56164aa5a | |||
| cfb5e909a9 | |||
| 071444a9e7 | |||
| 34ac972130 | |||
| 97b5cf04f5 | |||
| 0d50d730d9 | |||
| 3a7fd0bcc9 | |||
| f0edea5d52 | |||
| 9c6b07df99 | |||
| caacf461ab | |||
| 5bdbc75522 | |||
| 0d3e6b1d0a | |||
| a122e25cba | |||
| d7b287bfed | |||
| ba4f585318 | |||
| 3f859723a6 | |||
| c820d0e62b | |||
| 7a47032a96 | |||
| 2db4dd6a40 | |||
| f58e2b6dce | |||
| 859a53e79a | |||
| ad0edc6329 | |||
| 002fb7a35e | |||
| cc62a20a5d | |||
| ec7e965dfa | |||
| 9c3f5406a9 | |||
| f4ec6948d2 | |||
| 9a51c3be0f | |||
| b1ee54522a | |||
| c14d13440f | |||
| 8c84640484 | |||
| 0d8917ced6 | |||
| a006eb489d | |||
| f2941e04d3 | |||
| 2728546660 | |||
| eeb7c80518 | |||
| c8c40360ad | |||
| 79ab656217 | |||
| 5c250da388 | |||
| 505e0eb3a2 | |||
| 388444e51f | |||
| 08d7a9aa14 | |||
| f650ae7f18 | |||
| 6d138ae905 | |||
| 956678c08c | |||
| 911c854365 | |||
| 3c5dc17e3c | |||
| e709cc4cb1 | |||
| da7825e3e3 | |||
| 4039dc7968 | |||
| e345c4cc9e | |||
| a08cfa436e | |||
| 7207efb4da | |||
| 481611ff33 | |||
| b67cd37a38 | |||
| d2512d324a |
@@ -0,0 +1,44 @@
|
|||||||
|
name: Integration Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build and test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: setup nade
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install pkg
|
||||||
|
run: npm install -g pkg
|
||||||
|
|
||||||
|
- name: get client dependencies
|
||||||
|
working-directory: client
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build client
|
||||||
|
working-directory: client
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: get server dependencies
|
||||||
|
run: npm ci --only=production
|
||||||
|
|
||||||
|
- name: build binary
|
||||||
|
run: pkg -t node18-linux-x64 -o audiobookshelf .
|
||||||
|
|
||||||
|
- name: run audiobookshelf
|
||||||
|
run: |
|
||||||
|
./audiobookshelf &
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
- name: test if server is available
|
||||||
|
run: curl -sf http://127.0.0.1:3333 | grep Audiobookshelf
|
||||||
+1
-1
@@ -29,4 +29,4 @@ HEALTHCHECK \
|
|||||||
--timeout=3s \
|
--timeout=3s \
|
||||||
--start-period=10s \
|
--start-period=10s \
|
||||||
CMD curl -f http://127.0.0.1/healthcheck || exit 1
|
CMD curl -f http://127.0.0.1/healthcheck || exit 1
|
||||||
CMD ["npm", "start"]
|
CMD ["node", "index.js"]
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ $strings.ButtonPlay }}
|
{{ $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-tooltip v-if="userIsAdminOrUp && isBookLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
|
||||||
</ui-tooltip>
|
|
||||||
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -75,8 +72,11 @@
|
|||||||
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom">
|
|
||||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
<ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||||
|
|
||||||
|
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,9 +160,59 @@ export default {
|
|||||||
},
|
},
|
||||||
isHttps() {
|
isHttps() {
|
||||||
return location.protocol === 'https:' || process.env.NODE_ENV === 'development'
|
return location.protocol === 'https:' || process.env.NODE_ENV === 'development'
|
||||||
|
},
|
||||||
|
contextMenuItems() {
|
||||||
|
if (!this.userIsAdminOrUp) return []
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonQuickMatch,
|
||||||
|
action: 'quick-match'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
|
||||||
|
options.push({
|
||||||
|
text: 'Quick Embed Metadata',
|
||||||
|
action: 'quick-embed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
requestBatchQuickEmbed() {
|
||||||
|
const payload = {
|
||||||
|
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/tools/batch/embed-metadata`, {
|
||||||
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Audio metadata embed started')
|
||||||
|
this.cancelSelectionMode()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Audio metadata embed failed', error)
|
||||||
|
const errorMsg = error.response.data || 'Failed to embed metadata'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'quick-embed') {
|
||||||
|
this.requestBatchQuickEmbed()
|
||||||
|
} else if (action === 'quick-match') {
|
||||||
|
this.batchAutoMatchClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
async playSelectedItems() {
|
async playSelectedItems() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
|
|
||||||
|
|||||||
@@ -64,12 +64,22 @@
|
|||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
|
<!-- collapse series checkbox -->
|
||||||
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !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 && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||||
|
|
||||||
|
<!-- library filter select -->
|
||||||
<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" />
|
||||||
|
|
||||||
|
<!-- library sort select -->
|
||||||
<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" />
|
||||||
|
|
||||||
|
<!-- series filter select -->
|
||||||
<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-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" />
|
||||||
|
|
||||||
|
<!-- series sort select -->
|
||||||
<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" />
|
<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" />
|
||||||
|
|
||||||
|
<!-- issues page remove all button -->
|
||||||
<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>
|
||||||
<!-- search page -->
|
<!-- search page -->
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
||||||
<p>{{ route.title }}</p>
|
<p class="leading-4">{{ route.title }}</p>
|
||||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,14 @@
|
|||||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons text-2xl">file_download</span>
|
||||||
|
|
||||||
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
|
||||||
|
|
||||||
|
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||||
<span class="material-icons text-2xl">warning</span>
|
<span class="material-icons text-2xl">warning</span>
|
||||||
|
|
||||||
@@ -149,6 +157,9 @@ export default {
|
|||||||
isMusicLibrary() {
|
isMusicLibrary() {
|
||||||
return this.currentLibraryMediaType === 'music'
|
return this.currentLibraryMediaType === 'music'
|
||||||
},
|
},
|
||||||
|
isPodcastDownloadQueuePage() {
|
||||||
|
return this.$route.name === 'library-library-podcast-download-queue'
|
||||||
|
},
|
||||||
isPodcastSearchPage() {
|
isPodcastSearchPage() {
|
||||||
return this.$route.name === 'library-library-podcast-search'
|
return this.$route.name === 'library-library-podcast-search'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
<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-50 bg-primary px-2 md: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 md:h-40 z-50 bg-primary px-2 md: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-2 top-2 md:left-4 cursor-pointer">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
||||||
<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" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
||||||
<span class="material-icons text-sm">person</span>
|
<span class="material-icons text-sm">person</span>
|
||||||
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
<div class="flex items-center">
|
||||||
<p v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</p>
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||||
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
||||||
</p>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
<p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</p>
|
</div>
|
||||||
|
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
|
||||||
|
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-gray-400 flex items-center">
|
<div class="text-gray-400 flex items-center">
|
||||||
@@ -129,6 +132,9 @@ export default {
|
|||||||
isMusic() {
|
isMusic() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
|
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
|
||||||
},
|
},
|
||||||
|
isExplicit() {
|
||||||
|
return this.mediaMetadata.explicit || false
|
||||||
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="px-4 flex-grow">
|
<div v-else class="px-4 flex-grow">
|
||||||
<h1>{{ book.title }}</h1>
|
<h1>
|
||||||
|
<div class="flex items-center">
|
||||||
|
{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" />
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
||||||
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center h-full px-1 overflow-hidden">
|
||||||
|
<div class="h-5 w-5 min-w-5 text-lg mr-1.5 flex items-center justify-center">
|
||||||
|
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{actionIcon}}</span>
|
||||||
|
<widgets-loading-spinner v-else />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 taskRunningCardContent">
|
||||||
|
<p class="truncate text-sm">{{ title }}</p>
|
||||||
|
|
||||||
|
<p class="truncate text-xs text-gray-300">{{ description }}</p>
|
||||||
|
|
||||||
|
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
task: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return this.task.title || 'No Title'
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.task.description || ''
|
||||||
|
},
|
||||||
|
details() {
|
||||||
|
return this.task.details || 'Unknown'
|
||||||
|
},
|
||||||
|
isFinished() {
|
||||||
|
return this.task.isFinished || false
|
||||||
|
},
|
||||||
|
isFailed() {
|
||||||
|
return this.task.isFailed || false
|
||||||
|
},
|
||||||
|
failedMessage() {
|
||||||
|
return this.task.error || ''
|
||||||
|
},
|
||||||
|
action() {
|
||||||
|
return this.task.action || ''
|
||||||
|
},
|
||||||
|
actionIcon() {
|
||||||
|
switch (this.action) {
|
||||||
|
case 'download-podcast-episode':
|
||||||
|
return 'cloud_download'
|
||||||
|
case 'encode-m4b':
|
||||||
|
return 'sync'
|
||||||
|
default:
|
||||||
|
return 'settings'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
taskIconStatus() {
|
||||||
|
if (this.isFinished && this.isFailed) {
|
||||||
|
return 'text-red-500'
|
||||||
|
}
|
||||||
|
if (this.isFinished && !this.isFailed) {
|
||||||
|
return 'text-green-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.taskRunningCardContent {
|
||||||
|
width: calc(100% - 80px);
|
||||||
|
height: 75px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,9 +7,12 @@
|
|||||||
|
|
||||||
<!-- Alternative bookshelf title/author/sort -->
|
<!-- Alternative bookshelf title/author/sort -->
|
||||||
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||||
{{ displayTitle }}
|
<div class="flex items-center">
|
||||||
</p>
|
<span class="truncate">{{ displayTitle }}</span>
|
||||||
|
<widgets-explicit-indicator :explicit="isExplicit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p>
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || ' ' }}</p>
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,8 +105,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Episode # -->
|
<!-- Podcast Episode # -->
|
||||||
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
|
||||||
|
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
@@ -193,6 +198,9 @@ export default {
|
|||||||
isMusic() {
|
isMusic() {
|
||||||
return this.mediaType === 'music'
|
return this.mediaType === 'music'
|
||||||
},
|
},
|
||||||
|
isExplicit() {
|
||||||
|
return this.mediaMetadata.explicit || false
|
||||||
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
const config = this.$config || this.$nuxt.$config
|
const config = this.$config || this.$nuxt.$config
|
||||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
@@ -236,7 +244,7 @@ export default {
|
|||||||
if (this.recentEpisode.episode) {
|
if (this.recentEpisode.episode) {
|
||||||
return this.recentEpisode.episode.replace(/^#/, '')
|
return this.recentEpisode.episode.replace(/^#/, '')
|
||||||
}
|
}
|
||||||
return this.recentEpisode.index
|
return ''
|
||||||
},
|
},
|
||||||
collapsedSeries() {
|
collapsedSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
@@ -317,8 +325,13 @@ export default {
|
|||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
|
useEBookProgress() {
|
||||||
|
if (!this.userProgress || this.userProgress.progress) return false
|
||||||
|
return this.userProgress.ebookProgress > 0
|
||||||
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
if (this.useEBookProgress) return Math.max(Math.min(1, this.userProgress.ebookProgress), 0)
|
||||||
|
return this.userProgress ? Math.max(Math.min(1, this.userProgress.progress), 0) || 0 : 0
|
||||||
},
|
},
|
||||||
itemIsFinished() {
|
itemIsFinished() {
|
||||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
@@ -734,7 +747,7 @@ export default {
|
|||||||
episodeId: this.recentEpisode.id,
|
episodeId: this.recentEpisode.id,
|
||||||
title: this.recentEpisode.title,
|
title: this.recentEpisode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: this.recentEpisode.audioFile.duration || null,
|
duration: this.recentEpisode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -858,7 +871,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ export default {
|
|||||||
value: 'tracks',
|
value: 'tracks',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAbridged,
|
||||||
|
value: 'abridged',
|
||||||
|
sublist: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.ButtonIssues,
|
text: this.$strings.ButtonIssues,
|
||||||
value: 'issues',
|
value: 'issues',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
||||||
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
||||||
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base sm:text-lg">⨯</span></span>
|
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||||
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<template v-for="rate in rates">
|
<template v-for="rate in rates">
|
||||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||||
<div class="w-full h-full flex justify-center items-center">
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">⨯</span></p>
|
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="w-full py-1 px-4">
|
<div class="w-full py-1 px-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
||||||
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">⨯</span></p>
|
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
||||||
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ export default {
|
|||||||
},
|
},
|
||||||
canCreateBookmark() {
|
canCreateBookmark() {
|
||||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -111,7 +117,7 @@ export default {
|
|||||||
},
|
},
|
||||||
submitCreateBookmark() {
|
submitCreateBookmark() {
|
||||||
if (!this.newBookmarkTitle) {
|
if (!this.newBookmarkTitle) {
|
||||||
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
|
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)
|
||||||
}
|
}
|
||||||
var bookmark = {
|
var bookmark = {
|
||||||
title: this.newBookmarkTitle,
|
title: this.newBookmarkTitle,
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
<div class="flex items-center -mx-1 mb-1">
|
<div class="flex items-center -mx-1 mb-1">
|
||||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelStartedAt }}</div>
|
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelStartedAt }}</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
{{ $formatDate(_session.startedAt, 'MMMM do, yyyy HH:mm') }}
|
{{ $formatDatetime(_session.startedAt, dateFormat, timeFormat) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center -mx-1 mb-1">
|
<div class="flex items-center -mx-1 mb-1">
|
||||||
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelUpdatedAt }}</div>
|
<div class="w-40 px-1 text-gray-200">{{ $strings.LabelUpdatedAt }}</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
{{ $formatDate(_session.updatedAt, 'MMMM do, yyyy HH:mm') }}
|
{{ $formatDatetime(_session.updatedAt, dateFormat, timeFormat) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center -mx-1 mb-1">
|
<div class="flex items-center -mx-1 mb-1">
|
||||||
@@ -151,6 +151,12 @@ export default {
|
|||||||
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
||||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- book cover overlay -->
|
<!-- book cover overlay -->
|
||||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||||
@@ -27,14 +28,14 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-primary">
|
<div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-white border-opacity-10">
|
||||||
<div class="flex items-center justify-center py-2">
|
<div class="flex items-center justify-center py-2">
|
||||||
<p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p>
|
<p>{{ localCovers.length }} local image{{ localCovers.length !== 1 ? 's' : '' }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2">
|
||||||
<template v-for="cover in localCovers">
|
<template v-for="cover in localCovers">
|
||||||
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
|
|||||||
@@ -34,13 +34,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
|
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
|
||||||
<form @submit.prevent="submitMatchUpdate">
|
<form @submit.prevent="submitMatchUpdate">
|
||||||
<div v-if="selectedMatchOrig.cover" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.cover" class="flex flex-wrap md:flex-nowrap items-center justify-center">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
|
<div class="flex flex-grow items-center py-2">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="flex-grow mx-4" />
|
<ui-checkbox v-model="selectedMatchUsage.cover" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="min-w-12 max-w-12 md:min-w-16 md:max-w-16">
|
<ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" readonly :label="$strings.LabelCover" class="flex-grow mx-4" />
|
||||||
<a :href="selectedMatch.cover" target="_blank" class="w-full bg-primary">
|
</div>
|
||||||
<img :src="selectedMatch.cover" class="h-full w-full object-contain" />
|
|
||||||
</a>
|
<div class="flex py-2">
|
||||||
|
<div>
|
||||||
|
<p class="text-center text-gray-200">New</p>
|
||||||
|
<a :href="selectedMatch.cover" target="_blank" class="bg-primary">
|
||||||
|
<covers-preview-cover :src="selectedMatch.cover" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="media.coverPath">
|
||||||
|
<p class="text-center text-gray-200">Current</p>
|
||||||
|
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary">
|
||||||
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.title" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.title" class="flex items-center py-2">
|
||||||
@@ -164,6 +176,20 @@
|
|||||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
|
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }">
|
||||||
|
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
|
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.explicit != null }">
|
||||||
|
<ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" :disabled="!selectedMatchUsage.explicit" :checkbox-bg="!selectedMatchUsage.explicit ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? 'Explicit (checked)' : 'Not Explicit (unchecked)' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedMatchOrig.abridged != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.abridged == null }">
|
||||||
|
<ui-checkbox v-model="selectedMatchUsage.abridged" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
|
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.abridged != null }">
|
||||||
|
<ui-checkbox v-model="selectedMatch.abridged" :label="$strings.LabelAbridged" :disabled="!selectedMatchUsage.abridged" :checkbox-bg="!selectedMatchUsage.abridged ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
<p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? 'Abridged (checked)' : 'Unabridged (unchecked)' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end py-2">
|
<div class="flex items-center justify-end py-2">
|
||||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
@@ -209,6 +235,7 @@ export default {
|
|||||||
explicit: true,
|
explicit: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
isbn: true,
|
isbn: true,
|
||||||
|
abridged: true,
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
itunesPageUrl: true,
|
itunesPageUrl: true,
|
||||||
itunesId: true,
|
itunesId: true,
|
||||||
@@ -327,6 +354,7 @@ export default {
|
|||||||
res.itunesPageUrl = res.pageUrl || null
|
res.itunesPageUrl = res.pageUrl || null
|
||||||
res.itunesId = res.id || null
|
res.itunesId = res.id || null
|
||||||
res.author = res.artistName || null
|
res.author = res.artistName || null
|
||||||
|
res.explicit = res.explicit || false
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -352,6 +380,7 @@ export default {
|
|||||||
explicit: true,
|
explicit: true,
|
||||||
asin: true,
|
asin: true,
|
||||||
isbn: true,
|
isbn: true,
|
||||||
|
abridged: true,
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
itunesPageUrl: true,
|
itunesPageUrl: true,
|
||||||
itunesId: true,
|
itunesId: true,
|
||||||
@@ -468,7 +497,6 @@ export default {
|
|||||||
} else if (key === 'narrator') {
|
} else if (key === 'narrator') {
|
||||||
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
} else if (key === 'genres') {
|
} else if (key === 'genres') {
|
||||||
// updatePayload.metadata.genres = this.selectedMatch[key].split(',').map((v) => v.trim())
|
|
||||||
updatePayload.metadata.genres = [...this.selectedMatch[key]]
|
updatePayload.metadata.genres = [...this.selectedMatch[key]]
|
||||||
} else if (key === 'tags') {
|
} else if (key === 'tags') {
|
||||||
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
|
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ export default {
|
|||||||
newMaxNewEpisodesToDownload: 0
|
newMaxNewEpisodesToDownload: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
libraryItem: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isProcessing: {
|
isProcessing: {
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -46,8 +46,20 @@
|
|||||||
>{{ $strings.ButtonOpenManager }}
|
>{{ $strings.ButtonOpenManager }}
|
||||||
<span class="material-icons text-lg ml-2">launch</span>
|
<span class="material-icons text-lg ml-2">launch</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
|
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- queued alert -->
|
||||||
|
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
|
||||||
|
<p class="text-lg">Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||||
|
</widgets-alert>
|
||||||
|
|
||||||
|
<!-- processing alert -->
|
||||||
|
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
|
||||||
|
<p class="text-lg">Currently embedding metadata</p>
|
||||||
|
</widgets-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
||||||
@@ -71,10 +83,10 @@ export default {
|
|||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem?.id || null
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem?.media || {}
|
||||||
},
|
},
|
||||||
mediaTracks() {
|
mediaTracks() {
|
||||||
return this.media.tracks || []
|
return this.media.tracks || []
|
||||||
@@ -92,9 +104,49 @@ export default {
|
|||||||
showMp3Split() {
|
showMp3Split() {
|
||||||
if (!this.mediaTracks.length) return false
|
if (!this.mediaTracks.length) return false
|
||||||
return this.isSingleM4b && this.chapters.length
|
return this.isSingleM4b && this.chapters.length
|
||||||
|
},
|
||||||
|
queuedEmbedLIds() {
|
||||||
|
return this.$store.state.tasks.queuedEmbedLIds || []
|
||||||
|
},
|
||||||
|
isMetadataEmbedQueued() {
|
||||||
|
return this.queuedEmbedLIds.some((lid) => lid === this.libraryItemId)
|
||||||
|
},
|
||||||
|
tasks() {
|
||||||
|
return this.$store.getters['tasks/getTasksByLibraryItemId'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
embedTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'embed-metadata')
|
||||||
|
},
|
||||||
|
encodeTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'encode-m4b')
|
||||||
|
},
|
||||||
|
isEmbedTaskRunning() {
|
||||||
|
return this.embedTask && !this.embedTask?.isFinished
|
||||||
|
},
|
||||||
|
isEncodeTaskRunning() {
|
||||||
|
return this.encodeTask && !this.encodeTask?.isFinished
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
mounted() {}
|
quickEmbed() {
|
||||||
|
const payload = {
|
||||||
|
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata`)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Audio metadata encode started')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Audio metadata encode failed', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -11,8 +11,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
|
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
|
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||||
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episode" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episodeItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,8 +28,8 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
episodeItem: null,
|
||||||
processing: false,
|
processing: false,
|
||||||
selectedTab: 'details',
|
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
id: 'details',
|
id: 'details',
|
||||||
@@ -37,6 +44,29 @@ export default {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
const availableTabIds = this.tabs.map((tab) => tab.id)
|
||||||
|
if (!availableTabIds.length) {
|
||||||
|
this.show = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!availableTabIds.includes(this.selectedTab)) {
|
||||||
|
this.selectedTab = availableTabIds[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.episodeItem = null
|
||||||
|
this.init()
|
||||||
|
this.registerListeners()
|
||||||
|
} else {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
get() {
|
get() {
|
||||||
@@ -46,27 +76,118 @@ export default {
|
|||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
selectedTab: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.editPodcastModalTab
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('setEditPodcastModalTab', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
libraryItem() {
|
libraryItem() {
|
||||||
return this.$store.state.selectedLibraryItem
|
return this.$store.state.selectedLibraryItem
|
||||||
},
|
},
|
||||||
episode() {
|
episode() {
|
||||||
return this.$store.state.globals.selectedEpisode
|
return this.$store.state.globals.selectedEpisode
|
||||||
},
|
},
|
||||||
|
selectedEpisodeId() {
|
||||||
|
return this.episode.id
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
if (!this.libraryItem) return ''
|
return this.libraryItem?.media.metadata.title || 'Unknown'
|
||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
|
||||||
},
|
},
|
||||||
tabComponentName() {
|
tabComponentName() {
|
||||||
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
const _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
return _tab ? _tab.component : ''
|
return _tab ? _tab.component : ''
|
||||||
|
},
|
||||||
|
episodeTableEpisodeIds() {
|
||||||
|
return this.$store.state.episodeTableEpisodeIds || []
|
||||||
|
},
|
||||||
|
currentEpisodeIndex() {
|
||||||
|
if (!this.episodeTableEpisodeIds.length) return 0
|
||||||
|
return this.episodeTableEpisodeIds.findIndex((bid) => bid === this.selectedEpisodeId)
|
||||||
|
},
|
||||||
|
canGoPrev() {
|
||||||
|
return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex > 0
|
||||||
|
},
|
||||||
|
canGoNext() {
|
||||||
|
return this.episodeTableEpisodeIds.length && this.currentEpisodeIndex < this.episodeTableEpisodeIds.length - 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async goPrevEpisode() {
|
||||||
|
if (this.currentEpisodeIndex - 1 < 0) return
|
||||||
|
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
|
||||||
|
this.processing = true
|
||||||
|
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
|
||||||
|
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
if (prevEpisode) {
|
||||||
|
this.episodeItem = prevEpisode
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', prevEpisode)
|
||||||
|
} else {
|
||||||
|
console.error('Episode not found', prevEpisodeId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async goNextEpisode() {
|
||||||
|
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
|
||||||
|
this.processing = true
|
||||||
|
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
|
||||||
|
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
|
||||||
|
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
if (nextEpisode) {
|
||||||
|
this.episodeItem = nextEpisode
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', nextEpisode)
|
||||||
|
} else {
|
||||||
|
console.error('Episode not found', nextEpisodeId)
|
||||||
|
}
|
||||||
|
},
|
||||||
selectTab(tab) {
|
selectTab(tab) {
|
||||||
this.selectedTab = tab
|
if (this.selectedTab === tab) return
|
||||||
|
if (this.tabs.find((t) => t.id === tab)) {
|
||||||
|
this.selectedTab = tab
|
||||||
|
this.processing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.fetchFull()
|
||||||
|
},
|
||||||
|
async fetchFull() {
|
||||||
|
try {
|
||||||
|
this.processing = true
|
||||||
|
this.episodeItem = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${this.selectedEpisodeId}`)
|
||||||
|
this.processing = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch episode', this.selectedEpisodeId, error)
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hotkey(action) {
|
||||||
|
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
|
||||||
|
this.goNextEpisode()
|
||||||
|
} else if (action === this.$hotkeys.Modal.PREV_PAGE) {
|
||||||
|
this.goPrevEpisode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
registerListeners() {
|
||||||
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
|
},
|
||||||
|
unregisterListeners() {
|
||||||
|
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.unregisterListeners()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,21 +6,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
|
<div ref="episodeContainer" id="episodes-scroll" class="w-full overflow-x-hidden overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-for="(episode, index) in episodes"
|
v-for="(episode, index) in episodesList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative"
|
class="relative"
|
||||||
:class="itemEpisodeMap[episode.enclosure.url] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
:class="itemEpisodeMap[episode.enclosure.url?.split('?')[0]] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
||||||
@click="toggleSelectEpisode(index, episode)"
|
@click="toggleSelectEpisode(index, episode)"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||||
<span v-if="itemEpisodeMap[episode.enclosure.url]" class="material-icons text-success text-xl">download_done</span>
|
<span v-if="itemEpisodeMap[episode.enclosure.url?.split('?')[0]]" class="material-icons text-success text-xl">download_done</span>
|
||||||
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
<div class="px-8 py-2">
|
||||||
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
<div class="flex items-center font-semibold text-gray-200">
|
||||||
<p class="break-words mb-1">{{ episode.title }}</p>
|
<div v-if="episode.season || episode.episode">#</div>
|
||||||
|
<div v-if="episode.season">{{ episode.season }}x</div>
|
||||||
|
<div v-if="episode.episode">{{ episode.episode }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<div class="break-words">{{ episode.title }}</div>
|
||||||
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
||||||
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +64,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
selectedEpisodes: {},
|
selectedEpisodes: {},
|
||||||
selectAll: false
|
selectAll: false,
|
||||||
|
search: null,
|
||||||
|
searchTimeout: null,
|
||||||
|
searchText: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -77,7 +92,7 @@ export default {
|
|||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||||
},
|
},
|
||||||
allDownloaded() {
|
allDownloaded() {
|
||||||
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url])
|
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]])
|
||||||
},
|
},
|
||||||
episodesSelected() {
|
episodesSelected() {
|
||||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
||||||
@@ -93,23 +108,39 @@ export default {
|
|||||||
itemEpisodeMap() {
|
itemEpisodeMap() {
|
||||||
var map = {}
|
var map = {}
|
||||||
this.itemEpisodes.forEach((item) => {
|
this.itemEpisodes.forEach((item) => {
|
||||||
if (item.enclosure) map[item.enclosure.url] = true
|
if (item.enclosure) map[item.enclosure.url.split('?')[0]] = true
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
|
},
|
||||||
|
episodesList() {
|
||||||
|
return this.episodes.filter((episode) => {
|
||||||
|
if (!this.searchText) return true
|
||||||
|
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
inputUpdate() {
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
if (!this.search || !this.search.trim()) {
|
||||||
|
this.searchText = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.searchText = this.search.toLowerCase().trim()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
toggleSelectAll(val) {
|
toggleSelectAll(val) {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
const episode = this.episodes[i]
|
const episode = this.episodes[i]
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url]) this.selectedEpisodes[String(i)] = false
|
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) this.selectedEpisodes[String(i)] = false
|
||||||
else this.$set(this.selectedEpisodes, String(i), val)
|
else this.$set(this.selectedEpisodes, String(i), val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkSetIsSelectedAll() {
|
checkSetIsSelectedAll() {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (let i = 0; i < this.episodes.length; i++) {
|
||||||
const episode = this.episodes[i]
|
const episode = this.episodes[i]
|
||||||
if (!this.itemEpisodeMap[episode.enclosure.url] && !this.selectedEpisodes[String(i)]) {
|
if (!this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]] && !this.selectedEpisodes[String(i)]) {
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,7 +148,7 @@ export default {
|
|||||||
this.selectAll = true
|
this.selectAll = true
|
||||||
},
|
},
|
||||||
toggleSelectEpisode(index, episode) {
|
toggleSelectEpisode(index, episode) {
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url]) return
|
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return
|
||||||
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
||||||
this.checkSetIsSelectedAll()
|
this.checkSetIsSelectedAll()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,17 @@
|
|||||||
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" />
|
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="$strings.LabelGenres" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div class="md:w-1/4 p-2">
|
||||||
|
<ui-dropdown :label="$strings.LabelPodcastType" v-model="podcast.type" :items="podcastTypes" small />
|
||||||
|
</div>
|
||||||
|
<div class="md:w-1/4 p-2">
|
||||||
|
<ui-text-input-with-label v-model="podcast.language" :label="$strings.LabelLanguage" />
|
||||||
|
</div>
|
||||||
|
<div class="md:w-1/4 px-2 pt-7">
|
||||||
|
<ui-checkbox v-model="podcast.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="p-2 w-full">
|
<div class="p-2 w-full">
|
||||||
<ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" />
|
<ui-textarea-with-label v-model="podcast.description" :label="$strings.LabelDescription" :rows="3" />
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +93,10 @@ export default {
|
|||||||
itunesPageUrl: '',
|
itunesPageUrl: '',
|
||||||
itunesId: '',
|
itunesId: '',
|
||||||
itunesArtistId: '',
|
itunesArtistId: '',
|
||||||
autoDownloadEpisodes: false
|
autoDownloadEpisodes: false,
|
||||||
|
language: '',
|
||||||
|
explicit: false,
|
||||||
|
type: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -140,6 +154,9 @@ export default {
|
|||||||
selectedFolderPath() {
|
selectedFolderPath() {
|
||||||
if (!this.selectedFolder) return ''
|
if (!this.selectedFolder) return ''
|
||||||
return this.selectedFolder.fullPath
|
return this.selectedFolder.fullPath
|
||||||
|
},
|
||||||
|
podcastTypes() {
|
||||||
|
return this.$store.state.globals.podcastTypes || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -170,7 +187,9 @@ export default {
|
|||||||
itunesPageUrl: this.podcast.itunesPageUrl,
|
itunesPageUrl: this.podcast.itunesPageUrl,
|
||||||
itunesId: this.podcast.itunesId,
|
itunesId: this.podcast.itunesId,
|
||||||
itunesArtistId: this.podcast.itunesArtistId,
|
itunesArtistId: this.podcast.itunesArtistId,
|
||||||
language: this.podcast.language
|
language: this.podcast.language,
|
||||||
|
explicit: this.podcast.explicit,
|
||||||
|
type: this.podcast.type
|
||||||
},
|
},
|
||||||
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
||||||
}
|
}
|
||||||
@@ -205,9 +224,11 @@ export default {
|
|||||||
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
|
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
|
||||||
this.podcast.itunesId = this._podcastData.id || ''
|
this.podcast.itunesId = this._podcastData.id || ''
|
||||||
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
||||||
this.podcast.language = this._podcastData.language || ''
|
this.podcast.language = this._podcastData.language || this.feedMetadata.language || ''
|
||||||
this.podcast.autoDownloadEpisodes = false
|
this.podcast.autoDownloadEpisodes = false
|
||||||
|
this.podcast.type = this._podcastData.type || this.feedMetadata.type || 'episodic'
|
||||||
|
|
||||||
|
this.podcast.explicit = this._podcastData.explicit || this.feedMetadata.explicit === 'yes' || this.feedMetadata.explicit == 'true'
|
||||||
if (this.folderItems[0]) {
|
if (this.folderItems[0]) {
|
||||||
this.selectedFolderId = this.folderItems[0].value
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
this.folderUpdated()
|
this.folderUpdated()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
|
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 p-1">
|
<div class="w-1/5 p-1">
|
||||||
<ui-text-input-with-label v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" />
|
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-2/5 p-1">
|
<div class="w-2/5 p-1">
|
||||||
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
|
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
|
||||||
@@ -24,7 +24,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end pt-4">
|
<div class="flex items-center justify-end pt-4">
|
||||||
<ui-btn @click="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<!-- desktop -->
|
||||||
|
<ui-btn @click="submit" class="mx-2 hidden md:block">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn @click="saveAndClose" class="mx-2 hidden md:block">{{ $strings.ButtonSaveAndClose }}</ui-btn>
|
||||||
|
|
||||||
|
<!-- mobile -->
|
||||||
|
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enclosureUrl" class="py-4">
|
<div v-if="enclosureUrl" class="py-4">
|
||||||
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
|
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
|
||||||
@@ -89,6 +94,9 @@ export default {
|
|||||||
},
|
},
|
||||||
enclosureUrl() {
|
enclosureUrl() {
|
||||||
return this.enclosure.url
|
return this.enclosure.url
|
||||||
|
},
|
||||||
|
episodeTypes() {
|
||||||
|
return this.$store.state.globals.episodeTypes || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -122,26 +130,41 @@ export default {
|
|||||||
}
|
}
|
||||||
return updatePayload
|
return updatePayload
|
||||||
},
|
},
|
||||||
submit() {
|
async saveAndClose() {
|
||||||
const payload = this.getUpdatePayload()
|
const wasUpdated = await this.submit()
|
||||||
if (!Object.keys(payload).length) {
|
if (wasUpdated !== null) this.$emit('close')
|
||||||
return this.$toast.info('No updates were made')
|
},
|
||||||
|
async submit() {
|
||||||
|
if (this.isProcessing) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedDetails = this.getUpdatePayload()
|
||||||
|
if (!Object.keys(updatedDetails).length) {
|
||||||
|
this.$toast.info('No changes were made')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this.updateDetails(updatedDetails)
|
||||||
|
},
|
||||||
|
async updateDetails(updatedDetails) {
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
|
||||||
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
|
console.error('Failed update episode', error)
|
||||||
.then(() => {
|
this.isProcessing = false
|
||||||
this.isProcessing = false
|
this.$toast.error(error?.response?.data || 'Failed to update episode')
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isProcessing = false
|
||||||
|
if (updateResult) {
|
||||||
|
if (updateResult) {
|
||||||
this.$toast.success('Podcast episode updated')
|
this.$toast.success('Podcast episode updated')
|
||||||
this.$emit('close')
|
return true
|
||||||
})
|
} else {
|
||||||
.catch((error) => {
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
|
}
|
||||||
console.error('Failed update episode', error)
|
}
|
||||||
this.isProcessing = false
|
return false
|
||||||
this.$toast.error(errorMsg)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -14,6 +14,27 @@
|
|||||||
|
|
||||||
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentFeed.meta" class="mt-5">
|
||||||
|
<div class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ currentFeed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentFeed.meta.ownerName" class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ currentFeed.meta.ownerName }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentFeed.meta.ownerEmail" class="flex py-0.5">
|
||||||
|
<div class="w-48">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{ currentFeed.meta.ownerEmail }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderOpenRSSFeed }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderOpenRSSFeed }}</p>
|
||||||
@@ -22,6 +43,7 @@
|
|||||||
<ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
|
<ui-text-input-with-label v-model="newFeedSlug" :label="$strings.LabelRSSFeedSlug" />
|
||||||
<p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
|
<p class="text-xs text-gray-400 py-0.5 px-1">{{ $getString('MessageFeedURLWillBe', [demoFeedUrl]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<widgets-rss-feed-metadata-builder v-model="metadataDetails" />
|
||||||
|
|
||||||
<p v-if="isHttp" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsHttps }}</p>
|
<p v-if="isHttp" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsHttps }}</p>
|
||||||
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsPubDate }}</p>
|
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">{{ $strings.NoteRSSFeedPodcastAppsPubDate }}</p>
|
||||||
@@ -41,7 +63,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
newFeedSlug: null,
|
newFeedSlug: null,
|
||||||
currentFeed: null
|
currentFeed: null,
|
||||||
|
metadataDetails: {
|
||||||
|
preventIndexing: true,
|
||||||
|
ownerName: '',
|
||||||
|
ownerEmail: ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -107,7 +134,8 @@ export default {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
serverAddress: window.origin,
|
serverAddress: window.origin,
|
||||||
slug: this.newFeedSlug
|
slug: this.newFeedSlug,
|
||||||
|
metadataDetails: this.metadataDetails
|
||||||
}
|
}
|
||||||
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}`
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full">
|
<div class="h-full w-full">
|
||||||
<div class="h-full flex items-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden justify-center">
|
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
||||||
<span v-show="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="frame" class="w-full" style="height: 650px">
|
<div id="frame" class="w-full" style="height: 80%">
|
||||||
<div id="viewer" class="border border-gray-100 bg-white shadow-md"></div>
|
<div id="viewer"></div>
|
||||||
|
|
||||||
<div class="py-4 flex justify-center" style="height: 50px">
|
|
||||||
<p>{{ progress }}%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full flex items-center justify-center overflow-x-hidden">
|
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center justify-center overflow-x-hidden">
|
||||||
<span v-show="hasNext" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
<span v-if="hasNext" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,108 +17,252 @@
|
|||||||
<script>
|
<script>
|
||||||
import ePub from 'epubjs'
|
import ePub from 'epubjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} EpubReader
|
||||||
|
* @property {ePub.Book} book
|
||||||
|
* @property {ePub.Rendition} rendition
|
||||||
|
*/
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
windowWidth: 0,
|
||||||
|
/** @type {ePub.Book} */
|
||||||
book: null,
|
book: null,
|
||||||
rendition: null,
|
/** @type {ePub.Rendition} */
|
||||||
chapters: [],
|
rendition: null
|
||||||
title: '',
|
|
||||||
author: '',
|
|
||||||
progress: 0,
|
|
||||||
hasNext: true,
|
|
||||||
hasPrev: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
methods: {
|
/** @returns {string} */
|
||||||
changedChapter() {
|
libraryItemId() {
|
||||||
if (this.rendition) {
|
return this.libraryItem?.id
|
||||||
this.rendition.display(this.selectedChapter)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
hasPrev() {
|
||||||
|
return !this.rendition?.location?.atStart
|
||||||
|
},
|
||||||
|
hasNext() {
|
||||||
|
return !this.rendition?.location?.atEnd
|
||||||
|
},
|
||||||
|
/** @returns {Array<ePub.NavItem>} */
|
||||||
|
chapters() {
|
||||||
|
return this.book ? this.book.navigation.toc : []
|
||||||
|
},
|
||||||
|
userMediaProgress() {
|
||||||
|
if (!this.libraryItemId) return
|
||||||
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
localStorageLocationsKey() {
|
||||||
|
return `ebookLocations-${this.libraryItemId}`
|
||||||
|
},
|
||||||
|
readerWidth() {
|
||||||
|
if (this.windowWidth < 640) return this.windowWidth
|
||||||
|
return this.windowWidth - 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
prev() {
|
prev() {
|
||||||
if (this.rendition) {
|
return this.rendition?.prev()
|
||||||
this.rendition.prev()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
if (this.rendition) {
|
return this.rendition?.next()
|
||||||
this.rendition.next()
|
},
|
||||||
|
goToChapter(href) {
|
||||||
|
return this.rendition?.display(href)
|
||||||
|
},
|
||||||
|
keyUp(e) {
|
||||||
|
const rtl = this.book.package.metadata.direction === 'rtl'
|
||||||
|
if ((e.keyCode || e.which) == 37) {
|
||||||
|
return rtl ? this.next() : this.prev()
|
||||||
|
} else if ((e.keyCode || e.which) == 39) {
|
||||||
|
return rtl ? this.prev() : this.next()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyUp() {
|
/**
|
||||||
if ((e.keyCode || e.which) == 37) {
|
* @param {object} payload
|
||||||
this.prev()
|
* @param {string} payload.ebookLocation - CFI of the current location
|
||||||
} else if ((e.keyCode || e.which) == 39) {
|
* @param {string} payload.ebookProgress - eBook Progress Percentage
|
||||||
this.next()
|
*/
|
||||||
|
updateProgress(payload) {
|
||||||
|
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||||
|
console.error('EpubReader.updateProgress failed:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getAllEbookLocationData() {
|
||||||
|
const locations = []
|
||||||
|
let totalSize = 0 // Total in bytes
|
||||||
|
|
||||||
|
for (const key in localStorage) {
|
||||||
|
if (!localStorage.hasOwnProperty(key) || !key.startsWith('ebookLocations-')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ebookLocations = JSON.parse(localStorage[key])
|
||||||
|
if (!ebookLocations.locations) throw new Error('Invalid locations object')
|
||||||
|
|
||||||
|
ebookLocations.key = key
|
||||||
|
ebookLocations.size = (localStorage[key].length + key.length) * 2
|
||||||
|
locations.push(ebookLocations)
|
||||||
|
totalSize += ebookLocations.size
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse ebook locations', key, error)
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by oldest lastAccessed first
|
||||||
|
locations.sort((a, b) => a.lastAccessed - b.lastAccessed)
|
||||||
|
|
||||||
|
return {
|
||||||
|
locations,
|
||||||
|
totalSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** @param {string} locationString */
|
||||||
|
checkSaveLocations(locationString) {
|
||||||
|
const maxSizeInBytes = 3000000 // Allow epub locations to take up to 3MB of space
|
||||||
|
const newLocationsSize = JSON.stringify({ lastAccessed: Date.now(), locations: locationString }).length * 2
|
||||||
|
|
||||||
|
// Too large overall
|
||||||
|
if (newLocationsSize > maxSizeInBytes) {
|
||||||
|
console.error('Epub locations are too large to store. Size =', newLocationsSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ebookLocationsData = this.getAllEbookLocationData()
|
||||||
|
|
||||||
|
let availableSpace = maxSizeInBytes - ebookLocationsData.totalSize
|
||||||
|
|
||||||
|
// Remove epub locations until there is room for locations
|
||||||
|
while (availableSpace < newLocationsSize && ebookLocationsData.locations.length) {
|
||||||
|
const oldestLocation = ebookLocationsData.locations.shift()
|
||||||
|
console.log(`Removing cached locations for epub "${oldestLocation.key}" taking up ${oldestLocation.size} bytes`)
|
||||||
|
availableSpace += oldestLocation.size
|
||||||
|
localStorage.removeItem(oldestLocation.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cacheing epub locations with key "${this.localStorageLocationsKey}" taking up ${newLocationsSize} bytes`)
|
||||||
|
this.saveLocations(locationString)
|
||||||
|
},
|
||||||
|
/** @param {string} locationString */
|
||||||
|
saveLocations(locationString) {
|
||||||
|
localStorage.setItem(
|
||||||
|
this.localStorageLocationsKey,
|
||||||
|
JSON.stringify({
|
||||||
|
lastAccessed: Date.now(),
|
||||||
|
locations: locationString
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
loadLocations() {
|
||||||
|
const locationsObjString = localStorage.getItem(this.localStorageLocationsKey)
|
||||||
|
if (!locationsObjString) return null
|
||||||
|
|
||||||
|
const locationsObject = JSON.parse(locationsObjString)
|
||||||
|
|
||||||
|
// Remove invalid location objects
|
||||||
|
if (!locationsObject.locations) {
|
||||||
|
console.error('Invalid epub locations stored', this.localStorageLocationsKey)
|
||||||
|
localStorage.removeItem(this.localStorageLocationsKey)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update lastAccessed
|
||||||
|
this.saveLocations(locationsObject.locations)
|
||||||
|
|
||||||
|
return locationsObject.locations
|
||||||
|
},
|
||||||
|
/** @param {string} location - CFI of the new location */
|
||||||
|
relocated(location) {
|
||||||
|
if (this.userMediaProgress?.ebookLocation === location.start.cfi) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.end.percentage) {
|
||||||
|
this.updateProgress({
|
||||||
|
ebookLocation: location.start.cfi,
|
||||||
|
ebookProgress: location.end.percentage
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.updateProgress({
|
||||||
|
ebookLocation: location.start.cfi
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
initEpub() {
|
initEpub() {
|
||||||
// var book = ePub(this.url, {
|
/** @type {EpubReader} */
|
||||||
// requestHeaders: {
|
const reader = this
|
||||||
// Authorization: `Bearer ${this.userToken}`
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
var book = ePub(this.url)
|
|
||||||
this.book = book
|
|
||||||
|
|
||||||
this.rendition = book.renderTo('viewer', {
|
/** @type {ePub.Book} */
|
||||||
width: window.innerWidth - 200,
|
reader.book = new ePub(reader.url, {
|
||||||
height: 600,
|
width: this.readerWidth,
|
||||||
ignoreClass: 'annotator-hl',
|
height: window.innerHeight - 50
|
||||||
manager: 'continuous',
|
|
||||||
spread: 'always'
|
|
||||||
})
|
})
|
||||||
var displayed = this.rendition.display()
|
|
||||||
|
|
||||||
book.ready
|
/** @type {ePub.Rendition} */
|
||||||
.then(() => {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
console.log('Book ready')
|
width: this.readerWidth,
|
||||||
return book.locations.generate(1600)
|
height: window.innerHeight * 0.8
|
||||||
|
})
|
||||||
|
|
||||||
|
// load saved progress
|
||||||
|
reader.rendition.display(this.userMediaProgress?.ebookLocation || reader.book.locations.start)
|
||||||
|
|
||||||
|
// load style
|
||||||
|
reader.rendition.themes.default({ '*': { color: '#fff!important' } })
|
||||||
|
|
||||||
|
reader.book.ready.then(() => {
|
||||||
|
// set up event listeners
|
||||||
|
reader.rendition.on('relocated', reader.relocated)
|
||||||
|
reader.rendition.on('keydown', reader.keyUp)
|
||||||
|
|
||||||
|
let touchStart = 0
|
||||||
|
let touchEnd = 0
|
||||||
|
reader.rendition.on('touchstart', (event) => {
|
||||||
|
touchStart = event.changedTouches[0].screenX
|
||||||
})
|
})
|
||||||
.then((locations) => {
|
|
||||||
// console.log('Loaded locations', locations)
|
reader.rendition.on('touchend', (event) => {
|
||||||
// Wait for book to be rendered to get current page
|
touchEnd = event.changedTouches[0].screenX
|
||||||
displayed.then(() => {
|
const touchDistanceX = Math.abs(touchEnd - touchStart)
|
||||||
// Get the current CFI
|
if (touchStart < touchEnd && touchDistanceX > 120) {
|
||||||
var currentLocation = this.rendition.currentLocation()
|
this.next()
|
||||||
if (!currentLocation.start) {
|
}
|
||||||
console.error('No Start', currentLocation)
|
if (touchStart > touchEnd && touchDistanceX > 120) {
|
||||||
} else {
|
this.prev()
|
||||||
var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi)
|
}
|
||||||
// console.log('current page', currentPage)
|
})
|
||||||
}
|
|
||||||
|
// load ebook cfi locations
|
||||||
|
const savedLocations = this.loadLocations()
|
||||||
|
if (savedLocations) {
|
||||||
|
reader.book.locations.load(savedLocations)
|
||||||
|
} else {
|
||||||
|
reader.book.locations.generate().then(() => {
|
||||||
|
this.checkSaveLocations(reader.book.locations.save())
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
book.loaded.navigation.then((toc) => {
|
|
||||||
var _chapters = []
|
|
||||||
toc.forEach((chapter) => {
|
|
||||||
_chapters.push(chapter)
|
|
||||||
})
|
|
||||||
this.chapters = _chapters
|
|
||||||
})
|
|
||||||
book.loaded.metadata.then((metadata) => {
|
|
||||||
this.author = metadata.creator
|
|
||||||
this.title = metadata.title
|
|
||||||
})
|
|
||||||
|
|
||||||
this.rendition.on('keyup', this.keyUp)
|
|
||||||
|
|
||||||
this.rendition.on('relocated', (location) => {
|
|
||||||
var percent = book.locations.percentageFromCfi(location.start.cfi)
|
|
||||||
this.progress = Math.floor(percent * 100)
|
|
||||||
|
|
||||||
this.hasNext = !location.atEnd
|
|
||||||
this.hasPrev = !location.atStart
|
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.rendition?.resize(this.readerWidth, window.innerHeight * 0.8)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
|
this.book?.destroy()
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
this.initEpub()
|
this.initEpub()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full w-full">
|
|
||||||
<div id="viewer" class="border border-gray-100 bg-white text-black shadow-md h-screen overflow-y-auto p-4" v-html="pageHtml"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
url: String,
|
|
||||||
libraryItem: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
bookInfo: {},
|
|
||||||
page: 0,
|
|
||||||
numPages: 0,
|
|
||||||
pageHtml: '',
|
|
||||||
progress: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
libraryItemId() {
|
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
|
||||||
},
|
|
||||||
hasPrev() {
|
|
||||||
return this.page > 0
|
|
||||||
},
|
|
||||||
hasNext() {
|
|
||||||
return this.page < this.numPages - 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
prev() {
|
|
||||||
if (!this.hasPrev) return
|
|
||||||
this.page--
|
|
||||||
this.loadPage()
|
|
||||||
},
|
|
||||||
next() {
|
|
||||||
if (!this.hasNext) return
|
|
||||||
this.page++
|
|
||||||
this.loadPage()
|
|
||||||
},
|
|
||||||
keyUp() {
|
|
||||||
if ((e.keyCode || e.which) == 37) {
|
|
||||||
this.prev()
|
|
||||||
} else if ((e.keyCode || e.which) == 39) {
|
|
||||||
this.next()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadPage() {
|
|
||||||
this.$axios
|
|
||||||
.$get(`/api/ebooks/${this.libraryItemId}/page/${this.page}?dev=${this.$isDev ? 1 : 0}`)
|
|
||||||
.then((html) => {
|
|
||||||
this.pageHtml = html
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to load page', error)
|
|
||||||
this.$toast.error('Failed to load page')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadInfo() {
|
|
||||||
this.$axios
|
|
||||||
.$get(`/api/ebooks/${this.libraryItemId}/info?dev=${this.$isDev ? 1 : 0}`)
|
|
||||||
.then((bookInfo) => {
|
|
||||||
this.bookInfo = bookInfo
|
|
||||||
this.numPages = bookInfo.pages
|
|
||||||
this.page = 0
|
|
||||||
this.loadPage()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to load page', error)
|
|
||||||
this.$toast.error('Failed to load info')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
initEpub() {
|
|
||||||
if (!this.libraryItemId) return
|
|
||||||
this.loadInfo()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.initEpub()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,24 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 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 left-4 z-20">
|
||||||
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-4 left-4">
|
<div class="absolute top-4 left-1/2 transform -translate-x-1/2">
|
||||||
<h1 class="text-2xl mb-1">{{ abTitle }}</h1>
|
<h1 class="text-lg sm:text-xl md:text-2xl mb-1" style="line-height: 1.15; font-weight: 100">
|
||||||
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
<span style="font-weight: 600">{{ abTitle }}</span>
|
||||||
|
<span v-if="abAuthor" style="display: inline"> – </span>
|
||||||
|
<span v-if="abAuthor">{{ abAuthor }}</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute top-4 right-4 z-20">
|
||||||
|
<span v-if="hasSettings" class="material-icons cursor-pointer text-2xl" @click="openSettings">settings</span>
|
||||||
|
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
||||||
|
|
||||||
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
<!-- TOC side nav -->
|
||||||
|
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
||||||
|
<div v-if="hasToC" class="w-72 h-full max-h-full absolute top-0 left-0 bg-bg shadow-xl transition-transform z-30" :class="tocOpen ? 'translate-x-0' : '-translate-x-72'" @click.stop.prevent="toggleToC">
|
||||||
|
<div class="p-4 h-full overflow-hidden">
|
||||||
|
<p class="text-lg font-semibold mb-2">Table of Contents</p>
|
||||||
|
<div class="tocContent">
|
||||||
|
<ul>
|
||||||
|
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
||||||
|
<a :href="chapter.href" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
chapters: [],
|
||||||
|
tocOpen: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
show(newVal) {
|
show(newVal) {
|
||||||
@@ -37,13 +61,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
componentName() {
|
componentName() {
|
||||||
if (this.ebookType === 'epub' && this.$isDev) return 'readers-epub-reader2'
|
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||||
else if (this.ebookType === 'epub') return 'readers-epub-reader'
|
|
||||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||||
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
||||||
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
hasToC() {
|
||||||
|
return this.isEpub
|
||||||
|
},
|
||||||
|
hasSettings() {
|
||||||
|
return false
|
||||||
|
},
|
||||||
abTitle() {
|
abTitle() {
|
||||||
return this.mediaMetadata.title
|
return this.mediaMetadata.title
|
||||||
},
|
},
|
||||||
@@ -111,18 +140,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleToC() {
|
||||||
|
this.tocOpen = !this.tocOpen
|
||||||
|
this.chapters = this.$refs.readerComponent.chapters
|
||||||
|
},
|
||||||
|
openSettings() {},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
console.log('Reader hotkey', action)
|
console.log('Reader hotkey', action)
|
||||||
if (!this.$refs.readerComponent) return
|
if (!this.$refs.readerComponent) return
|
||||||
|
|
||||||
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
||||||
if (this.$refs.readerComponent.next) this.$refs.readerComponent.next()
|
this.next()
|
||||||
} else if (action === this.$hotkeys.EReader.PREV_PAGE) {
|
} else if (action === this.$hotkeys.EReader.PREV_PAGE) {
|
||||||
if (this.$refs.readerComponent.prev) this.$refs.readerComponent.prev()
|
this.prev()
|
||||||
} else if (action === this.$hotkeys.EReader.CLOSE) {
|
} else if (action === this.$hotkeys.EReader.CLOSE) {
|
||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
next() {
|
||||||
|
if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next()
|
||||||
|
},
|
||||||
|
prev() {
|
||||||
|
if (this.$refs.readerComponent?.prev) this.$refs.readerComponent.prev()
|
||||||
|
},
|
||||||
registerListeners() {
|
registerListeners() {
|
||||||
this.$eventBus.$on('reader-hotkey', this.hotkey)
|
this.$eventBus.$on('reader-hotkey', this.hotkey)
|
||||||
},
|
},
|
||||||
@@ -151,4 +191,8 @@ export default {
|
|||||||
.ebook-viewer {
|
.ebook-viewer {
|
||||||
height: calc(100% - 96px);
|
height: calc(100% - 96px);
|
||||||
}
|
}
|
||||||
|
.tocContent {
|
||||||
|
height: calc(100% - 36px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<p class="truncate text-xs sm:text-sm md:text-base">/{{ backup.path.replace(/\\/g, '/') }}</p>
|
<p class="truncate text-xs sm:text-sm md:text-base">/{{ backup.path.replace(/\\/g, '/') }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden sm:table-cell font-sans text-sm">{{ backup.datePretty }}</td>
|
<td class="hidden sm:table-cell font-sans text-sm">{{ $formatDatetime(backup.createdAt, dateFormat, timeFormat) }}</td>
|
||||||
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
|
<td class="hidden sm:table-cell font-mono md:text-sm text-xs">{{ $bytesPretty(backup.fileSize) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="w-full flex flex-row items-center justify-center">
|
<div class="w-full flex flex-row items-center justify-center">
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<p class="text-error text-lg font-semibold">{{ $strings.MessageImportantNotice }}</p>
|
<p class="text-error text-lg font-semibold">{{ $strings.MessageImportantNotice }}</p>
|
||||||
<p class="text-base py-1" v-html="$strings.MessageRestoreBackupWarning" />
|
<p class="text-base py-1" v-html="$strings.MessageRestoreBackupWarning" />
|
||||||
|
|
||||||
<p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ selectedBackup.datePretty }}?</p>
|
<p class="text-lg text-center my-8">{{ $strings.MessageRestoreBackupConfirm }} {{ $formatDatetime(selectedBackup.createdAt, dateFormat, timeFormat) }}?</p>
|
||||||
<div class="flex px-1 items-center">
|
<div class="flex px-1 items-center">
|
||||||
<ui-btn color="primary" @click="showConfirmApply = false">{{ $strings.ButtonNevermind }}</ui-btn>
|
<ui-btn color="primary" @click="showConfirmApply = false">{{ $strings.ButtonNevermind }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -71,6 +71,12 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -90,7 +96,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteBackupClick(backup) {
|
deleteBackupClick(backup) {
|
||||||
if (confirm(this.$getString('MessageConfirmDeleteBackup', [backup.datePretty]))) {
|
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/backups/${backup.id}`)
|
.$delete(`/api/backups/${backup.id}`)
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="user.lastSeen" direction="top" :text="$formatDate(user.lastSeen, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="user.lastSeen" direction="top" :text="$formatDatetime(user.lastSeen, dateFormat, timeFormat)">
|
||||||
{{ $dateDistanceFromNow(user.lastSeen) }}
|
{{ $dateDistanceFromNow(user.lastSeen) }}
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
<ui-tooltip direction="top" :text="$formatDate(user.createdAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip direction="top" :text="$formatDatetime(user.createdAt, dateFormat, timeFormat)">
|
||||||
{{ $formatDate(user.createdAt, 'MMM d, yyyy') }}
|
{{ $formatDate(user.createdAt, dateFormat) }}
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-0">
|
<td class="py-0">
|
||||||
@@ -74,6 +74,12 @@ export default {
|
|||||||
var usermap = {}
|
var usermap = {}
|
||||||
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
|
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
|
||||||
return usermap
|
return usermap
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full my-2">
|
||||||
|
<div class="w-full bg-primary px-4 md:px-6 py-2 flex items-center">
|
||||||
|
<p class="pr-2 md:pr-4">{{ $strings.HeaderDownloadQueue }}</p>
|
||||||
|
<div class="h-5 md:h-7 w-5 md:w-7 rounded-full bg-white bg-opacity-10 flex items-center justify-center">
|
||||||
|
<span class="text-sm font-mono">{{ queue.length }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<transition name="slide">
|
||||||
|
<div class="w-full">
|
||||||
|
<table class="text-sm tracksTable">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 min-w-48">{{ $strings.LabelPodcast }}</th>
|
||||||
|
<th class="text-left w-32 min-w-32">{{ $strings.LabelEpisode }}</th>
|
||||||
|
<th class="text-left px-4">{{ $strings.LabelEpisodeTitle }}</th>
|
||||||
|
<th class="text-left px-4 w-48">{{ $strings.LabelPubDate }}</th>
|
||||||
|
</tr>
|
||||||
|
<template v-for="downloadQueued in queue">
|
||||||
|
<tr :key="downloadQueued.id">
|
||||||
|
<td class="px-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<nuxt-link :to="`/item/${downloadQueued.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ downloadQueued.podcastTitle }}</nuxt-link>
|
||||||
|
<widgets-explicit-indicator :explicit="downloadQueued.podcastExplicit" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div v-if="downloadQueued.season">{{ downloadQueued.season }}x</div>
|
||||||
|
<div v-if="downloadQueued.episode">{{ downloadQueued.episode }}</div>
|
||||||
|
<widgets-podcast-type-indicator :type="downloadQueued.episodeType" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4">
|
||||||
|
{{ downloadQueued.episodeDisplayTitle }}
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p>{{ $dateDistanceFromNow(downloadQueued.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
queue: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
libraryItemId: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,16 +2,17 @@
|
|||||||
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
|
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="text-sm font-semibold">
|
<div class="flex items-center">
|
||||||
{{ title }}
|
<span class="text-sm font-semibold">{{ title }}</span>
|
||||||
</p>
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
||||||
|
|
||||||
<div class="flex justify-between pt-2 max-w-xl">
|
<div class="flex justify-between pt-2 max-w-xl">
|
||||||
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
|
||||||
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
||||||
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
|
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
@@ -128,6 +129,9 @@ export default {
|
|||||||
},
|
},
|
||||||
publishedAt() {
|
publishedAt() {
|
||||||
return this.episode.publishedAt
|
return this.episode.publishedAt
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -19,7 +19,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
||||||
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
|
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<template v-for="episode in episodesList">
|
||||||
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -46,7 +51,10 @@ export default {
|
|||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
episodesToRemove: [],
|
episodesToRemove: [],
|
||||||
processing: false,
|
processing: false,
|
||||||
quickMatchingEpisodes: false
|
quickMatchingEpisodes: false,
|
||||||
|
search: null,
|
||||||
|
searchTimeout: null,
|
||||||
|
searchText: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -137,15 +145,40 @@ export default {
|
|||||||
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
episodesList() {
|
||||||
|
return this.episodesSorted.filter((episode) => {
|
||||||
|
if (!this.searchText) return true
|
||||||
|
return (
|
||||||
|
(episode.title && episode.title.toLowerCase().includes(this.searchText)) ||
|
||||||
|
(episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
selectedIsFinished() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedEpisodes.find((episode) => {
|
return !this.selectedEpisodes.find((episode) => {
|
||||||
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||||
return !itemProgress || !itemProgress.isFinished
|
return !itemProgress || !itemProgress.isFinished
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
inputUpdate() {
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
if (!this.search || !this.search.trim()) {
|
||||||
|
this.searchText = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.searchText = this.search.toLowerCase().trim()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
contextMenuAction(action) {
|
contextMenuAction(action) {
|
||||||
if (action === 'quick-match-episodes') {
|
if (action === 'quick-match-episodes') {
|
||||||
if (this.quickMatchingEpisodes) return
|
if (this.quickMatchingEpisodes) return
|
||||||
@@ -195,7 +228,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -263,7 +296,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
})
|
})
|
||||||
@@ -281,6 +314,8 @@ export default {
|
|||||||
this.showPodcastRemoveModal = true
|
this.showPodcastRemoveModal = true
|
||||||
},
|
},
|
||||||
editEpisode(episode) {
|
editEpisode(episode) {
|
||||||
|
const episodeIds = this.episodesSorted.map((e) => e.id)
|
||||||
|
this.$store.commit('setEpisodeTableEpisodeIds', episodeIds)
|
||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setSelectedEpisode', episode)
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
|
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
|
||||||
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
|
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="label" class="select-none text-gray-100" :class="labelClassname">{{ label }}</div>
|
<div v-if="label" class="select-none" :class="[labelClassname, disabled ? 'text-gray-400' : 'text-gray-100']">{{ label }}</div>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
beforeDestroy() {
|
beforeDestroy() {}
|
||||||
console.log('Before destroy')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -51,8 +51,8 @@ export default {
|
|||||||
tooltip.style.zIndex = 100
|
tooltip.style.zIndex = 100
|
||||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||||
tooltip.innerHTML = this.text
|
tooltip.innerHTML = this.text
|
||||||
tooltip.addEventListener('mouseover', this.cancelHide);
|
tooltip.addEventListener('mouseover', this.cancelHide)
|
||||||
tooltip.addEventListener('mouseleave', this.hideTooltip);
|
tooltip.addEventListener('mouseleave', this.hideTooltip)
|
||||||
|
|
||||||
this.setTooltipPosition(tooltip)
|
this.setTooltipPosition(tooltip)
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export default {
|
|||||||
this.isShowing = false
|
this.isShowing = false
|
||||||
},
|
},
|
||||||
cancelHide() {
|
cancelHide() {
|
||||||
if (this.hideTimeout) clearTimeout(this.hideTimeout);
|
if (this.hideTimeout) clearTimeout(this.hideTimeout)
|
||||||
},
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
if (!this.isShowing) this.showTooltip()
|
if (!this.isShowing) this.showTooltip()
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<ui-tooltip v-if="alreadyInLibrary" :text="$strings.LabelAlreadyInYourLibrary" direction="top">
|
||||||
|
<span class="material-icons ml-1 text-success" style="font-size: 0.8rem">check_circle</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
alreadyInLibrary: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-full md:w-1/2 px-1">
|
<div class="w-full md:w-1/4 px-1">
|
||||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
|
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||||
@@ -61,6 +61,11 @@
|
|||||||
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +94,8 @@ export default {
|
|||||||
isbn: null,
|
isbn: null,
|
||||||
asin: null,
|
asin: null,
|
||||||
genres: [],
|
genres: [],
|
||||||
explicit: false
|
explicit: false,
|
||||||
|
abridged: false
|
||||||
},
|
},
|
||||||
newTags: []
|
newTags: []
|
||||||
}
|
}
|
||||||
@@ -271,6 +277,7 @@ export default {
|
|||||||
this.details.isbn = this.mediaMetadata.isbn || null
|
this.details.isbn = this.mediaMetadata.isbn || null
|
||||||
this.details.asin = this.mediaMetadata.asin || null
|
this.details.asin = this.mediaMetadata.asin || null
|
||||||
this.details.explicit = !!this.mediaMetadata.explicit
|
this.details.explicit = !!this.mediaMetadata.explicit
|
||||||
|
this.details.abridged = !!this.mediaMetadata.abridged
|
||||||
this.newTags = [...(this.media.tags || [])]
|
this.newTags = [...(this.media.tags || [])]
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@
|
|||||||
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
|
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div v-if="cronExpression && isValid" class="flex items-center justify-center text-yellow-400 mt-2">
|
||||||
|
<span class="material-icons-outlined mr-2 text-xl">event</span>
|
||||||
|
<p>{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -63,6 +67,14 @@ export default {
|
|||||||
isValid: true
|
isValid: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
minuteIsValid() {
|
minuteIsValid() {
|
||||||
return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59)
|
return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59)
|
||||||
@@ -70,6 +82,11 @@ export default {
|
|||||||
hourIsValid() {
|
hourIsValid() {
|
||||||
return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23)
|
return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23)
|
||||||
},
|
},
|
||||||
|
nextRun() {
|
||||||
|
if (!this.cronExpression) return ''
|
||||||
|
const parsed = this.$getNextScheduledDate(this.cronExpression)
|
||||||
|
return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || ''
|
||||||
|
},
|
||||||
description() {
|
description() {
|
||||||
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''
|
if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return ''
|
||||||
|
|
||||||
@@ -271,6 +288,11 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
|
this.selectedInterval = 'custom'
|
||||||
|
this.selectedHour = 0
|
||||||
|
this.selectedMinute = 0
|
||||||
|
this.selectedWeekdays = []
|
||||||
|
|
||||||
if (!this.value) return
|
if (!this.value) return
|
||||||
const pieces = this.value.split(' ')
|
const pieces = this.value.split(' ')
|
||||||
if (pieces.length !== 5) {
|
if (pieces.length !== 5) {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<ui-tooltip v-if="explicit" :text="$strings.LabelExplicit" direction="top">
|
||||||
|
<span class="material-icons ml-1" style="font-size: 0.8rem">explicit</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
explicit: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,15 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative">
|
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative" v-click-outside="clickOutsideObj">
|
||||||
<div class="flex h-full items-center justify-center">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full cursor-pointer" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<widgets-loading-spinner />
|
<div class="flex h-full items-center justify-center">
|
||||||
</div>
|
<ui-tooltip text="Tasks running" direction="bottom" class="flex items-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<transition name="menu">
|
||||||
|
<div class="sm:w-80 w-full relative">
|
||||||
|
<div v-show="showMenu" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalTaskRunningMenu">
|
||||||
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
|
<template v-if="tasksRunningOrFailed.length">
|
||||||
|
<p class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelTasks }}</p>
|
||||||
|
<template v-for="task in tasksRunningOrFailed">
|
||||||
|
<nuxt-link :key="task.id" v-if="actionLink(task)" :to="actionLink(task)">
|
||||||
|
<li class="text-gray-50 select-none relative hover:bg-black-400 py-1 cursor-pointer">
|
||||||
|
<cards-item-task-running-card :task="task" />
|
||||||
|
</li>
|
||||||
|
</nuxt-link>
|
||||||
|
<li v-else :key="task.id" class="text-gray-50 select-none relative hover:bg-black-400 py-1">
|
||||||
|
<cards-item-task-running-card :task="task" />
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<li v-else class="py-2 px-2">
|
||||||
|
<p>{{ $strings.MessageNoTasksRunning }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
clickOutsideObj: {
|
||||||
|
handler: this.clickedOutside,
|
||||||
|
events: ['mousedown'],
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
showMenu: false,
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tasks() {
|
tasks() {
|
||||||
@@ -17,9 +53,39 @@ export default {
|
|||||||
},
|
},
|
||||||
tasksRunning() {
|
tasksRunning() {
|
||||||
return this.tasks.some((t) => !t.isFinished)
|
return this.tasks.some((t) => !t.isFinished)
|
||||||
|
},
|
||||||
|
tasksRunningOrFailed() {
|
||||||
|
// return just the tasks that are running or failed in the last 1 minute
|
||||||
|
return this.tasks.filter((t) => !t.isFinished || (t.isFailed && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
},
|
||||||
|
clickedOutside() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
actionLink(task) {
|
||||||
|
switch (task.action) {
|
||||||
|
case 'download-podcast-episode':
|
||||||
|
return `/library/${task.data.libraryId}/podcast/download-queue`
|
||||||
|
case 'encode-m4b':
|
||||||
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=m4b`
|
||||||
|
case 'embed-metadata':
|
||||||
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=embed`
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.globalTaskRunningMenu {
|
||||||
|
max-height: 80vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex mt-2 -mx-1">
|
||||||
|
<div class="w-1/4 px-1">
|
||||||
|
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -65,7 +70,8 @@ export default {
|
|||||||
itunesId: null,
|
itunesId: null,
|
||||||
itunesArtistId: null,
|
itunesArtistId: null,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
language: null
|
language: null,
|
||||||
|
type: null
|
||||||
},
|
},
|
||||||
newTags: []
|
newTags: []
|
||||||
}
|
}
|
||||||
@@ -93,6 +99,9 @@ export default {
|
|||||||
},
|
},
|
||||||
filterData() {
|
filterData() {
|
||||||
return this.$store.state.libraries.filterData || {}
|
return this.$store.state.libraries.filterData || {}
|
||||||
|
},
|
||||||
|
podcastTypes() {
|
||||||
|
return this.$store.state.globals.podcastTypes || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -219,6 +228,7 @@ export default {
|
|||||||
this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
|
this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
|
||||||
this.details.language = this.mediaMetadata.language || ''
|
this.details.language = this.mediaMetadata.language || ''
|
||||||
this.details.explicit = !!this.mediaMetadata.explicit
|
this.details.explicit = !!this.mediaMetadata.explicit
|
||||||
|
this.details.type = this.mediaMetadata.type || 'episodic'
|
||||||
|
|
||||||
this.newTags = [...(this.media.tags || [])]
|
this.newTags = [...(this.media.tags || [])]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="type == 'bonus'">
|
||||||
|
<ui-tooltip text="Bonus" direction="top">
|
||||||
|
<span class="material-icons ml-1" style="font-size: 0.8rem">local_play</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-if="type == 'trailer'">
|
||||||
|
<ui-tooltip text="Trailer" direction="top">
|
||||||
|
<span class="material-icons ml-1" style="font-size: 0.8rem">local_movies</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'full'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full py-2">
|
||||||
|
<div class="flex -mb-px">
|
||||||
|
<div class="w-1/2 h-6 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false">
|
||||||
|
<p class="text-sm">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2 h-6 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true">
|
||||||
|
<p class="text-sm">{{ $strings.HeaderAdvanced }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 200px">
|
||||||
|
<template v-if="!showAdvancedView">
|
||||||
|
<div class="flex-grow pt-2 mb-2">
|
||||||
|
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex-grow pt-2 mb-2">
|
||||||
|
<ui-checkbox v-model="preventIndexing" :label="$strings.LabelPreventIndexing" checkbox-bg="primary" border-color="gray-600" label-class="pl-2" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full relative mb-1">
|
||||||
|
<ui-text-input-with-label v-model="ownerName" :label="$strings.LabelRSSFeedCustomOwnerName" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full relative mb-1">
|
||||||
|
<ui-text-input-with-label v-model="ownerEmail" :label="$strings.LabelRSSFeedCustomOwnerEmail" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
preventIndexing: true,
|
||||||
|
ownerName: '',
|
||||||
|
ownerEmail: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAdvancedView: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {
|
||||||
|
preventIndexing: {
|
||||||
|
get() {
|
||||||
|
return this.value.preventIndexing
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', {
|
||||||
|
...this.value,
|
||||||
|
preventIndexing: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ownerName: {
|
||||||
|
get() {
|
||||||
|
return this.value.ownerName
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', {
|
||||||
|
...this.value,
|
||||||
|
ownerName: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ownerEmail: {
|
||||||
|
get() {
|
||||||
|
return this.value.ownerEmail
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('input', {
|
||||||
|
...this.value,
|
||||||
|
ownerEmail: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -278,6 +278,13 @@ export default {
|
|||||||
console.log('Task finished', task)
|
console.log('Task finished', task)
|
||||||
this.$store.commit('tasks/addUpdateTask', task)
|
this.$store.commit('tasks/addUpdateTask', task)
|
||||||
},
|
},
|
||||||
|
metadataEmbedQueueUpdate(data) {
|
||||||
|
if (data.queued) {
|
||||||
|
this.$store.commit('tasks/addQueuedEmbedLId', data.libraryItemId)
|
||||||
|
} else {
|
||||||
|
this.$store.commit('tasks/removeQueuedEmbedLId', data.libraryItemId)
|
||||||
|
}
|
||||||
|
},
|
||||||
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)
|
||||||
@@ -418,6 +425,7 @@ export default {
|
|||||||
// Task Listeners
|
// Task Listeners
|
||||||
this.socket.on('task_started', this.taskStarted)
|
this.socket.on('task_started', this.taskStarted)
|
||||||
this.socket.on('task_finished', this.taskFinished)
|
this.socket.on('task_finished', this.taskFinished)
|
||||||
|
this.socket.on('metadata_embed_queue_update', this.metadataEmbedQueueUpdate)
|
||||||
|
|
||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
|
|
||||||
@@ -531,12 +539,18 @@ export default {
|
|||||||
},
|
},
|
||||||
loadTasks() {
|
loadTasks() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get('/api/tasks')
|
.$get('/api/tasks?include=queue')
|
||||||
.then((payload) => {
|
.then((payload) => {
|
||||||
console.log('Fetched tasks', payload)
|
console.log('Fetched tasks', payload)
|
||||||
if (payload.tasks) {
|
if (payload.tasks) {
|
||||||
this.$store.commit('tasks/setTasks', payload.tasks)
|
this.$store.commit('tasks/setTasks', payload.tasks)
|
||||||
}
|
}
|
||||||
|
if (payload.queuedTaskData?.embedMetadata?.length) {
|
||||||
|
this.$store.commit(
|
||||||
|
'tasks/setQueuedEmbedLIds',
|
||||||
|
payload.queuedTaskData.embedMetadata.map((td) => td.libraryItemId)
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to load tasks', error)
|
console.error('Failed to load tasks', error)
|
||||||
|
|||||||
Generated
+35
-2
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.15",
|
"version": "2.2.18",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.15",
|
"version": "2.2.18",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
@@ -5464,6 +5465,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/cron-parser": {
|
||||||
|
"version": "4.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz",
|
||||||
|
"integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==",
|
||||||
|
"dependencies": {
|
||||||
|
"luxon": "^3.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@@ -9134,6 +9146,14 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/make-dir": {
|
"node_modules/make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
@@ -21582,6 +21602,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||||
},
|
},
|
||||||
|
"cron-parser": {
|
||||||
|
"version": "4.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.1.tgz",
|
||||||
|
"integrity": "sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==",
|
||||||
|
"requires": {
|
||||||
|
"luxon": "^3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cross-spawn": {
|
"cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@@ -24397,6 +24425,11 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"luxon": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg=="
|
||||||
|
},
|
||||||
"make-dir": {
|
"make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.15",
|
"version": "2.2.18",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
|
|||||||
@@ -62,14 +62,20 @@
|
|||||||
<div class="w-full h-px bg-white bg-opacity-10 my-8" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-8" />
|
||||||
|
|
||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
<div v-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
<!-- queued alert -->
|
||||||
<ui-checkbox v-if="!isFinished" v-model="shouldBackupAudioFiles" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mb-4">
|
||||||
|
<p class="text-lg">Audiobook is queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p>
|
||||||
|
</widgets-alert>
|
||||||
|
<!-- metadata embed action buttons -->
|
||||||
|
<div v-else-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
||||||
|
<ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
<ui-btn v-if="!isTaskFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- m4b embed action buttons -->
|
||||||
<div v-else class="w-full flex items-center mb-4">
|
<div v-else class="w-full flex items-center mb-4">
|
||||||
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
|
||||||
<span class="material-icons text-xl">{{ showEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span>
|
<span class="material-icons text-xl">{{ showEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span>
|
||||||
@@ -83,6 +89,7 @@
|
|||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageM4BFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- advanced encoding options -->
|
||||||
<div v-if="isM4BTool" class="overflow-hidden">
|
<div v-if="isM4BTool" class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
<div v-if="showEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
|
||||||
@@ -191,6 +198,7 @@ export default {
|
|||||||
cnosole.error('No audio files')
|
cnosole.error('No audio files')
|
||||||
return redirect('/?error=no audio files')
|
return redirect('/?error=no audio files')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
libraryItem
|
libraryItem
|
||||||
}
|
}
|
||||||
@@ -200,7 +208,6 @@ export default {
|
|||||||
processing: false,
|
processing: false,
|
||||||
audiofilesEncoding: {},
|
audiofilesEncoding: {},
|
||||||
audiofilesFinished: {},
|
audiofilesFinished: {},
|
||||||
isFinished: false,
|
|
||||||
toneObject: null,
|
toneObject: null,
|
||||||
selectedTool: 'embed',
|
selectedTool: 'embed',
|
||||||
isCancelingEncode: false,
|
isCancelingEncode: false,
|
||||||
@@ -272,11 +279,28 @@ export default {
|
|||||||
isTaskFinished() {
|
isTaskFinished() {
|
||||||
return this.task && this.task.isFinished
|
return this.task && this.task.isFinished
|
||||||
},
|
},
|
||||||
|
tasks() {
|
||||||
|
return this.$store.getters['tasks/getTasksByLibraryItemId'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
embedTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'embed-metadata')
|
||||||
|
},
|
||||||
|
encodeTask() {
|
||||||
|
return this.tasks.find((t) => t.action === 'encode-m4b')
|
||||||
|
},
|
||||||
task() {
|
task() {
|
||||||
return this.$store.getters['tasks/getTaskByLibraryItemId'](this.libraryItemId)
|
if (this.isEmbedTool) return this.embedTask
|
||||||
|
else if (this.isM4BTool) return this.encodeTask
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
taskRunning() {
|
taskRunning() {
|
||||||
return this.task && !this.task.isFinished
|
return this.task && !this.task.isFinished
|
||||||
|
},
|
||||||
|
queuedEmbedLIds() {
|
||||||
|
return this.$store.state.tasks.queuedEmbedLIds || []
|
||||||
|
},
|
||||||
|
isMetadataEmbedQueued() {
|
||||||
|
return this.queuedEmbedLIds.some((lid) => lid === this.libraryItemId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -322,7 +346,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
this.processing = true
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
embedClick() {
|
embedClick() {
|
||||||
@@ -349,24 +373,6 @@ export default {
|
|||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
audioMetadataStarted(data) {
|
|
||||||
console.log('audio metadata started', data)
|
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
|
||||||
this.audiofilesFinished = {}
|
|
||||||
},
|
|
||||||
audioMetadataFinished(data) {
|
|
||||||
console.log('audio metadata finished', data)
|
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
|
||||||
this.processing = false
|
|
||||||
this.audiofilesEncoding = {}
|
|
||||||
|
|
||||||
if (data.failed) {
|
|
||||||
this.$toast.error(data.error)
|
|
||||||
} else {
|
|
||||||
this.isFinished = true
|
|
||||||
this.$toast.success('Audio file metadata updated')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
audiofileMetadataStarted(data) {
|
audiofileMetadataStarted(data) {
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
this.$set(this.audiofilesEncoding, data.ino, true)
|
this.$set(this.audiofilesEncoding, data.ino, true)
|
||||||
@@ -412,14 +418,10 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$root.socket.on('audio_metadata_started', this.audioMetadataStarted)
|
|
||||||
this.$root.socket.on('audio_metadata_finished', this.audioMetadataFinished)
|
|
||||||
this.$root.socket.on('audiofile_metadata_started', this.audiofileMetadataStarted)
|
this.$root.socket.on('audiofile_metadata_started', this.audiofileMetadataStarted)
|
||||||
this.$root.socket.on('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
this.$root.socket.on('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$root.socket.off('audio_metadata_started', this.audioMetadataStarted)
|
|
||||||
this.$root.socket.off('audio_metadata_finished', this.audioMetadataFinished)
|
|
||||||
this.$root.socket.off('audiofile_metadata_started', this.audiofileMetadataStarted)
|
this.$root.socket.off('audiofile_metadata_started', this.audiofileMetadataStarted)
|
||||||
this.$root.socket.off('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
this.$root.socket.off('audiofile_metadata_finished', this.audiofileMetadataFinished)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="enableBackups" class="mb-6">
|
<div v-if="enableBackups" class="mb-6">
|
||||||
<div class="flex items-center pl-6">
|
<div class="flex items-center pl-6 mb-2">
|
||||||
<span class="material-icons-outlined text-2xl text-black-50">schedule</span>
|
<span class="material-icons-outlined text-2xl text-black-50 mr-2">schedule</span>
|
||||||
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
<div class="w-48"><span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span></div>
|
||||||
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
<div class="text-gray-100">{{ scheduleDescription }}</div>
|
||||||
|
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer ml-2" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="nextBackupDate" class="flex items-center pl-6 py-0.5 px-2">
|
||||||
|
<span class="material-icons-outlined text-2xl text-black-50 mr-2">event</span>
|
||||||
|
<div class="w-48"><span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span></div>
|
||||||
|
<div class="text-gray-100">{{ nextBackupDate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,10 +71,21 @@ export default {
|
|||||||
serverSettings() {
|
serverSettings() {
|
||||||
return this.$store.state.serverSettings
|
return this.$store.state.serverSettings
|
||||||
},
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.serverSettings.timeFormat
|
||||||
|
},
|
||||||
scheduleDescription() {
|
scheduleDescription() {
|
||||||
if (!this.cronExpression) return ''
|
if (!this.cronExpression) return ''
|
||||||
const parsed = this.$parseCronExpression(this.cronExpression)
|
const parsed = this.$parseCronExpression(this.cronExpression)
|
||||||
return parsed ? parsed.description : 'Custom cron expression ' + this.cronExpression
|
return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}`
|
||||||
|
},
|
||||||
|
nextBackupDate() {
|
||||||
|
if (!this.cronExpression) return ''
|
||||||
|
const parsed = this.$getNextScheduledDate(this.cronExpression)
|
||||||
|
return this.$formatJsDatetime(parsed, this.dateFormat, this.timeFormat) || ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -90,15 +108,15 @@ export default {
|
|||||||
updateServerSettings(payload) {
|
updateServerSettings(payload) {
|
||||||
this.updatingServerSettings = true
|
this.updatingServerSettings = true
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('updateServerSettings', payload)
|
.dispatch('updateServerSettings', payload)
|
||||||
.then((success) => {
|
.then((success) => {
|
||||||
console.log('Updated Server Settings', success)
|
console.log('Updated Server Settings', success)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update server settings', error)
|
console.error('Failed to update server settings', error)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
|
|||||||
@@ -68,8 +68,14 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="flex-grow py-2">
|
||||||
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||||
|
<p class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-grow py-2">
|
||||||
|
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-52" @input="(val) => updateSettingsKey('timeFormat', val)" />
|
||||||
|
<p class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
@@ -293,6 +299,17 @@ export default {
|
|||||||
},
|
},
|
||||||
dateFormats() {
|
dateFormats() {
|
||||||
return this.$store.state.globals.dateFormats
|
return this.$store.state.globals.dateFormats
|
||||||
|
},
|
||||||
|
timeFormats() {
|
||||||
|
return this.$store.state.globals.timeFormats
|
||||||
|
},
|
||||||
|
dateExample() {
|
||||||
|
const date = new Date(2014, 2, 25)
|
||||||
|
return this.$formatJsDate(date, this.newServerSettings.dateFormat)
|
||||||
|
},
|
||||||
|
timeExample() {
|
||||||
|
const date = new Date(2014, 2, 25, 17, 30, 0)
|
||||||
|
return this.$formatJsTime(date, this.newServerSettings.timeFormat)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -60,6 +60,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLargestItems }}</h1>
|
||||||
|
<p v-if="!top10LargestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||||
|
<template v-for="(ab, index) in top10LargestItems">
|
||||||
|
<div :key="index" class="w-full py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
||||||
|
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.size) / largestItemSize) + '%' }" />
|
||||||
|
</div>
|
||||||
|
<div class="w-4 ml-3">
|
||||||
|
<p class="text-sm font-bold">{{ $bytesPretty(ab.size) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,6 +124,13 @@ export default {
|
|||||||
if (!this.top10LongestItems.length) return 0
|
if (!this.top10LongestItems.length) return 0
|
||||||
return this.top10LongestItems[0].duration
|
return this.top10LongestItems[0].duration
|
||||||
},
|
},
|
||||||
|
top10LargestItems() {
|
||||||
|
return this.libraryStats ? this.libraryStats.largestItems || [] : []
|
||||||
|
},
|
||||||
|
largestItemSize() {
|
||||||
|
if (!this.top10LargestItems.length) return 0
|
||||||
|
return this.top10LargestItems[0].size
|
||||||
|
},
|
||||||
authorsWithCount() {
|
authorsWithCount() {
|
||||||
return this.libraryStats ? this.libraryStats.authorsWithCount : []
|
return this.libraryStats ? this.libraryStats.authorsWithCount : []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
@@ -105,6 +105,12 @@ export default {
|
|||||||
if (!this.userFilter) return null
|
if (!this.userFilter) return null
|
||||||
var user = this.users.find((u) => u.id === this.userFilter)
|
var user = this.users.find((u) => u.id === this.userFilter)
|
||||||
return user ? user.username : null
|
return user ? user.username : null
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -149,7 +155,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: libraryItem.media.metadata.title,
|
subtitle: libraryItem.media.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: libraryItem.media.coverPath || null
|
coverPath: libraryItem.media.coverPath || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,12 +79,12 @@
|
|||||||
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="item.startedAt" direction="top" :text="$formatDate(item.startedAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="item.startedAt" direction="top" :text="$formatDatetime(item.startedAt, dateFormat, timeFormat)">
|
||||||
<p class="text-sm">{{ $dateDistanceFromNow(item.startedAt) }}</p>
|
<p class="text-sm">{{ $dateDistanceFromNow(item.startedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="item.lastUpdate" direction="top" :text="$formatDate(item.lastUpdate, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="item.lastUpdate" direction="top" :text="$formatDatetime(item.lastUpdate, dateFormat, timeFormat)">
|
||||||
<p class="text-sm">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
<p class="text-sm">{{ $dateDistanceFromNow(item.lastUpdate) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
@@ -149,6 +149,12 @@ export default {
|
|||||||
latestSession() {
|
latestSession() {
|
||||||
if (!this.listeningSessions.sessions || !this.listeningSessions.sessions.length) return null
|
if (!this.listeningSessions.sessions || !this.listeningSessions.sessions.length) return null
|
||||||
return this.listeningSessions.sessions[0]
|
return this.listeningSessions.sessions[0]
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
|
||||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
@@ -96,6 +96,12 @@ export default {
|
|||||||
},
|
},
|
||||||
userOnline() {
|
userOnline() {
|
||||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
|
timeFormat() {
|
||||||
|
return this.$store.state.serverSettings.timeFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -140,7 +146,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: libraryItem.media.metadata.title,
|
subtitle: libraryItem.media.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: libraryItem.media.coverPath || null
|
coverPath: libraryItem.media.coverPath || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h1 class="text-2xl md:text-3xl font-semibold">
|
<h1 class="text-2xl md:text-3xl font-semibold">
|
||||||
{{ title }}
|
<div class="flex items-center">
|
||||||
|
{{ title }}
|
||||||
|
<widgets-explicit-indicator :explicit="isExplicit" />
|
||||||
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||||
@@ -121,6 +124,14 @@
|
|||||||
{{ sizePretty }}
|
{{ sizePretty }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isBook" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelAbridged }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ isAbridged ? 'Yes' : 'No' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +164,7 @@
|
|||||||
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
||||||
<p v-if="progressPercent < 1" class="leading-6">{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
|
<p v-if="progressPercent < 1" class="leading-6">{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
<p v-else class="text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
|
<p v-else class="text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
|
||||||
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
|
<p v-if="progressPercent < 1 && !useEBookProgress" class="text-gray-200 text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
|
||||||
<p class="text-gray-400 text-xs pt-1">{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
|
<p class="text-gray-400 text-xs pt-1">{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
|
||||||
|
|
||||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||||
@@ -315,6 +326,12 @@ export default {
|
|||||||
isInvalid() {
|
isInvalid() {
|
||||||
return this.libraryItem.isInvalid
|
return this.libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
|
isExplicit() {
|
||||||
|
return !!this.mediaMetadata.explicit
|
||||||
|
},
|
||||||
|
isAbridged() {
|
||||||
|
return !!this.mediaMetadata.abridged
|
||||||
|
},
|
||||||
invalidAudioFiles() {
|
invalidAudioFiles() {
|
||||||
if (!this.isBook) return []
|
if (!this.isBook) return []
|
||||||
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
||||||
@@ -465,7 +482,12 @@ export default {
|
|||||||
const duration = this.userMediaProgress.duration || this.duration
|
const duration = this.userMediaProgress.duration || this.duration
|
||||||
return duration - this.userMediaProgress.currentTime
|
return duration - this.userMediaProgress.currentTime
|
||||||
},
|
},
|
||||||
|
useEBookProgress() {
|
||||||
|
if (!this.userMediaProgress || this.userMediaProgress.progress) return false
|
||||||
|
return this.userMediaProgress.ebookProgress > 0
|
||||||
|
},
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
|
if (this.useEBookProgress) return Math.max(Math.min(1, this.userMediaProgress.ebookProgress), 0)
|
||||||
return this.userMediaProgress ? Math.max(Math.min(1, this.userMediaProgress.progress), 0) : 0
|
return this.userMediaProgress ? Math.max(Math.min(1, this.userMediaProgress.progress), 0) : 0
|
||||||
},
|
},
|
||||||
userProgressStartedAt() {
|
userProgressStartedAt() {
|
||||||
@@ -632,7 +654,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.title,
|
subtitle: this.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.libraryItem.media.coverPath || null
|
coverPath: this.libraryItem.media.coverPath || null
|
||||||
})
|
})
|
||||||
@@ -753,9 +775,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.libraryItem.episodesDownloading) {
|
this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || []
|
||||||
this.episodeDownloadsQueued = this.libraryItem.episodesDownloading || []
|
this.episodesDownloading = this.libraryItem.episodesDownloading || []
|
||||||
}
|
|
||||||
|
|
||||||
// use this items library id as the current
|
// use this items library id as the current
|
||||||
if (this.libraryId) {
|
if (this.libraryId) {
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
|
<app-book-shelf-toolbar page="podcast-search" />
|
||||||
|
|
||||||
|
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||||
|
<div class="w-full max-w-5xl mx-auto py-4">
|
||||||
|
<p class="text-xl mb-2 font-semibold px-4 md:px-0">{{ $strings.HeaderCurrentDownloads }}</p>
|
||||||
|
<p v-if="!episodesDownloading.length" class="text-lg py-4">{{ $strings.MessageNoDownloadsInProgress }}</p>
|
||||||
|
<template v-for="episode in episodesDownloading">
|
||||||
|
<div :key="episode.id" class="flex py-5 relative">
|
||||||
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
|
||||||
|
<div class="flex-grow pl-4 max-w-2xl">
|
||||||
|
<!-- mobile -->
|
||||||
|
<div class="flex md:hidden mb-2">
|
||||||
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
||||||
|
<div class="flex-grow px-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcastTitle }}</nuxt-link>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcastExplicit" />
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- desktop -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcastTitle }}</nuxt-link>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcastExplicit" />
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center font-semibold text-gray-200">
|
||||||
|
<div v-if="episode.season || episode.episode">#</div>
|
||||||
|
<div v-if="episode.season">{{ episode.season }}x</div>
|
||||||
|
<div v-if="episode.episode">{{ episode.episode }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<span class="font-semibold text-sm md:text-base">{{ episode.episodeDisplayTitle }}</span>
|
||||||
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<tables-podcast-download-queue-table v-if="episodeDownloadsQueued.length" :queue="episodeDownloadsQueued"></tables-podcast-download-queue-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ params, redirect }) {
|
||||||
|
if (!params.library) {
|
||||||
|
console.error('No library...', params.library)
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
libraryId: params.library
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
episodesDownloading: [],
|
||||||
|
episodeDownloadsQueued: [],
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
episodeDownloadQueued(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryId === this.libraryId) {
|
||||||
|
this.episodeDownloadsQueued.push(episodeDownload)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadStarted(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryId === this.libraryId) {
|
||||||
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
this.episodesDownloading.push(episodeDownload)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadFinished(episodeDownload) {
|
||||||
|
if (episodeDownload.libraryId === this.libraryId) {
|
||||||
|
this.episodeDownloadsQueued = this.episodeDownloadsQueued.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeDownloadQueueUpdated(downloadQueueDetails) {
|
||||||
|
this.episodeDownloadsQueued = downloadQueueDetails.queue.filter((q) => q.libraryId == this.libraryId)
|
||||||
|
},
|
||||||
|
async loadInitialDownloadQueue() {
|
||||||
|
this.processing = true
|
||||||
|
const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
|
||||||
|
console.error('Failed to get download queue', error)
|
||||||
|
this.$toast.error('Failed to get download queue')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
|
this.episodeDownloadsQueued = queuePayload?.queue || []
|
||||||
|
|
||||||
|
if (queuePayload?.currentDownload) {
|
||||||
|
this.episodesDownloading.push(queuePayload.currentDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize listeners after load to prevent event race conditions
|
||||||
|
this.initListeners()
|
||||||
|
},
|
||||||
|
initListeners() {
|
||||||
|
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
|
||||||
|
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
|
||||||
|
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||||
|
this.$root.socket.on('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.libraryId) {
|
||||||
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadInitialDownloadQueue()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
|
||||||
|
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
|
||||||
|
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
|
||||||
|
this.$root.socket.off('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -14,19 +14,36 @@
|
|||||||
<div class="flex md:hidden mb-2">
|
<div class="flex md:hidden mb-2">
|
||||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
<div class="flex items-center">
|
||||||
|
<div class="flex" @click.stop>
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcast.metadata.explicit" />
|
||||||
|
</div>
|
||||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- desktop -->
|
<!-- desktop -->
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
<div class="flex items-center">
|
||||||
|
<div class="flex" @click.stop>
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<widgets-explicit-indicator :explicit="episode.podcast.metadata.explicit" />
|
||||||
|
</div>
|
||||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="font-semibold mb-2 text-sm md:text-base">{{ episode.title }}</p>
|
<div class="flex items-center font-semibold text-gray-200">
|
||||||
|
<div v-if="episode.season || episode.episode">#</div>
|
||||||
|
<div v-if="episode.season">{{ episode.season }}x</div>
|
||||||
|
<div v-if="episode.episode">{{ episode.episode }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class="font-semibold text-sm md:text-base">{{ episode.title }}</div>
|
||||||
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
||||||
|
|
||||||
@@ -113,6 +130,9 @@ export default {
|
|||||||
if (i.episodeId) episodeIds[i.episodeId] = true
|
if (i.episodeId) episodeIds[i.episodeId] = true
|
||||||
})
|
})
|
||||||
return episodeIds
|
return episodeIds
|
||||||
|
},
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -156,7 +176,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: episode.podcast.metadata.title,
|
subtitle: episode.podcast.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.duration || null,
|
duration: episode.duration || null,
|
||||||
coverPath: episode.podcast.coverPath || null
|
coverPath: episode.podcast.coverPath || null
|
||||||
})
|
})
|
||||||
@@ -194,7 +214,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: episode.podcast.metadata.title,
|
subtitle: episode.podcast.metadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, 'MMM do, yyyy')}` : 'Unknown publish date',
|
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
||||||
duration: episode.duration || null,
|
duration: episode.duration || null,
|
||||||
coverPath: episode.podcast.coverPath || null
|
coverPath: episode.podcast.coverPath || null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,12 @@
|
|||||||
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||||
<div class="w-full max-w-4xl mx-auto flex">
|
<div class="w-full max-w-4xl mx-auto flex">
|
||||||
<form @submit.prevent="submit" class="flex flex-grow">
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
<ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" />
|
<ui-text-input v-model="searchInput" type="search" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
<ui-btn type="submit" :disabled="processing" class="hidden md:block">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit" :disabled="processing" class="hidden md:block">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
<ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="ml-2" @change="opmlFileUpload">{{ $strings.ButtonUploadOPMLFile }}</ui-file-input>
|
<ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="ml-2" @change="opmlFileUpload">{{ $strings.ButtonUploadOPMLFile }}</ui-file-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full max-w-3xl mx-auto py-4">
|
<div class="w-full max-w-3xl mx-auto py-4">
|
||||||
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">{{ $strings.MessageNoPodcastsFound }}</p>
|
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">{{ $strings.MessageNoPodcastsFound }}</p>
|
||||||
<template v-for="podcast in results">
|
<template v-for="podcast in results">
|
||||||
@@ -20,7 +19,11 @@
|
|||||||
<img v-if="podcast.cover" :src="podcast.cover" class="h-full w-full" />
|
<img v-if="podcast.cover" :src="podcast.cover" class="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow pl-4 max-w-2xl">
|
<div class="flex-grow pl-4 max-w-2xl">
|
||||||
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
|
<div class="flex items-center">
|
||||||
|
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
|
||||||
|
<widgets-explicit-indicator :explicit="podcast.explicit" />
|
||||||
|
<widgets-already-in-library-indicator :already-in-library="podcast.alreadyInLibrary"/>
|
||||||
|
</div>
|
||||||
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
|
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} {{ $strings.HeaderEpisodes }}</p>
|
<p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} {{ $strings.HeaderEpisodes }}</p>
|
||||||
@@ -68,10 +71,14 @@ export default {
|
|||||||
selectedPodcast: null,
|
selectedPodcast: null,
|
||||||
selectedPodcastFeed: null,
|
selectedPodcastFeed: null,
|
||||||
showOPMLFeedsModal: false,
|
showOPMLFeedsModal: false,
|
||||||
opmlFeeds: []
|
opmlFeeds: [],
|
||||||
|
existentPodcasts: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
@@ -144,18 +151,29 @@ export default {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Got results', results)
|
console.log('Got results', results)
|
||||||
|
for (let result of results) {
|
||||||
|
let podcast = this.existentPodcasts.find((p) => p.itunesId === result.id || p.title === result.title.toLowerCase())
|
||||||
|
if (podcast) {
|
||||||
|
result.alreadyInLibrary = true
|
||||||
|
result.existentId = podcast.id
|
||||||
|
}
|
||||||
|
}
|
||||||
this.results = results
|
this.results = results
|
||||||
this.termSearched = term
|
this.termSearched = term
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
async selectPodcast(podcast) {
|
async selectPodcast(podcast) {
|
||||||
console.log('Selected podcast', podcast)
|
console.log('Selected podcast', podcast)
|
||||||
|
if(podcast.existentId){
|
||||||
|
this.$router.push(`/item/${podcast.existentId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!podcast.feedUrl) {
|
if (!podcast.feedUrl) {
|
||||||
this.$toast.error('Invalid podcast - no feed')
|
this.$toast.error('Invalid podcast - no feed')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => {
|
var payload = await this.$axios.$post(`/api/podcasts/feed`, {rssFeed: podcast.feedUrl}).catch((error) => {
|
||||||
console.error('Failed to get feed', error)
|
console.error('Failed to get feed', error)
|
||||||
this.$toast.error('Failed to get podcast feed')
|
this.$toast.error('Failed to get podcast feed')
|
||||||
return null
|
return null
|
||||||
@@ -167,8 +185,26 @@ export default {
|
|||||||
this.selectedPodcast = podcast
|
this.selectedPodcast = podcast
|
||||||
this.showNewPodcastModal = true
|
this.showNewPodcastModal = true
|
||||||
console.log('Got podcast feed', payload.podcast)
|
console.log('Got podcast feed', payload.podcast)
|
||||||
|
},
|
||||||
|
async fetchExistentPodcastsInYourLibrary() {
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const podcasts = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/items?page=0&minified=1`).catch((error) => {
|
||||||
|
console.error('Failed to fetch podcasts', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
this.existentPodcasts = podcasts.results.map((p) => {
|
||||||
|
return {
|
||||||
|
title: p.media.metadata.title.toLowerCase(),
|
||||||
|
itunesId: p.media.metadata.itunesId,
|
||||||
|
id: p.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
this.fetchExistentPodcastsInYourLibrary()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -127,6 +127,7 @@ export default class LocalAudioPlayer extends EventEmitter {
|
|||||||
|
|
||||||
setHlsStream() {
|
setHlsStream() {
|
||||||
this.trackStartTime = 0
|
this.trackStartTime = 0
|
||||||
|
this.currentTrackIndex = 0
|
||||||
|
|
||||||
// iOS does not support Media Elements but allows for HLS in the native audio player
|
// iOS does not support Media Elements but allows for HLS in the native audio player
|
||||||
if (!Hls.isSupported()) {
|
if (!Hls.isSupported()) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const SupportedFileTypes = {
|
const SupportedFileTypes = {
|
||||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
image: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma'],
|
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb'],
|
||||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
info: ['nfo'],
|
info: ['nfo'],
|
||||||
text: ['txt'],
|
text: ['txt'],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const defaultCode = 'en-us'
|
|||||||
const languageCodeMap = {
|
const languageCodeMap = {
|
||||||
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||||
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
||||||
// 'es': { label: 'Español', dateFnsLocale: 'es' },
|
'es': { label: 'Español', dateFnsLocale: 'es' },
|
||||||
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||||
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||||
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||||
|
|||||||
@@ -23,6 +23,22 @@ Vue.prototype.$formatJsDate = (jsdate, fnsFormat = 'MM/dd/yyyy HH:mm') => {
|
|||||||
if (!jsdate || !isDate(jsdate)) return ''
|
if (!jsdate || !isDate(jsdate)) return ''
|
||||||
return format(jsdate, fnsFormat)
|
return format(jsdate, fnsFormat)
|
||||||
}
|
}
|
||||||
|
Vue.prototype.$formatTime = (unixms, fnsFormat = 'HH:mm') => {
|
||||||
|
if (!unixms) return ''
|
||||||
|
return format(unixms, fnsFormat)
|
||||||
|
}
|
||||||
|
Vue.prototype.$formatJsTime = (jsdate, fnsFormat = 'HH:mm') => {
|
||||||
|
if (!jsdate || !isDate(jsdate)) return ''
|
||||||
|
return format(jsdate, fnsFormat)
|
||||||
|
}
|
||||||
|
Vue.prototype.$formatDatetime = (unixms, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
||||||
|
if (!unixms) return ''
|
||||||
|
return format(unixms, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||||
|
}
|
||||||
|
Vue.prototype.$formatJsDatetime = (jsdate, fnsDateFormart = 'MM/dd/yyyy', fnsTimeFormat = 'HH:mm') => {
|
||||||
|
if (!jsdate || !isDate(jsdate)) return ''
|
||||||
|
return format(jsdate, `${fnsDateFormart} ${fnsTimeFormat}`)
|
||||||
|
}
|
||||||
Vue.prototype.$addDaysToToday = (daysToAdd) => {
|
Vue.prototype.$addDaysToToday = (daysToAdd) => {
|
||||||
var date = addDays(new Date(), daysToAdd)
|
var date = addDays(new Date(), daysToAdd)
|
||||||
if (!date || !isDate(date)) return null
|
if (!date || !isDate(date)) return null
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import cronParser from 'cron-parser'
|
||||||
|
|
||||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||||
if (isNaN(bytes) || bytes == 0) {
|
if (isNaN(bytes) || bytes == 0) {
|
||||||
@@ -136,6 +137,11 @@ Vue.prototype.$parseCronExpression = (expression) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$getNextScheduledDate = (expression) => {
|
||||||
|
const interval = cronParser.parseExpression(expression);
|
||||||
|
return interval.next().toDate()
|
||||||
|
}
|
||||||
|
|
||||||
export function supplant(str, subs) {
|
export function supplant(str, subs) {
|
||||||
// source: http://crockford.com/javascript/remedial.html
|
// source: http://crockford.com/javascript/remedial.html
|
||||||
return str.replace(/{([^{}]*)}/g,
|
return str.replace(/{([^{}]*)}/g,
|
||||||
|
|||||||
+42
-3
@@ -32,11 +32,50 @@ export const state = () => ({
|
|||||||
text: 'DD/MM/YYYY',
|
text: 'DD/MM/YYYY',
|
||||||
value: 'dd/MM/yyyy'
|
value: 'dd/MM/yyyy'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'DD.MM.YYYY',
|
||||||
|
value: 'dd.MM.yyyy'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'YYYY-MM-DD',
|
text: 'YYYY-MM-DD',
|
||||||
value: 'yyyy-MM-dd'
|
value: 'yyyy-MM-dd'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'MMM do, yyyy',
|
||||||
|
value: 'MMM do, yyyy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'MMMM do, yyyy',
|
||||||
|
value: 'MMMM do, yyyy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'dd MMM yyyy',
|
||||||
|
value: 'dd MMM yyyy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'dd MMMM yyyy',
|
||||||
|
value: 'dd MMMM yyyy'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
timeFormats: [
|
||||||
|
{
|
||||||
|
text: 'h:mma (am/pm)',
|
||||||
|
value: 'h:mma'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'HH:mm (24-hour)',
|
||||||
|
value: 'HH:mm'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
podcastTypes: [
|
||||||
|
{ text: 'Episodic', value: 'episodic' },
|
||||||
|
{ text: 'Serial', value: 'serial' }
|
||||||
|
],
|
||||||
|
episodeTypes: [
|
||||||
|
{ text: 'Full', value: 'full' },
|
||||||
|
{ text: 'Trailer', value: 'trailer' },
|
||||||
|
{ text: 'Bonus', value: 'bonus' }
|
||||||
|
],
|
||||||
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
|
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,14 +99,14 @@ export const getters = {
|
|||||||
|
|
||||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
},
|
},
|
||||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null) => {
|
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null, raw = false) => {
|
||||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||||
if (!libraryItemId) return placeholder
|
if (!libraryItemId) return placeholder
|
||||||
var userToken = rootGetters['user/getToken']
|
var userToken = rootGetters['user/getToken']
|
||||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}`
|
||||||
}
|
}
|
||||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}`
|
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}`
|
||||||
},
|
},
|
||||||
getIsBatchSelectingMediaItems: (state) => {
|
getIsBatchSelectingMediaItems: (state) => {
|
||||||
return state.selectedMediaItems.length
|
return state.selectedMediaItems.length
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const state = () => ({
|
|||||||
playerQueueAutoPlay: true,
|
playerQueueAutoPlay: true,
|
||||||
playerIsFullscreen: false,
|
playerIsFullscreen: false,
|
||||||
editModalTab: 'details',
|
editModalTab: 'details',
|
||||||
|
editPodcastModalTab: 'details',
|
||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showEReader: false,
|
showEReader: false,
|
||||||
selectedLibraryItem: null,
|
selectedLibraryItem: null,
|
||||||
@@ -21,6 +22,7 @@ export const state = () => ({
|
|||||||
previousPath: '/',
|
previousPath: '/',
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
bookshelfBookIds: [],
|
bookshelfBookIds: [],
|
||||||
|
episodeTableEpisodeIds: [],
|
||||||
openModal: null,
|
openModal: null,
|
||||||
innerModalOpen: false,
|
innerModalOpen: false,
|
||||||
lastBookshelfScrollData: {},
|
lastBookshelfScrollData: {},
|
||||||
@@ -135,6 +137,9 @@ export const mutations = {
|
|||||||
setBookshelfBookIds(state, val) {
|
setBookshelfBookIds(state, val) {
|
||||||
state.bookshelfBookIds = val || []
|
state.bookshelfBookIds = val || []
|
||||||
},
|
},
|
||||||
|
setEpisodeTableEpisodeIds(state, val) {
|
||||||
|
state.episodeTableEpisodeIds = val || []
|
||||||
|
},
|
||||||
setPreviousPath(state, val) {
|
setPreviousPath(state, val) {
|
||||||
state.previousPath = val
|
state.previousPath = val
|
||||||
},
|
},
|
||||||
@@ -198,6 +203,9 @@ export const mutations = {
|
|||||||
setShowEditModal(state, val) {
|
setShowEditModal(state, val) {
|
||||||
state.showEditModal = val
|
state.showEditModal = val
|
||||||
},
|
},
|
||||||
|
setEditPodcastModalTab(state, tab) {
|
||||||
|
state.editPodcastModalTab = tab
|
||||||
|
},
|
||||||
showEReader(state, libraryItem) {
|
showEReader(state, libraryItem) {
|
||||||
state.selectedLibraryItem = libraryItem
|
state.selectedLibraryItem = libraryItem
|
||||||
|
|
||||||
|
|||||||
+22
-4
@@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
tasks: []
|
tasks: [],
|
||||||
|
queuedEmbedLIds: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getTaskByLibraryItemId: (state) => (libraryItemId) => {
|
getTasksByLibraryItemId: (state) => (libraryItemId) => {
|
||||||
return state.tasks.find(t => t.data && t.data.libraryItemId === libraryItemId)
|
return state.tasks.filter(t => t.data && t.data.libraryItemId === libraryItemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,14 +19,31 @@ export const mutations = {
|
|||||||
state.tasks = tasks
|
state.tasks = tasks
|
||||||
},
|
},
|
||||||
addUpdateTask(state, task) {
|
addUpdateTask(state, task) {
|
||||||
var index = state.tasks.findIndex(d => d.id === task.id)
|
const index = state.tasks.findIndex(d => d.id === task.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state.tasks.splice(index, 1, task)
|
state.tasks.splice(index, 1, task)
|
||||||
} else {
|
} else {
|
||||||
|
// Remove duplicate (only have one library item per action)
|
||||||
|
state.tasks = state.tasks.filter(_task => {
|
||||||
|
if (!_task.data?.libraryItemId || _task.action !== task.action) return true
|
||||||
|
return _task.data.libraryItemId !== task.data.libraryItemId
|
||||||
|
})
|
||||||
|
|
||||||
state.tasks.push(task)
|
state.tasks.push(task)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeTask(state, task) {
|
removeTask(state, task) {
|
||||||
state.tasks = state.tasks.filter(d => d.id !== task.id)
|
state.tasks = state.tasks.filter(d => d.id !== task.id)
|
||||||
|
},
|
||||||
|
setQueuedEmbedLIds(state, libraryItemIds) {
|
||||||
|
state.queuedEmbedLIds = libraryItemIds
|
||||||
|
},
|
||||||
|
addQueuedEmbedLId(state, libraryItemId) {
|
||||||
|
if (!state.queuedEmbedLIds.some(lid => lid === libraryItemId)) {
|
||||||
|
state.queuedEmbedLIds.push(libraryItemId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeQueuedEmbedLId(state, libraryItemId) {
|
||||||
|
state.queuedEmbedLIds = state.queuedEmbedLIds.filter(lid => lid !== libraryItemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+25
-2
@@ -17,9 +17,10 @@
|
|||||||
"ButtonCloseFeed": "Feed schließen",
|
"ButtonCloseFeed": "Feed schließen",
|
||||||
"ButtonCollections": "Sammlungen",
|
"ButtonCollections": "Sammlungen",
|
||||||
"ButtonConfigureScanner": "Scannereinstellungen",
|
"ButtonConfigureScanner": "Scannereinstellungen",
|
||||||
"ButtonCreate": "Ertsellen",
|
"ButtonCreate": "Erstellen",
|
||||||
"ButtonCreateBackup": "Sicherung erstellen",
|
"ButtonCreateBackup": "Sicherung erstellen",
|
||||||
"ButtonDelete": "Löschen",
|
"ButtonDelete": "Löschen",
|
||||||
|
"ButtonDownloadQueue": "Queue",
|
||||||
"ButtonEdit": "Bearbeiten",
|
"ButtonEdit": "Bearbeiten",
|
||||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||||
@@ -92,7 +93,9 @@
|
|||||||
"HeaderCollection": "Sammlungen",
|
"HeaderCollection": "Sammlungen",
|
||||||
"HeaderCollectionItems": "Sammlungseinträge",
|
"HeaderCollectionItems": "Sammlungseinträge",
|
||||||
"HeaderCover": "Titelbild",
|
"HeaderCover": "Titelbild",
|
||||||
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
@@ -126,6 +129,7 @@
|
|||||||
"HeaderPreviewCover": "Vorschau Titelbild",
|
"HeaderPreviewCover": "Vorschau Titelbild",
|
||||||
"HeaderRemoveEpisode": "Episode löschen",
|
"HeaderRemoveEpisode": "Episode löschen",
|
||||||
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
||||||
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
||||||
"HeaderSchedule": "Zeitplan",
|
"HeaderSchedule": "Zeitplan",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"HeaderSettingsGeneral": "Allgemein",
|
"HeaderSettingsGeneral": "Allgemein",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Einschlaf-Timer",
|
"HeaderSleepTimer": "Einschlaf-Timer",
|
||||||
|
"HeaderStatsLargestItems": "Largest Items",
|
||||||
"HeaderStatsLongestItems": "Längste Einträge (h)",
|
"HeaderStatsLongestItems": "Längste Einträge (h)",
|
||||||
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
||||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||||
@@ -150,6 +155,7 @@
|
|||||||
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
||||||
"HeaderUsers": "Benutzer",
|
"HeaderUsers": "Benutzer",
|
||||||
"HeaderYourStats": "Eigene Statistiken",
|
"HeaderYourStats": "Eigene Statistiken",
|
||||||
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Kontoart",
|
"LabelAccountType": "Kontoart",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
@@ -162,6 +168,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
|
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
|
"LabelAlreadyInYourLibrary": "Already in your library",
|
||||||
"LabelAppend": "Anhängen",
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||||
@@ -192,6 +199,7 @@
|
|||||||
"LabelCronExpression": "Cron Ausdruck",
|
"LabelCronExpression": "Cron Ausdruck",
|
||||||
"LabelCurrent": "Aktuell",
|
"LabelCurrent": "Aktuell",
|
||||||
"LabelCurrently": "Aktuell:",
|
"LabelCurrently": "Aktuell:",
|
||||||
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Datum & Uhrzeit",
|
"LabelDatetime": "Datum & Uhrzeit",
|
||||||
"LabelDescription": "Beschreibung",
|
"LabelDescription": "Beschreibung",
|
||||||
"LabelDeselectAll": "Alles abwählen",
|
"LabelDeselectAll": "Alles abwählen",
|
||||||
@@ -209,6 +217,7 @@
|
|||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episodentitel",
|
"LabelEpisodeTitle": "Episodentitel",
|
||||||
"LabelEpisodeType": "Episodentyp",
|
"LabelEpisodeType": "Episodentyp",
|
||||||
|
"LabelExample": "Example",
|
||||||
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
@@ -270,6 +279,8 @@
|
|||||||
"LabelNewestAuthors": "Neuste Autoren",
|
"LabelNewestAuthors": "Neuste Autoren",
|
||||||
"LabelNewestEpisodes": "Neueste Episoden",
|
"LabelNewestEpisodes": "Neueste Episoden",
|
||||||
"LabelNewPassword": "Neues Passwort",
|
"LabelNewPassword": "Neues Passwort",
|
||||||
|
"LabelNextBackupDate": "Next backup date",
|
||||||
|
"LabelNextScheduledRun": "Next scheduled run",
|
||||||
"LabelNotes": "Hinweise",
|
"LabelNotes": "Hinweise",
|
||||||
"LabelNotFinished": "nicht beendet",
|
"LabelNotFinished": "nicht beendet",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -300,7 +311,9 @@
|
|||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
||||||
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
"LabelProvider": "Anbieter",
|
"LabelProvider": "Anbieter",
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
@@ -312,7 +325,10 @@
|
|||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Offen",
|
"LabelRSSFeedOpen": "RSS Feed Offen",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Begriff suchen",
|
"LabelSearchTerm": "Begriff suchen",
|
||||||
@@ -357,6 +373,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
|
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
||||||
|
"LabelSettingsTimeFormat": "Time Format",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
@@ -381,9 +398,10 @@
|
|||||||
"LabelStatsWeekListening": "Gehörte Wochen",
|
"LabelStatsWeekListening": "Gehörte Wochen",
|
||||||
"LabelSubtitle": "Untertitel",
|
"LabelSubtitle": "Untertitel",
|
||||||
"LabelSupportedFileTypes": "Unterstützte Dateitypen",
|
"LabelSupportedFileTypes": "Unterstützte Dateitypen",
|
||||||
"LabelTag": "Tag",
|
"LabelTag": "Schlagwort",
|
||||||
"LabelTags": "Schlagwörter",
|
"LabelTags": "Schlagwörter",
|
||||||
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
||||||
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeListened": "Gehörte Zeit",
|
"LabelTimeListened": "Gehörte Zeit",
|
||||||
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
||||||
"LabelTimeRemaining": "{0} verbleibend",
|
"LabelTimeRemaining": "{0} verbleibend",
|
||||||
@@ -403,6 +421,7 @@
|
|||||||
"LabelTracksMultiTrack": "Mehrfachdatei",
|
"LabelTracksMultiTrack": "Mehrfachdatei",
|
||||||
"LabelTracksSingleTrack": "Einzeldatei",
|
"LabelTracksSingleTrack": "Einzeldatei",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "Unbekannt",
|
"LabelUnknown": "Unbekannt",
|
||||||
"LabelUpdateCover": "Titelbild aktualisieren",
|
"LabelUpdateCover": "Titelbild aktualisieren",
|
||||||
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
|
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
|
||||||
@@ -485,6 +504,8 @@
|
|||||||
"MessageNoCollections": "Keine Sammlungen",
|
"MessageNoCollections": "Keine Sammlungen",
|
||||||
"MessageNoCoversFound": "Keine Titelbilder gefunden",
|
"MessageNoCoversFound": "Keine Titelbilder gefunden",
|
||||||
"MessageNoDescription": "Keine Beschreibung",
|
"MessageNoDescription": "Keine Beschreibung",
|
||||||
|
"MessageNoDownloadsInProgress": "No downloads currently in progress",
|
||||||
|
"MessageNoDownloadsQueued": "No downloads queued",
|
||||||
"MessageNoEpisodeMatchesFound": "Keine Episodenübereinstimmungen gefunden",
|
"MessageNoEpisodeMatchesFound": "Keine Episodenübereinstimmungen gefunden",
|
||||||
"MessageNoEpisodes": "Keine Episoden",
|
"MessageNoEpisodes": "Keine Episoden",
|
||||||
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
||||||
@@ -501,6 +522,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
||||||
"MessageNoSeries": "Keine Serien",
|
"MessageNoSeries": "Keine Serien",
|
||||||
"MessageNoTags": "Keine Tags",
|
"MessageNoTags": "Keine Tags",
|
||||||
|
"MessageNoTasksRunning": "No Tasks Running",
|
||||||
"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",
|
||||||
@@ -546,6 +568,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||||
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
|
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
|
||||||
"PlaceholderSearch": "Suche...",
|
"PlaceholderSearch": "Suche...",
|
||||||
|
"PlaceholderSearchEpisode": "Search episode...",
|
||||||
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
||||||
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
||||||
"ToastAuthorImageRemoveFailed": "Bild konnte nicht entfernt werden",
|
"ToastAuthorImageRemoveFailed": "Bild konnte nicht entfernt werden",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Create",
|
"ButtonCreate": "Create",
|
||||||
"ButtonCreateBackup": "Create Backup",
|
"ButtonCreateBackup": "Create Backup",
|
||||||
"ButtonDelete": "Delete",
|
"ButtonDelete": "Delete",
|
||||||
|
"ButtonDownloadQueue": "Queue",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edit Chapters",
|
"ButtonEditChapters": "Edit Chapters",
|
||||||
"ButtonEditPodcast": "Edit Podcast",
|
"ButtonEditPodcast": "Edit Podcast",
|
||||||
@@ -92,7 +93,9 @@
|
|||||||
"HeaderCollection": "Collection",
|
"HeaderCollection": "Collection",
|
||||||
"HeaderCollectionItems": "Collection Items",
|
"HeaderCollectionItems": "Collection Items",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEpisodes": "Episodes",
|
"HeaderEpisodes": "Episodes",
|
||||||
"HeaderFiles": "Files",
|
"HeaderFiles": "Files",
|
||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
@@ -126,6 +129,7 @@
|
|||||||
"HeaderPreviewCover": "Preview Cover",
|
"HeaderPreviewCover": "Preview Cover",
|
||||||
"HeaderRemoveEpisode": "Remove Episode",
|
"HeaderRemoveEpisode": "Remove Episode",
|
||||||
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
||||||
"HeaderSavedMediaProgress": "Saved Media Progress",
|
"HeaderSavedMediaProgress": "Saved Media Progress",
|
||||||
"HeaderSchedule": "Schedule",
|
"HeaderSchedule": "Schedule",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"HeaderSettingsGeneral": "General",
|
"HeaderSettingsGeneral": "General",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Sleep Timer",
|
"HeaderSleepTimer": "Sleep Timer",
|
||||||
|
"HeaderStatsLargestItems": "Largest Items",
|
||||||
"HeaderStatsLongestItems": "Longest Items (hrs)",
|
"HeaderStatsLongestItems": "Longest Items (hrs)",
|
||||||
"HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
|
"HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
|
||||||
"HeaderStatsRecentSessions": "Recent Sessions",
|
"HeaderStatsRecentSessions": "Recent Sessions",
|
||||||
@@ -150,6 +155,7 @@
|
|||||||
"HeaderUpdateLibrary": "Update Library",
|
"HeaderUpdateLibrary": "Update Library",
|
||||||
"HeaderUsers": "Users",
|
"HeaderUsers": "Users",
|
||||||
"HeaderYourStats": "Your Stats",
|
"HeaderYourStats": "Your Stats",
|
||||||
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Account Type",
|
"LabelAccountType": "Account Type",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Guest",
|
"LabelAccountTypeGuest": "Guest",
|
||||||
@@ -162,6 +168,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "All Users",
|
"LabelAllUsers": "All Users",
|
||||||
|
"LabelAlreadyInYourLibrary": "Already in your library",
|
||||||
"LabelAppend": "Append",
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Author",
|
"LabelAuthor": "Author",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Author (First Last)",
|
||||||
@@ -192,6 +199,7 @@
|
|||||||
"LabelCronExpression": "Cron Expression",
|
"LabelCronExpression": "Cron Expression",
|
||||||
"LabelCurrent": "Current",
|
"LabelCurrent": "Current",
|
||||||
"LabelCurrently": "Currently:",
|
"LabelCurrently": "Currently:",
|
||||||
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Datetime",
|
"LabelDatetime": "Datetime",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
"LabelDeselectAll": "Deselect All",
|
"LabelDeselectAll": "Deselect All",
|
||||||
@@ -209,6 +217,7 @@
|
|||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episode Title",
|
"LabelEpisodeTitle": "Episode Title",
|
||||||
"LabelEpisodeType": "Episode Type",
|
"LabelEpisodeType": "Episode Type",
|
||||||
|
"LabelExample": "Example",
|
||||||
"LabelExplicit": "Explicit",
|
"LabelExplicit": "Explicit",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "File",
|
"LabelFile": "File",
|
||||||
@@ -270,6 +279,8 @@
|
|||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
"LabelNewestEpisodes": "Newest Episodes",
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNewPassword": "New Password",
|
"LabelNewPassword": "New Password",
|
||||||
|
"LabelNextBackupDate": "Next backup date",
|
||||||
|
"LabelNextScheduledRun": "Next scheduled run",
|
||||||
"LabelNotes": "Notes",
|
"LabelNotes": "Notes",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -300,7 +311,9 @@
|
|||||||
"LabelPlayMethod": "Play Method",
|
"LabelPlayMethod": "Play Method",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Progress",
|
"LabelProgress": "Progress",
|
||||||
"LabelProvider": "Provider",
|
"LabelProvider": "Provider",
|
||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
@@ -312,7 +325,10 @@
|
|||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Open",
|
"LabelRSSFeedOpen": "RSS Feed Open",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Search Term",
|
"LabelSearchTerm": "Search Term",
|
||||||
@@ -357,6 +373,7 @@
|
|||||||
"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",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Store metadata with item",
|
"LabelSettingsStoreMetadataWithItem": "Store metadata with item",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension",
|
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension",
|
||||||
|
"LabelSettingsTimeFormat": "Time Format",
|
||||||
"LabelShowAll": "Show All",
|
"LabelShowAll": "Show All",
|
||||||
"LabelSize": "Size",
|
"LabelSize": "Size",
|
||||||
"LabelSleepTimer": "Sleep timer",
|
"LabelSleepTimer": "Sleep timer",
|
||||||
@@ -384,6 +401,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
"LabelTimeListenedToday": "Time Listened Today",
|
"LabelTimeListenedToday": "Time Listened Today",
|
||||||
"LabelTimeRemaining": "{0} remaining",
|
"LabelTimeRemaining": "{0} remaining",
|
||||||
@@ -403,6 +421,7 @@
|
|||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "Unknown",
|
"LabelUnknown": "Unknown",
|
||||||
"LabelUpdateCover": "Update Cover",
|
"LabelUpdateCover": "Update Cover",
|
||||||
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
|
||||||
@@ -485,6 +504,8 @@
|
|||||||
"MessageNoCollections": "No Collections",
|
"MessageNoCollections": "No Collections",
|
||||||
"MessageNoCoversFound": "No Covers Found",
|
"MessageNoCoversFound": "No Covers Found",
|
||||||
"MessageNoDescription": "No description",
|
"MessageNoDescription": "No description",
|
||||||
|
"MessageNoDownloadsInProgress": "No downloads currently in progress",
|
||||||
|
"MessageNoDownloadsQueued": "No downloads queued",
|
||||||
"MessageNoEpisodeMatchesFound": "No episode matches found",
|
"MessageNoEpisodeMatchesFound": "No episode matches found",
|
||||||
"MessageNoEpisodes": "No Episodes",
|
"MessageNoEpisodes": "No Episodes",
|
||||||
"MessageNoFoldersAvailable": "No Folders Available",
|
"MessageNoFoldersAvailable": "No Folders Available",
|
||||||
@@ -501,6 +522,7 @@
|
|||||||
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
"MessageNoSearchResultsFor": "No search results for \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "No Series",
|
||||||
"MessageNoTags": "No Tags",
|
"MessageNoTags": "No Tags",
|
||||||
|
"MessageNoTasksRunning": "No Tasks Running",
|
||||||
"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",
|
||||||
@@ -546,6 +568,7 @@
|
|||||||
"PlaceholderNewFolderPath": "New folder path",
|
"PlaceholderNewFolderPath": "New folder path",
|
||||||
"PlaceholderNewPlaylist": "New playlist name",
|
"PlaceholderNewPlaylist": "New playlist name",
|
||||||
"PlaceholderSearch": "Search..",
|
"PlaceholderSearch": "Search..",
|
||||||
|
"PlaceholderSearchEpisode": "Search episode..",
|
||||||
"ToastAccountUpdateFailed": "Failed to update account",
|
"ToastAccountUpdateFailed": "Failed to update account",
|
||||||
"ToastAccountUpdateSuccess": "Account updated",
|
"ToastAccountUpdateSuccess": "Account updated",
|
||||||
"ToastAuthorImageRemoveFailed": "Failed to remove image",
|
"ToastAuthorImageRemoveFailed": "Failed to remove image",
|
||||||
|
|||||||
+607
-584
File diff suppressed because it is too large
Load Diff
+212
-189
@@ -8,18 +8,19 @@
|
|||||||
"ButtonAuthors": "Auteurs",
|
"ButtonAuthors": "Auteurs",
|
||||||
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
|
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
|
||||||
"ButtonCancel": "Annuler",
|
"ButtonCancel": "Annuler",
|
||||||
"ButtonCancelEncode": "Annuler l'encodage",
|
"ButtonCancelEncode": "Annuler l’encodage",
|
||||||
"ButtonChangeRootPassword": "Changer le mot de passe Administrateur",
|
"ButtonChangeRootPassword": "Modifier le mot de passe Administrateur",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Vérifier & télécharger de nouveaux épisodes",
|
"ButtonCheckAndDownloadNewEpisodes": "Vérifier et télécharger de nouveaux épisodes",
|
||||||
"ButtonChooseAFolder": "Choisir un dossier",
|
"ButtonChooseAFolder": "Choisir un dossier",
|
||||||
"ButtonChooseFiles": "Choisir les fichiers",
|
"ButtonChooseFiles": "Choisir les fichiers",
|
||||||
"ButtonClearFilter": "Effacer le filtre",
|
"ButtonClearFilter": "Effacer le filtre",
|
||||||
"ButtonCloseFeed": "Fermer le flux",
|
"ButtonCloseFeed": "Fermer le flux",
|
||||||
"ButtonCollections": "Collections",
|
"ButtonCollections": "Collections",
|
||||||
"ButtonConfigureScanner": "Configurer l'analyse",
|
"ButtonConfigureScanner": "Configurer l’analyse",
|
||||||
"ButtonCreate": "Créer",
|
"ButtonCreate": "Créer",
|
||||||
"ButtonCreateBackup": "Créer une sauvegarde",
|
"ButtonCreateBackup": "Créer une sauvegarde",
|
||||||
"ButtonDelete": "Effacer",
|
"ButtonDelete": "Effacer",
|
||||||
|
"ButtonDownloadQueue": "Queue",
|
||||||
"ButtonEdit": "Modifier",
|
"ButtonEdit": "Modifier",
|
||||||
"ButtonEditChapters": "Modifier les chapitres",
|
"ButtonEditChapters": "Modifier les chapitres",
|
||||||
"ButtonEditPodcast": "Modifier les podcasts",
|
"ButtonEditPodcast": "Modifier les podcasts",
|
||||||
@@ -30,16 +31,16 @@
|
|||||||
"ButtonIssues": "Parutions",
|
"ButtonIssues": "Parutions",
|
||||||
"ButtonLatest": "Dernière version",
|
"ButtonLatest": "Dernière version",
|
||||||
"ButtonLibrary": "Bibliothèque",
|
"ButtonLibrary": "Bibliothèque",
|
||||||
"ButtonLogout": "Se Déconnecter",
|
"ButtonLogout": "Me déconnecter",
|
||||||
"ButtonLookup": "Rechercher",
|
"ButtonLookup": "Chercher",
|
||||||
"ButtonManageTracks": "Gérer les pistes",
|
"ButtonManageTracks": "Gérer les pistes",
|
||||||
"ButtonMapChapterTitles": "Correspondance des titres de chapitres",
|
"ButtonMapChapterTitles": "Correspondance des titres de chapitres",
|
||||||
"ButtonMatchAllAuthors": "Rechercher tous les auteurs",
|
"ButtonMatchAllAuthors": "Chercher tous les auteurs",
|
||||||
"ButtonMatchBooks": "Rechercher les Livres",
|
"ButtonMatchBooks": "Chercher les livres",
|
||||||
"ButtonNevermind": "Oubliez cela",
|
"ButtonNevermind": "Non merci",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Ouvrir le Flux",
|
"ButtonOpenFeed": "Ouvrir le flux",
|
||||||
"ButtonOpenManager": "Ouvrir le Gestionnaire",
|
"ButtonOpenManager": "Ouvrir le gestionnaire",
|
||||||
"ButtonPlay": "Écouter",
|
"ButtonPlay": "Écouter",
|
||||||
"ButtonPlaying": "En lecture",
|
"ButtonPlaying": "En lecture",
|
||||||
"ButtonPlaylists": "Listes de lecture",
|
"ButtonPlaylists": "Listes de lecture",
|
||||||
@@ -59,25 +60,25 @@
|
|||||||
"ButtonReset": "Réinitialiser",
|
"ButtonReset": "Réinitialiser",
|
||||||
"ButtonRestore": "Rétablir",
|
"ButtonRestore": "Rétablir",
|
||||||
"ButtonSave": "Sauvegarder",
|
"ButtonSave": "Sauvegarder",
|
||||||
"ButtonSaveAndClose": "Sauvegarder & Fermer",
|
"ButtonSaveAndClose": "Sauvegarder et Fermer",
|
||||||
"ButtonSaveTracklist": "Sauvegarder la liste de lecture",
|
"ButtonSaveTracklist": "Sauvegarder la liste de lecture",
|
||||||
"ButtonScan": "Analyser",
|
"ButtonScan": "Analyser",
|
||||||
"ButtonScanLibrary": "Analyser la bibliothèque",
|
"ButtonScanLibrary": "Analyser la bibliothèque",
|
||||||
"ButtonSearch": "Rechercher",
|
"ButtonSearch": "Chercher",
|
||||||
"ButtonSelectFolderPath": "Sélectionner le Chemin du dossier",
|
"ButtonSelectFolderPath": "Sélectionner le chemin du dossier",
|
||||||
"ButtonSeries": "Séries",
|
"ButtonSeries": "Séries",
|
||||||
"ButtonSetChaptersFromTracks": "Positionner les Chapitre par rapports aux Pistes",
|
"ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes",
|
||||||
"ButtonShiftTimes": "Décaler le Temps",
|
"ButtonShiftTimes": "Décaler l’horodatage du livre",
|
||||||
"ButtonShow": "Afficher",
|
"ButtonShow": "Afficher",
|
||||||
"ButtonStartM4BEncode": "Démarrer l'encodage M4B",
|
"ButtonStartM4BEncode": "Démarrer l’encodage M4B",
|
||||||
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées Intégrées",
|
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées intégrées",
|
||||||
"ButtonSubmit": "Soumettre",
|
"ButtonSubmit": "Soumettre",
|
||||||
"ButtonUpload": "Téléverser",
|
"ButtonUpload": "Téléverser",
|
||||||
"ButtonUploadBackup": "Téléverser une Sauvegarde",
|
"ButtonUploadBackup": "Téléverser une sauvegarde",
|
||||||
"ButtonUploadCover": "Téléverser une Couverture",
|
"ButtonUploadCover": "Téléverser une couverture",
|
||||||
"ButtonUploadOPMLFile": "Téléverser un Fichier OPML",
|
"ButtonUploadOPMLFile": "Téléverser un fichier OPML",
|
||||||
"ButtonUserDelete": "Effacer l'utilisateur {0}",
|
"ButtonUserDelete": "Effacer l’utilisateur {0}",
|
||||||
"ButtonUserEdit": "Modifier l'utilisateur {0}",
|
"ButtonUserEdit": "Modifier l’utilisateur {0}",
|
||||||
"ButtonViewAll": "Afficher tout",
|
"ButtonViewAll": "Afficher tout",
|
||||||
"ButtonYes": "Oui",
|
"ButtonYes": "Oui",
|
||||||
"HeaderAccount": "Compte",
|
"HeaderAccount": "Compte",
|
||||||
@@ -86,47 +87,50 @@
|
|||||||
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
||||||
"HeaderAudioTracks": "Pistes zudio",
|
"HeaderAudioTracks": "Pistes zudio",
|
||||||
"HeaderBackups": "Sauvegardes",
|
"HeaderBackups": "Sauvegardes",
|
||||||
"HeaderChangePassword": "Chager le mot de passe",
|
"HeaderChangePassword": "Modifier le mot de passe",
|
||||||
"HeaderChapters": "Chapitres",
|
"HeaderChapters": "Chapitres",
|
||||||
"HeaderChooseAFolder": "Choisir un dossier",
|
"HeaderChooseAFolder": "Choisir un dossier",
|
||||||
"HeaderCollection": "Collection",
|
"HeaderCollection": "Collection",
|
||||||
"HeaderCollectionItems": "Entrées de la Collection",
|
"HeaderCollectionItems": "Entrées de la Collection",
|
||||||
"HeaderCover": "Couverture",
|
"HeaderCover": "Couverture",
|
||||||
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Détails",
|
"HeaderDetails": "Détails",
|
||||||
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEpisodes": "Épisodes",
|
"HeaderEpisodes": "Épisodes",
|
||||||
"HeaderFiles": "Fichiers",
|
"HeaderFiles": "Fichiers",
|
||||||
"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": "Outils de gestion des métadonnées",
|
"HeaderItemMetadataUtils": "Outils de gestion des métadonnées",
|
||||||
"HeaderLastListeningSession": "Dernière Session d'écoute",
|
"HeaderLastListeningSession": "Dernière Session d’écoute",
|
||||||
"HeaderLatestEpisodes": "Dernier épisodes",
|
"HeaderLatestEpisodes": "Dernier épisodes",
|
||||||
"HeaderLibraries": "Bibliothèque",
|
"HeaderLibraries": "Bibliothèque",
|
||||||
"HeaderLibraryFiles": "Fichier de bibliothèque",
|
"HeaderLibraryFiles": "Fichier de bibliothèque",
|
||||||
"HeaderLibraryStats": "Statistiques de bibliothèque",
|
"HeaderLibraryStats": "Statistiques de bibliothèque",
|
||||||
"HeaderListeningSessions": "Sessions d'écoute",
|
"HeaderListeningSessions": "Sessions d’écoute",
|
||||||
"HeaderListeningStats": "Statistiques d'écoute",
|
"HeaderListeningStats": "Statistiques d’écoute",
|
||||||
"HeaderLogin": "Connexion",
|
"HeaderLogin": "Connexion",
|
||||||
"HeaderLogs": "Fichiers Journaux",
|
"HeaderLogs": "Journaux",
|
||||||
"HeaderManageGenres": "Gérer les genres",
|
"HeaderManageGenres": "Gérer les genres",
|
||||||
"HeaderManageTags": "Gérer les étiquettes",
|
"HeaderManageTags": "Gérer les étiquettes",
|
||||||
"HeaderMapDetails": "Édition en Masse",
|
"HeaderMapDetails": "Édition en masse",
|
||||||
"HeaderMatch": "Rechercher",
|
"HeaderMatch": "Chercher",
|
||||||
"HeaderMetadataToEmbed": "Métadonnée à Intégrer",
|
"HeaderMetadataToEmbed": "Métadonnée à intégrer",
|
||||||
"HeaderNewAccount": "Nouveau Compte",
|
"HeaderNewAccount": "Nouveau compte",
|
||||||
"HeaderNewLibrary": "Nouvelle Bibliothèque",
|
"HeaderNewLibrary": "Nouvelle bibliothèque",
|
||||||
"HeaderNotifications": "Notifications",
|
"HeaderNotifications": "Notifications",
|
||||||
"HeaderOpenRSSFeed": "Ouvrir Flux RSS",
|
"HeaderOpenRSSFeed": "Ouvrir Flux RSS",
|
||||||
"HeaderOtherFiles": "Autres fichiers",
|
"HeaderOtherFiles": "Autres fichiers",
|
||||||
"HeaderPermissions": "Permissions",
|
"HeaderPermissions": "Permissions",
|
||||||
"HeaderPlayerQueue": "Liste d'écoute",
|
"HeaderPlayerQueue": "Liste d’écoute",
|
||||||
"HeaderPlaylist": "Liste de lecture",
|
"HeaderPlaylist": "Liste de lecture",
|
||||||
"HeaderPlaylistItems": "Éléments de la liste de lecture",
|
"HeaderPlaylistItems": "Éléments de la liste de lecture",
|
||||||
"HeaderPodcastsToAdd": "Podcasts à ajouter",
|
"HeaderPodcastsToAdd": "Podcasts à ajouter",
|
||||||
"HeaderPreviewCover": "Prévisualiser la couverture",
|
"HeaderPreviewCover": "Prévisualiser la couverture",
|
||||||
"HeaderRemoveEpisode": "Supprimer l'épisode",
|
"HeaderRemoveEpisode": "Supprimer l’épisode",
|
||||||
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
|
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
|
||||||
"HeaderRSSFeedIsOpen": "Le Flux RSS et Ouvert",
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
|
"HeaderRSSFeedIsOpen": "Le Flux RSS est actif",
|
||||||
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
|
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
|
||||||
"HeaderSchedule": "Programmation",
|
"HeaderSchedule": "Programmation",
|
||||||
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
||||||
@@ -138,45 +142,48 @@
|
|||||||
"HeaderSettingsGeneral": "Général",
|
"HeaderSettingsGeneral": "Général",
|
||||||
"HeaderSettingsScanner": "Scanneur",
|
"HeaderSettingsScanner": "Scanneur",
|
||||||
"HeaderSleepTimer": "Minuterie",
|
"HeaderSleepTimer": "Minuterie",
|
||||||
|
"HeaderStatsLargestItems": "Articles les plus lourd",
|
||||||
"HeaderStatsLongestItems": "Articles les plus long (heures)",
|
"HeaderStatsLongestItems": "Articles les plus long (heures)",
|
||||||
"HeaderStatsMinutesListeningChart": "Minutes d'écoute (7 derniers jours)",
|
"HeaderStatsMinutesListeningChart": "Minutes d’écoute (7 derniers jours)",
|
||||||
"HeaderStatsRecentSessions": "Sessions récentes",
|
"HeaderStatsRecentSessions": "Sessions récentes",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Auteurs",
|
"HeaderStatsTop10Authors": "Top 10 Auteurs",
|
||||||
"HeaderStatsTop5Genres": "Top 5 Genres",
|
"HeaderStatsTop5Genres": "Top 5 Genres",
|
||||||
"HeaderTools": "Outils",
|
"HeaderTools": "Outils",
|
||||||
"HeaderUpdateAccount": "Mettre à jour le compte",
|
"HeaderUpdateAccount": "Mettre à jour le compte",
|
||||||
"HeaderUpdateAuthor": "Mettre à jour l'auteur",
|
"HeaderUpdateAuthor": "Mettre à jour l’auteur",
|
||||||
"HeaderUpdateDetails": "Mettre à jour les détails",
|
"HeaderUpdateDetails": "Mettre à jour les détails",
|
||||||
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
|
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
|
||||||
"HeaderUsers": "Utilisateurs",
|
"HeaderUsers": "Utilisateurs",
|
||||||
"HeaderYourStats": "Vos statistiques",
|
"HeaderYourStats": "Vos statistiques",
|
||||||
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Type de compte",
|
"LabelAccountType": "Type de compte",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Invité",
|
"LabelAccountTypeGuest": "Invité",
|
||||||
"LabelAccountTypeUser": "Utilisateur",
|
"LabelAccountTypeUser": "Utilisateur",
|
||||||
"LabelActivity": "Activité",
|
"LabelActivity": "Activité",
|
||||||
"LabelAddedAt": "Date d'ajout",
|
"LabelAddedAt": "Date d’ajout",
|
||||||
"LabelAddToCollection": "Ajouter à la collection",
|
"LabelAddToCollection": "Ajouter à la collection",
|
||||||
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
|
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
|
||||||
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
|
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
|
||||||
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
|
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
|
||||||
"LabelAll": "Tout",
|
"LabelAll": "Tout",
|
||||||
"LabelAllUsers": "Tous les Utilisateurs",
|
"LabelAllUsers": "Tous les utilisateurs",
|
||||||
|
"LabelAlreadyInYourLibrary": "Already in your library",
|
||||||
"LabelAppend": "Ajouter",
|
"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)",
|
||||||
"LabelAuthors": "Auteurs",
|
"LabelAuthors": "Auteurs",
|
||||||
"LabelAutoDownloadEpisodes": "Téléchargement automatique d'épisode",
|
"LabelAutoDownloadEpisodes": "Téléchargement automatique d’épisode",
|
||||||
"LabelBackToUser": "Revenir à l'Utilisateur",
|
"LabelBackToUser": "Revenir à l’Utilisateur",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Activer les Sauvegardes Automatiques",
|
"LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Taille de Sauvegarde Maximale (en GB)",
|
"LabelBackupsMaxBackupSize": "Taille maximale de la sauvegarde (en Go)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.",
|
"LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.",
|
||||||
"LabelBackupsNumberToKeep": "Nombre de Sauvegardes à maintenir",
|
"LabelBackupsNumberToKeep": "Nombre de sauvegardes à maintenir",
|
||||||
"LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.",
|
"LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.",
|
||||||
"LabelBooks": "Livres",
|
"LabelBooks": "Livres",
|
||||||
"LabelChangePassword": "Changer le mot de passe",
|
"LabelChangePassword": "Modifier le mot de passe",
|
||||||
"LabelChaptersFound": "Chapitres trouvés",
|
"LabelChaptersFound": "Chapitres trouvés",
|
||||||
"LabelChapterTitle": "Titres du chapitre",
|
"LabelChapterTitle": "Titres du chapitre",
|
||||||
"LabelClosePlayer": "Fermer le lecteur",
|
"LabelClosePlayer": "Fermer le lecteur",
|
||||||
@@ -187,16 +194,17 @@
|
|||||||
"LabelContinueListening": "Continuer la lecture",
|
"LabelContinueListening": "Continuer la lecture",
|
||||||
"LabelContinueSeries": "Continuer la série",
|
"LabelContinueSeries": "Continuer la série",
|
||||||
"LabelCover": "Couverture",
|
"LabelCover": "Couverture",
|
||||||
"LabelCoverImageURL": "URL vers l'image de couverture",
|
"LabelCoverImageURL": "URL vers l’image de couverture",
|
||||||
"LabelCreatedAt": "Créé le",
|
"LabelCreatedAt": "Créé le",
|
||||||
"LabelCronExpression": "Expression Cron",
|
"LabelCronExpression": "Expression Cron",
|
||||||
"LabelCurrent": "Courrant",
|
"LabelCurrent": "Courrant",
|
||||||
"LabelCurrently": "En ce moment :",
|
"LabelCurrently": "En ce moment :",
|
||||||
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Datetime",
|
"LabelDatetime": "Datetime",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
"LabelDeselectAll": "Tout Déselectionner",
|
"LabelDeselectAll": "Tout déselectionner",
|
||||||
"LabelDevice": "Appareil",
|
"LabelDevice": "Appareil",
|
||||||
"LabelDeviceInfo": "Détail de l'appareil",
|
"LabelDeviceInfo": "Détail de l’appareil",
|
||||||
"LabelDirectory": "Répertoire",
|
"LabelDirectory": "Répertoire",
|
||||||
"LabelDiscFromFilename": "Disque depuis le fichier",
|
"LabelDiscFromFilename": "Disque depuis le fichier",
|
||||||
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
||||||
@@ -207,15 +215,16 @@
|
|||||||
"LabelEnable": "Activer",
|
"LabelEnable": "Activer",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
"LabelEpisode": "Épisode",
|
"LabelEpisode": "Épisode",
|
||||||
"LabelEpisodeTitle": "Titre de l'épisode",
|
"LabelEpisodeTitle": "Titre de l’épisode",
|
||||||
"LabelEpisodeType": "Type de l'épisode",
|
"LabelEpisodeType": "Type de l’épisode",
|
||||||
|
"LabelExample": "Example",
|
||||||
"LabelExplicit": "Restriction",
|
"LabelExplicit": "Restriction",
|
||||||
"LabelFeedURL": "URL deu flux",
|
"LabelFeedURL": "URL deu flux",
|
||||||
"LabelFile": "Fichier",
|
"LabelFile": "Fichier",
|
||||||
"LabelFileBirthtime": "Creation du fichier",
|
"LabelFileBirthtime": "Creation du fichier",
|
||||||
"LabelFileModified": "Modification du fichier",
|
"LabelFileModified": "Modification du fichier",
|
||||||
"LabelFilename": "Nom de Fichier",
|
"LabelFilename": "Nom de fichier",
|
||||||
"LabelFilterByUser": "Filtrer par l'utilisateur",
|
"LabelFilterByUser": "Filtrer par l’utilisateur",
|
||||||
"LabelFindEpisodes": "Trouver des épisodes",
|
"LabelFindEpisodes": "Trouver des épisodes",
|
||||||
"LabelFinished": "Fini(e)",
|
"LabelFinished": "Fini(e)",
|
||||||
"LabelFolder": "Dossier",
|
"LabelFolder": "Dossier",
|
||||||
@@ -245,16 +254,16 @@
|
|||||||
"LabelLastTime": "Progression",
|
"LabelLastTime": "Progression",
|
||||||
"LabelLastUpdate": "Dernière mise à jour",
|
"LabelLastUpdate": "Dernière mise à jour",
|
||||||
"LabelLess": "Moins",
|
"LabelLess": "Moins",
|
||||||
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l'utilisateur",
|
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l’utilisateur",
|
||||||
"LabelLibrary": "Bibliothèque",
|
"LabelLibrary": "Bibliothèque",
|
||||||
"LabelLibraryItem": "Article de bibliothèque",
|
"LabelLibraryItem": "Article de bibliothèque",
|
||||||
"LabelLibraryName": "Nom de bibliothèque",
|
"LabelLibraryName": "Nom de la bibliothèque",
|
||||||
"LabelLimit": "Limite",
|
"LabelLimit": "Limite",
|
||||||
"LabelListenAgain": "Écouter à nouveau",
|
"LabelListenAgain": "Écouter à nouveau",
|
||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Rechercher de nouveaux épisode après cette date",
|
"LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date",
|
||||||
"LabelMediaPlayer": "Lecteur multimédia",
|
"LabelMediaPlayer": "Lecteur multimédia",
|
||||||
"LabelMediaType": "Type de média",
|
"LabelMediaType": "Type de média",
|
||||||
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
||||||
@@ -270,50 +279,57 @@
|
|||||||
"LabelNewestAuthors": "Nouveaux auteurs",
|
"LabelNewestAuthors": "Nouveaux auteurs",
|
||||||
"LabelNewestEpisodes": "Derniers épisodes",
|
"LabelNewestEpisodes": "Derniers épisodes",
|
||||||
"LabelNewPassword": "Nouveau mot de passe",
|
"LabelNewPassword": "Nouveau mot de passe",
|
||||||
|
"LabelNextBackupDate": "Next backup date",
|
||||||
|
"LabelNextScheduledRun": "Next scheduled run",
|
||||||
"LabelNotes": "Notes",
|
"LabelNotes": "Notes",
|
||||||
"LabelNotFinished": "Non terminé(e)",
|
"LabelNotFinished": "Non terminé(e)",
|
||||||
"LabelNotificationAppriseURL": "URL(s) d'apprise",
|
"LabelNotificationAppriseURL": "URL(s) d’apprise",
|
||||||
"LabelNotificationAvailableVariables": "Variables disponibles",
|
"LabelNotificationAvailableVariables": "Variables disponibles",
|
||||||
"LabelNotificationBodyTemplate": "Modèle de Message",
|
"LabelNotificationBodyTemplate": "Modèle de Message",
|
||||||
"LabelNotificationEvent": "Evènement de Notification",
|
"LabelNotificationEvent": "Evènement de Notification",
|
||||||
"LabelNotificationsMaxFailedAttempts": "Nombres de tentatives d'envoi",
|
"LabelNotificationsMaxFailedAttempts": "Nombres de tentatives d’envoi",
|
||||||
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
|
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
|
||||||
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
|
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Le notification seront ignorées si la file d'attente est à son maximum. Cela empêche un flot trop important.",
|
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Le notification seront ignorées si la file d’attente est à son maximum. Cela empêche un flot trop important.",
|
||||||
"LabelNotificationTitleTemplate": "Modèle de Titre",
|
"LabelNotificationTitleTemplate": "Modèle de Titre",
|
||||||
"LabelNotStarted": "Non Démarré(e)",
|
"LabelNotStarted": "Non Démarré(e)",
|
||||||
"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",
|
"LabelOverwrite": "Écraser",
|
||||||
"LabelPassword": "Mot de Passe",
|
"LabelPassword": "Mot de passe",
|
||||||
"LabelPath": "Chemin",
|
"LabelPath": "Chemin",
|
||||||
"LabelPermissionsAccessAllLibraries": "Peut accéder à toutes les bibliothèque",
|
"LabelPermissionsAccessAllLibraries": "Peut accéder à toutes les bibliothèque",
|
||||||
"LabelPermissionsAccessAllTags": "Peut accéder à toutes les étiquettes",
|
"LabelPermissionsAccessAllTags": "Peut accéder à toutes les étiquettes",
|
||||||
"LabelPermissionsAccessExplicitContent": "Peut acceter au contenu restreint",
|
"LabelPermissionsAccessExplicitContent": "Peut accéder au contenu restreint",
|
||||||
"LabelPermissionsDelete": "Peut supprimer",
|
"LabelPermissionsDelete": "Peut supprimer",
|
||||||
"LabelPermissionsDownload": "Peut télécharger",
|
"LabelPermissionsDownload": "Peut télécharger",
|
||||||
"LabelPermissionsUpdate": "Peut mettre à Jour",
|
"LabelPermissionsUpdate": "Peut mettre à jour",
|
||||||
"LabelPermissionsUpload": "Peut téléverser",
|
"LabelPermissionsUpload": "Peut téléverser",
|
||||||
"LabelPhotoPathURL": "Chemin / URL des photos",
|
"LabelPhotoPathURL": "Chemin / URL des photos",
|
||||||
"LabelPlaylists": "Listes de lecture",
|
"LabelPlaylists": "Listes de lecture",
|
||||||
"LabelPlayMethod": "Méthode d'écoute",
|
"LabelPlayMethod": "Méthode d’écoute",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||||
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Progression",
|
"LabelProgress": "Progression",
|
||||||
"LabelProvider": "Fournisseur",
|
"LabelProvider": "Fournisseur",
|
||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
"LabelPublisher": "Éditeur",
|
"LabelPublisher": "Éditeur",
|
||||||
"LabelPublishYear": "Année d'édition",
|
"LabelPublishYear": "Année d’édition",
|
||||||
"LabelRecentlyAdded": "Derniers ajouts",
|
"LabelRecentlyAdded": "Derniers ajouts",
|
||||||
"LabelRecentSeries": "Séries récentes",
|
"LabelRecentSeries": "Séries récentes",
|
||||||
"LabelRecommended": "Recommandé",
|
"LabelRecommended": "Recommandé",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de parution",
|
"LabelReleaseDate": "Date de parution",
|
||||||
"LabelRemoveCover": "Supprimer la couverture",
|
"LabelRemoveCover": "Supprimer la couverture",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
||||||
"LabelRSSFeedSlug": "Identificateur d'adresse du Flux RSS ",
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
|
"LabelRSSFeedSlug": "Identificateur d’adresse du Flux RSS ",
|
||||||
"LabelRSSFeedURL": "Adresse du flux RSS",
|
"LabelRSSFeedURL": "Adresse du flux RSS",
|
||||||
"LabelSearchTerm": "Terme de recherche",
|
"LabelSearchTerm": "Terme de recherche",
|
||||||
"LabelSearchTitle": "Titre de recherche",
|
"LabelSearchTitle": "Titre de recherche",
|
||||||
@@ -323,40 +339,41 @@
|
|||||||
"LabelSeries": "Séries",
|
"LabelSeries": "Séries",
|
||||||
"LabelSeriesName": "Nom de la série",
|
"LabelSeriesName": "Nom de la série",
|
||||||
"LabelSeriesProgress": "Progression de séries",
|
"LabelSeriesProgress": "Progression de séries",
|
||||||
"LabelSettingsBookshelfViewHelp": "Design Skeuomorphic avec une étagère en bois",
|
"LabelSettingsBookshelfViewHelp": "Interface Skeuomorphic avec une étagère en bois",
|
||||||
"LabelSettingsChromecastSupport": "Support Chromecast",
|
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
||||||
"LabelSettingsDateFormat": "Format de date",
|
"LabelSettingsDateFormat": "Format de date",
|
||||||
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance du dossier pour la bibliothèque",
|
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque",
|
||||||
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque les fichiers changent. *Nécessite un redémarrage*",
|
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque les fichiers changent. *Nécessite un redémarrage*",
|
||||||
"LabelSettingsEnableEReader": "Active E-reader pour tous les utilisateurs",
|
"LabelSettingsEnableEReader": "Active E-reader pour tous les utilisateurs",
|
||||||
"LabelSettingsEnableEReaderHelp": "E-reader est toujours en cours de développement, mais ce paramètre l'active pour tous les utilisateurs (ou utiliser l'interrupteur \"Fonctionnalités Expérimentales\" pour l'activer seulement pour vous)",
|
"LabelSettingsEnableEReaderHelp": "E-reader est toujours en cours de développement, mais ce paramètre l’active pour tous les utilisateurs (ou utiliser l’interrupteur « Fonctionnalités expérimentales » pour l’activer seulement pour vous)",
|
||||||
"LabelSettingsExperimentalFeatures": "Fonctionnalités Expérimentales",
|
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquels nous attendons votre retour et expérience. Cliquer pour ouvrir la discussion Github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquels nous attendons votre retour et expérience. Cliquer pour ouvrir la discussion Github.",
|
||||||
"LabelSettingsFindCovers": "Rechercher des Couvertures",
|
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
||||||
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l'analyser tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d'analyse.",
|
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyser tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.",
|
||||||
"LabelSettingsHomePageBookshelfView": "La page d'accueil utilise la vue étagère",
|
"LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère",
|
||||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "Les fichiers MP3 d'Overdrive viennent avec les minutages des chapitres intégrés en métadonnées. Activer ce paramètre utilisera ces minutages pour les chapitres automatiquement.",
|
"LabelSettingsOverdriveMediaMarkersHelp": "Les fichiers MP3 d’Overdrive viennent avec les minutages des chapitres intégrés en métadonnées. Activer ce paramètre utilisera ces minutages pour les chapitres automatiquement.",
|
||||||
"LabelSettingsParseSubtitles": "Analyse des sous-titres",
|
"LabelSettingsParseSubtitles": "Analyse des sous-titres",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par \" - \"<br>i.e. \"Titre du Livre - Ceci est un sous-titre\" aura le sous-titre \"Ceci est un sous-titre\"",
|
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par « - »<br>i.e. « Titre du Livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »",
|
||||||
"LabelSettingsPreferAudioMetadata": "Préférer les Métadonnées Audio",
|
"LabelSettingsPreferAudioMetadata": "Préférer les Métadonnées audio",
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "Les méta étiquettes ID3 des fichiers audios seront utilisés à la place des noms de dossier pour les détails du livre audio",
|
"LabelSettingsPreferAudioMetadataHelp": "Les méta étiquettes ID3 des fichiers audios seront utilisés à la place des noms de dossier pour les détails du livre audio",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Préférer les Métadonnées par correspondance",
|
"LabelSettingsPreferMatchedMetadata": "Préférer les Métadonnées par correspondance",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l'article lors d'une Recherche par Correspondance Rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l’article lors d’une recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Préférer les Métadonnées OPF",
|
"LabelSettingsPreferOPFMetadata": "Préférer les Métadonnées OPF",
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "Les fichiers de métadonnées OPF seront utilisés à la place des noms de dossier pour les détails du Livre Audio",
|
"LabelSettingsPreferOPFMetadataHelp": "Les fichiers de métadonnées OPF seront utilisés à la place des noms de dossier pour les détails du Livre Audio",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri",
|
"LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. pour le préfixe \"le\", le livre avec pour titre \"Le Titre du Livre\" sera trié en tant que \"Titre du Livre, Le\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »",
|
||||||
"LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées",
|
"LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standardes de 1.6:1.",
|
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standardes de 1.6:1.",
|
||||||
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles",
|
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiersde l'article. Seul un fichier nommé \"cover\" sera gardé.",
|
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de l’article. Seul un fichier nommé « cover » sera conservé.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
|
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de l'article avec une extension \".abs\".",
|
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de l’article avec une extension « .abs ».",
|
||||||
|
"LabelSettingsTimeFormat": "Time Format",
|
||||||
"LabelShowAll": "Afficher Tout",
|
"LabelShowAll": "Afficher Tout",
|
||||||
"LabelSize": "Taille",
|
"LabelSize": "Taille",
|
||||||
"LabelSleepTimer": "Minuterie",
|
"LabelSleepTimer": "Minuterie",
|
||||||
@@ -369,23 +386,24 @@
|
|||||||
"LabelStatsBestDay": "Meilleur Jour",
|
"LabelStatsBestDay": "Meilleur Jour",
|
||||||
"LabelStatsDailyAverage": "Moyenne Journalière",
|
"LabelStatsDailyAverage": "Moyenne Journalière",
|
||||||
"LabelStatsDays": "Jours",
|
"LabelStatsDays": "Jours",
|
||||||
"LabelStatsDaysListened": "Jours d'écoute",
|
"LabelStatsDaysListened": "Jours d’écoute",
|
||||||
"LabelStatsHours": "Heures",
|
"LabelStatsHours": "Heures",
|
||||||
"LabelStatsInARow": "d'affilé(s)",
|
"LabelStatsInARow": "d’affilé(s)",
|
||||||
"LabelStatsItemsFinished": "Articles Terminés",
|
"LabelStatsItemsFinished": "Articles terminés",
|
||||||
"LabelStatsItemsInLibrary": "Articles dans la Bibliothèque",
|
"LabelStatsItemsInLibrary": "Articles dans la Bibliothèque",
|
||||||
"LabelStatsMinutes": "minutes",
|
"LabelStatsMinutes": "minutes",
|
||||||
"LabelStatsMinutesListening": "Minutes d'écoute",
|
"LabelStatsMinutesListening": "Minutes d’écoute",
|
||||||
"LabelStatsOverallDays": "Jours au total",
|
"LabelStatsOverallDays": "Jours au total",
|
||||||
"LabelStatsOverallHours": "Heures au total",
|
"LabelStatsOverallHours": "Heures au total",
|
||||||
"LabelStatsWeekListening": "Écoute de la semaine",
|
"LabelStatsWeekListening": "Écoute de la semaine",
|
||||||
"LabelSubtitle": "Sous-Titre",
|
"LabelSubtitle": "Sous-Titre",
|
||||||
"LabelSupportedFileTypes": "Types de fichiers Supportés",
|
"LabelSupportedFileTypes": "Types de fichiers supportés",
|
||||||
"LabelTag": "Étiquette",
|
"LabelTag": "Étiquette",
|
||||||
"LabelTags": "Étiquettes",
|
"LabelTags": "Étiquettes",
|
||||||
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l'utilisateur",
|
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
||||||
"LabelTimeListened": "Temps d'écoute",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeListenedToday": "Nombres d'écoutes Aujourd'hui",
|
"LabelTimeListened": "Temps d’écoute",
|
||||||
|
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
||||||
"LabelTimeRemaining": "{0} restantes",
|
"LabelTimeRemaining": "{0} restantes",
|
||||||
"LabelTimeToShift": "Temps de décalage en secondes",
|
"LabelTimeToShift": "Temps de décalage en secondes",
|
||||||
"LabelTitle": "Titre",
|
"LabelTitle": "Titre",
|
||||||
@@ -394,166 +412,171 @@
|
|||||||
"LabelToolsMakeM4b": "Créer un fichier Livre Audio M4B",
|
"LabelToolsMakeM4b": "Créer un fichier Livre Audio M4B",
|
||||||
"LabelToolsMakeM4bDescription": "Génère un fichier Livre Audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.",
|
"LabelToolsMakeM4bDescription": "Génère un fichier Livre Audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.",
|
||||||
"LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3",
|
"LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3",
|
||||||
"LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, l'image de couverture et les chapitres.",
|
"LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, l’image de couverture et les chapitres.",
|
||||||
"LabelTotalDuration": "Durée Totale",
|
"LabelTotalDuration": "Durée Totale",
|
||||||
"LabelTotalTimeListened": "Temps d'écoute total",
|
"LabelTotalTimeListened": "Temps d’écoute total",
|
||||||
"LabelTrackFromFilename": "Piste depuis le fichier",
|
"LabelTrackFromFilename": "Piste depuis le fichier",
|
||||||
"LabelTrackFromMetadata": "Piste depuis les métadonnées",
|
"LabelTrackFromMetadata": "Piste depuis les métadonnées",
|
||||||
"LabelTracks": "Pistes",
|
"LabelTracks": "Pistes",
|
||||||
"LabelTracksMultiTrack": "Piste Multiple",
|
"LabelTracksMultiTrack": "Piste multiple",
|
||||||
"LabelTracksSingleTrack": "Piste Simple",
|
"LabelTracksSingleTrack": "Piste simple",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "Inconnu",
|
"LabelUnknown": "Inconnu",
|
||||||
"LabelUpdateCover": "Mettre à jour la Couverture",
|
"LabelUpdateCover": "Mettre à jour la couverture",
|
||||||
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu'une correspondance est trouvée",
|
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsqu’une correspondance est trouvée",
|
||||||
"LabelUpdatedAt": "Mis à jour à",
|
"LabelUpdatedAt": "Mis à jour à",
|
||||||
"LabelUpdateDetails": "Mettre à jours les Détails",
|
"LabelUpdateDetails": "Mettre à jours les détails",
|
||||||
"LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsqu'une correspondance est trouvée",
|
"LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsqu’une correspondance est trouvée",
|
||||||
"LabelUploaderDragAndDrop": "Glisser & Déposer des fichiers ou dossiers",
|
"LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers",
|
||||||
"LabelUploaderDropFiles": "Déposer des fichiers",
|
"LabelUploaderDropFiles": "Déposer des fichiers",
|
||||||
"LabelUseChapterTrack": "Utiliser la Piste du Chapitre",
|
"LabelUseChapterTrack": "Utiliser la piste du chapitre",
|
||||||
"LabelUseFullTrack": "Utiliser la Piste Complète",
|
"LabelUseFullTrack": "Utiliser la piste Complète",
|
||||||
"LabelUser": "Utilisateur",
|
"LabelUser": "Utilisateur",
|
||||||
"LabelUsername": "Nom d'Utilisateur",
|
"LabelUsername": "Nom d’utilisateur",
|
||||||
"LabelValue": "Valeur",
|
"LabelValue": "Valeur",
|
||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
"LabelViewBookmarks": "Afficher les Signets",
|
"LabelViewBookmarks": "Afficher les signets",
|
||||||
"LabelViewChapters": "Afficher les Chapitres",
|
"LabelViewChapters": "Afficher les chapitres",
|
||||||
"LabelViewQueue": "Afficher la liste de lecture",
|
"LabelViewQueue": "Afficher la liste de lecture",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
||||||
"LabelYourAudiobookDuration": "Durée de vos Livres Audios",
|
"LabelYourAudiobookDuration": "Durée de vos livres audios",
|
||||||
"LabelYourBookmarks": "Vos Signets",
|
"LabelYourBookmarks": "Vos signets",
|
||||||
"LabelYourPlaylists": "Vos listes de lecture",
|
"LabelYourPlaylists": "Vos listes de lecture",
|
||||||
"LabelYourProgress": "Votre progression",
|
"LabelYourProgress": "Votre progression",
|
||||||
"MessageAddToPlayerQueue": "Ajouter en file d'attente",
|
"MessageAddToPlayerQueue": "Ajouter en file d’attente",
|
||||||
"MessageAppriseDescription": "Nécessite une instance d'<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />L'URL de l'API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />l’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Les Sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les Sauvegardes n'incluent pas les fichiers de votre bibliothèque.",
|
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes n’incluent pas les fichiers de votre bibliothèque.",
|
||||||
"MessageBatchQuickMatchDescription": "La Recherche par Correspondance Rapide tentera d'ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l'option suivante pour autoriser la Recherche par Correspondance à écraser les données existantes.",
|
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l’option suivante pour autoriser la recherche par correspondance à écraser les données existantes.",
|
||||||
"MessageBookshelfNoCollections": "Vous n'avez pas encore de collections",
|
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
||||||
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0}: {1} »",
|
||||||
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n'est ouvert",
|
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert",
|
||||||
"MessageBookshelfNoSeries": "Vous n'avez aucune séries",
|
"MessageBookshelfNoSeries": "Vous n’avez aucune séries",
|
||||||
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
|
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
|
||||||
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
|
||||||
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
|
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
|
||||||
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
|
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
|
||||||
"MessageChapterStartIsAfter": "Le Chapitre Début est situé au début de votre Livre Audio",
|
"MessageChapterStartIsAfter": "Le Chapitre Début est situé au début de votre Livre Audio",
|
||||||
"MessageCheckingCron": "Vérification du cron...",
|
"MessageCheckingCron": "Vérification du cron…",
|
||||||
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?",
|
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?",
|
||||||
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque \"{0}\" ?",
|
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
|
||||||
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer comme terminé tous les livres de cette série ?",
|
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer comme terminé tous les livres de cette série ?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer comme non terminé tous les livres de cette série ?",
|
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer comme non terminé tous les livres de cette série ?",
|
||||||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection \"{0}\" ?",
|
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||||
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l'épisode \"{0}\" ?",
|
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
||||||
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
||||||
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture \"{0}\" ?",
|
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
|
||||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre \"{0}\" vers \"{1}\" pour tous les articles ?",
|
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » vers « {1} » pour tous les articles ?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||||
"MessageConfirmRenameGenreWarning": "Attention ! Un genre similaire avec une casse différente existe déjà \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "Attention ! Un genre similaire avec une casse différente existe déjà « {0} ».",
|
||||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l'étiquette \"{0}\" vers \"{1}\" pour tous les articles ?",
|
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » vers « {1} » pour tous les articles ?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
||||||
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {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 !",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
|
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
|
||||||
"MessageFeedURLWillBe": "L'URL du Flux sera {0}",
|
"MessageFeedURLWillBe": "l’URL du Flux sera {0}",
|
||||||
"MessageFetching": "Récupération...",
|
"MessageFetching": "Récupération…",
|
||||||
"MessageForceReScanDescription": "Analysera tous les fichiers de nouveau. Les étiquettes ID3 des fichiers audios, fichiers OPF, et les fichiers textes seront analysés comme s'ils étaient nouveaux.",
|
"MessageForceReScanDescription": "Analysera tous les fichiers de nouveau. Les étiquettes ID3 des fichiers audios, fichiers OPF, et les fichiers textes seront analysés comme s’ils étaient nouveaux.",
|
||||||
"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} articles mis à jour",
|
"MessageItemsUpdated": "{0} articles mis à jour",
|
||||||
"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…",
|
||||||
"MessageLoadingFolders": "Chargement des dossiers...",
|
"MessageLoadingFolders": "Chargement des dossiers…",
|
||||||
"MessageM4BFailed": "M4B en échec !",
|
"MessageM4BFailed": "M4B en échec !",
|
||||||
"MessageM4BFinished": "M4B terminé !",
|
"MessageM4BFinished": "M4B terminé !",
|
||||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l'horodatage.",
|
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
||||||
"MessageMarkAsFinished": "Marquer comme terminé",
|
"MessageMarkAsFinished": "Marquer comme terminé",
|
||||||
"MessageMarkAsNotFinished": "Marquer comme non Terminé",
|
"MessageMarkAsNotFinished": "Marquer comme non Terminé",
|
||||||
"MessageMatchBooksDescription": "tentera de faire correspondre les livres de la bibliothèque avec les livres du fournisseur sélectionné pour combler les détails et couverture manquants. N'écrase pas les données existantes.",
|
"MessageMatchBooksDescription": "tentera de faire correspondre les livres de la bibliothèque avec les livres du fournisseur sélectionné pour combler les détails et couverture manquants. N’écrase pas les données existantes.",
|
||||||
"MessageNoAudioTracks": "Pas de pistes audio",
|
"MessageNoAudioTracks": "Aucune piste audio",
|
||||||
"MessageNoAuthors": "Pas d'Auteurs",
|
"MessageNoAuthors": "Aucun auteur",
|
||||||
"MessageNoBackups": "Pas de Sauvegardes",
|
"MessageNoBackups": "Aucune sauvegarde",
|
||||||
"MessageNoBookmarks": "Pas de signets",
|
"MessageNoBookmarks": "Aucun signet",
|
||||||
"MessageNoChapters": "Pas de chapitres",
|
"MessageNoChapters": "Aucun chapitre",
|
||||||
"MessageNoCollections": "Pas de collections",
|
"MessageNoCollections": "Aucune collection",
|
||||||
"MessageNoCoversFound": "Aucune couverture trouvée",
|
"MessageNoCoversFound": "Aucune couverture trouvée",
|
||||||
"MessageNoDescription": "Pas de description",
|
"MessageNoDescription": "Aucune description",
|
||||||
"MessageNoEpisodeMatchesFound": "Pas de correspondance d'épisode trouvée",
|
"MessageNoDownloadsInProgress": "Aucun téléchargement en cours",
|
||||||
|
"MessageNoDownloadsQueued": "Aucun téléchargement en file d’attente",
|
||||||
|
"MessageNoEpisodeMatchesFound": "Aucune correspondance d’épisode trouvée",
|
||||||
"MessageNoEpisodes": "Aucun épisode",
|
"MessageNoEpisodes": "Aucun épisode",
|
||||||
"MessageNoFoldersAvailable": "Aucun dossier disponible",
|
"MessageNoFoldersAvailable": "Aucun dossier disponible",
|
||||||
"MessageNoGenres": "Pas de genres",
|
"MessageNoGenres": "Aucun genre",
|
||||||
"MessageNoIssues": "Pas de parution",
|
"MessageNoIssues": "Aucune parution",
|
||||||
"MessageNoItems": "Pas d'Articles",
|
"MessageNoItems": "Aucun article",
|
||||||
"MessageNoItemsFound": "Pas d'Articles Trouvés",
|
"MessageNoItemsFound": "Aucun article trouvé",
|
||||||
"MessageNoListeningSessions": "Pas de sessions d'écoutes",
|
"MessageNoListeningSessions": "Aucune session d’écoute en cours",
|
||||||
"MessageNoLogs": "Pas de journaux",
|
"MessageNoLogs": "Aucun journaux",
|
||||||
"MessageNoMediaProgress": "Pas de Média en cours",
|
"MessageNoMediaProgress": "Aucun média en cours",
|
||||||
"MessageNoNotifications": "Pas de Notifications",
|
"MessageNoNotifications": "Aucune notification",
|
||||||
"MessageNoPodcastsFound": "Pas de podcasts trouvés",
|
"MessageNoPodcastsFound": "Aucun podcast trouvé",
|
||||||
"MessageNoResults": "Pas de résultats",
|
"MessageNoResults": "Aucun résultat",
|
||||||
"MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"",
|
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
|
||||||
"MessageNoSeries": "Pas de séries",
|
"MessageNoSeries": "Aucune série",
|
||||||
"MessageNoTags": "Pas d'étiquettes",
|
"MessageNoTags": "Aucune d’étiquettes",
|
||||||
|
"MessageNoTasksRunning": "No Tasks Running",
|
||||||
"MessageNotYetImplemented": "Non implémenté",
|
"MessageNotYetImplemented": "Non implémenté",
|
||||||
"MessageNoUpdateNecessary": "Pas de mise à jour nécessaire",
|
"MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
|
||||||
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire",
|
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
|
||||||
"MessageNoUserPlaylists": "Vous n'avez aucune liste de lecture",
|
"MessageNoUserPlaylists": "Vous n’avez aucune liste de lecture",
|
||||||
"MessageOr": "ou",
|
"MessageOr": "ou",
|
||||||
"MessagePauseChapter": "Suspendre la lecture du chapitre",
|
"MessagePauseChapter": "Suspendre la lecture du chapitre",
|
||||||
"MessagePlayChapter": "Écouter depuis le début du chapitre",
|
"MessagePlayChapter": "Écouter depuis le début du chapitre",
|
||||||
"MessagePlaylistCreateFromCollection": "Créer une liste de lecture depuis la collection",
|
"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. Souhaitez-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. Souhaitez-vous continuer ?",
|
||||||
"MessageRemoveChapter": "Supprimer le chapitre",
|
"MessageRemoveChapter": "Supprimer le chapitre",
|
||||||
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Supprimer de la liste d'écoute",
|
"MessageRemoveFromPlayerQueue": "Supprimer de la liste d’écoute",
|
||||||
"MessageRemoveUserWarning": "Êtes-vous certain de vouloir supprimer définitivement l'utilisateur \"{0}\" ?",
|
"MessageRemoveUserWarning": "Êtes-vous certain de vouloir supprimer définitivement l’utilisateur « {0} » ?",
|
||||||
"MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur",
|
"MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur",
|
||||||
"MessageResetChaptersConfirm": "Êtes-vous certain de vouloir réinitialiser les chapitres et annuler les changements effectués ?",
|
"MessageResetChaptersConfirm": "Êtes-vous certain de vouloir réinitialiser les chapitres et annuler les changements effectués ?",
|
||||||
"MessageRestoreBackupConfirm": "Êtes-vous certain de vouloir restaurer la sauvegarde créée le",
|
"MessageRestoreBackupConfirm": "Êtes-vous certain de vouloir restaurer la sauvegarde créée le",
|
||||||
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items & /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
|
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
|
||||||
"MessageSearchResultsFor": "Résultats de recherche pour",
|
"MessageSearchResultsFor": "Résultats de recherche pour",
|
||||||
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
||||||
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
|
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
|
||||||
"MessageStartPlaybackAtTime": "Démarrer la lecture pour \"{0}\" à {1} ?",
|
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
|
||||||
"MessageThinking": "On réfléchit...",
|
"MessageThinking": "Je cherche…",
|
||||||
"MessageUploaderItemFailed": "Échec du téléversement",
|
"MessageUploaderItemFailed": "Échec du téléversement",
|
||||||
"MessageUploaderItemSuccess": "Téléversement effectué !",
|
"MessageUploaderItemSuccess": "Téléversement effectué !",
|
||||||
"MessageUploading": "Téléversement...",
|
"MessageUploading": "Téléversement…",
|
||||||
"MessageValidCronExpression": "Expression cron valide",
|
"MessageValidCronExpression": "Expression cron valide",
|
||||||
"MessageWatcherIsDisabledGlobally": "La Surveillance est désactivée par un paramètre global du serveur",
|
"MessageWatcherIsDisabledGlobally": "La surveillance est désactivée par un paramètre global du serveur",
|
||||||
"MessageXLibraryIsEmpty": "La bibliothèque {0} est vide !",
|
"MessageXLibraryIsEmpty": "La bibliothèque {0} est vide !",
|
||||||
"MessageYourAudiobookDurationIsLonger": "La durée de votre Livre Audio est plus longue que la durée trouvée",
|
"MessageYourAudiobookDurationIsLonger": "La durée de votre Livre Audio est plus longue que la durée trouvée",
|
||||||
"MessageYourAudiobookDurationIsShorter": "La durée de votre Livre Audio est plus courte que la durée trouvée",
|
"MessageYourAudiobookDurationIsShorter": "La durée de votre Livre Audio est plus courte que la durée trouvée",
|
||||||
"NoteChangeRootPassword": "L'utilisateur Root est le seul a pouvoir utiliser un mote de passe vide",
|
"NoteChangeRootPassword": "seul l’utilisateur « root » peut utiliser un mot de passe vide",
|
||||||
"NoteChapterEditorTimes": "Information: L'horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du Livre Audio.",
|
"NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du Livre Audio.",
|
||||||
"NoteFolderPicker": "Information: Les dossiers déjà surveillés ne sont pas affichés",
|
"NoteFolderPicker": "Information : Les dossiers déjà surveillés ne sont pas affichés",
|
||||||
"NoteFolderPickerDebian": "Information: La sélection de dossier sur une installation debian n'est pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.",
|
"NoteFolderPickerDebian": "Information : La sélection de dossier sur une installation debian n’est pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
|
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
|
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Si vous téléverser uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
|
"NoteUploaderOnlyAudioFiles": "Si vous téléverser uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
|
||||||
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d'élément sont ignorés.",
|
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d’élément sont ignorés.",
|
||||||
"PlaceholderNewCollection": "Nom de la nouvelle collection",
|
"PlaceholderNewCollection": "Nom de la nouvelle collection",
|
||||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||||
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
||||||
"PlaceholderSearch": "Recherche...",
|
"PlaceholderSearch": "Recherche...",
|
||||||
|
"PlaceholderSearchEpisode": "Search episode...",
|
||||||
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
||||||
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
||||||
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l'image",
|
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l’image",
|
||||||
"ToastAuthorImageRemoveSuccess": "Image de l'auteur supprimée",
|
"ToastAuthorImageRemoveSuccess": "Image de l’auteur supprimée",
|
||||||
"ToastAuthorUpdateFailed": "Échec de la mise à jour de l'auteur",
|
"ToastAuthorUpdateFailed": "Échec de la mise à jour de l’auteur",
|
||||||
"ToastAuthorUpdateMerged": "Auteur fusionné",
|
"ToastAuthorUpdateMerged": "Auteur fusionné",
|
||||||
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
|
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
|
||||||
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (pas d'image trouvée)",
|
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (aucune image trouvée)",
|
||||||
"ToastBackupCreateFailed": "Échec de la création de sauvegarde",
|
"ToastBackupCreateFailed": "Échec de la création de sauvegarde",
|
||||||
"ToastBackupCreateSuccess": "Sauvegarde créée",
|
"ToastBackupCreateSuccess": "Sauvegarde créée",
|
||||||
"ToastBackupDeleteFailed": "Échec de la suppression de sauvegarde",
|
"ToastBackupDeleteFailed": "Échec de la suppression de sauvegarde",
|
||||||
@@ -577,23 +600,23 @@
|
|||||||
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
||||||
"ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
|
"ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
|
||||||
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
||||||
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l'article",
|
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’article",
|
||||||
"ToastItemCoverUpdateSuccess": "Couverture de l'article mise à jour",
|
"ToastItemCoverUpdateSuccess": "Couverture de l’article mise à jour",
|
||||||
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l'article",
|
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’article",
|
||||||
"ToastItemDetailsUpdateSuccess": "Détails de l'article mis à jour",
|
"ToastItemDetailsUpdateSuccess": "Détails de l’article mis à jour",
|
||||||
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire pour les détails de l'article",
|
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire sur les détails de l’article",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Échec de l'annotation terminée",
|
"ToastItemMarkedAsFinishedFailed": "Échec de l’annotation terminée",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Échec de l'annotation non-terminée",
|
"ToastItemMarkedAsNotFinishedFailed": "Échec de l’annotation non-terminée",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
|
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
|
||||||
"ToastLibraryCreateFailed": "Échec de la création de bibliothèque",
|
"ToastLibraryCreateFailed": "Échec de la création de bibliothèque",
|
||||||
"ToastLibraryCreateSuccess": "Bibliothèque \"{0}\" créée",
|
"ToastLibraryCreateSuccess": "Bibliothèque « {0} » créée",
|
||||||
"ToastLibraryDeleteFailed": "Échec de la suppression de la bibliothèque",
|
"ToastLibraryDeleteFailed": "Échec de la suppression de la bibliothèque",
|
||||||
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
|
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
|
||||||
"ToastLibraryScanFailedToStart": "Échec du démarrage de l'analyse",
|
"ToastLibraryScanFailedToStart": "Échec du démarrage de l’analyse",
|
||||||
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
||||||
"ToastLibraryUpdateFailed": "Échec de la mise à jour de la bibliothèque",
|
"ToastLibraryUpdateFailed": "Échec de la mise à jour de la bibliothèque",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
"ToastLibraryUpdateSuccess": "Bibliothèque « {0} » mise à jour",
|
||||||
"ToastPlaylistCreateFailed": "Échec de la création de la liste de lecture",
|
"ToastPlaylistCreateFailed": "Échec de la création de la liste de lecture",
|
||||||
"ToastPlaylistCreateSuccess": "Liste de lecture créée",
|
"ToastPlaylistCreateSuccess": "Liste de lecture créée",
|
||||||
"ToastPlaylistRemoveFailed": "Échec de la suppression de la liste de lecture",
|
"ToastPlaylistRemoveFailed": "Échec de la suppression de la liste de lecture",
|
||||||
@@ -602,17 +625,17 @@
|
|||||||
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
|
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
|
||||||
"ToastPodcastCreateFailed": "Échec de la création du Podcast",
|
"ToastPodcastCreateFailed": "Échec de la création du Podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast créé",
|
"ToastPodcastCreateSuccess": "Podcast créé",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Échec de la suppression de l'article de la collection",
|
"ToastRemoveItemFromCollectionFailed": "Échec de la suppression de l’article de la collection",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||||
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||||
"ToastSeriesUpdateFailed": "Echec de la mise à jour de la série",
|
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
||||||
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||||
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||||
"ToastSessionDeleteSuccess": "Session supprimée",
|
"ToastSessionDeleteSuccess": "Session supprimée",
|
||||||
"ToastSocketConnected": "WebSocket connecté",
|
"ToastSocketConnected": "WebSocket connecté",
|
||||||
"ToastSocketDisconnected": "WebSocket déconnecté",
|
"ToastSocketDisconnected": "WebSocket déconnecté",
|
||||||
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||||
"ToastUserDeleteFailed": "Échec de la suppression de l'utilisateur",
|
"ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur",
|
||||||
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Napravi",
|
"ButtonCreate": "Napravi",
|
||||||
"ButtonCreateBackup": "Napravi backup",
|
"ButtonCreateBackup": "Napravi backup",
|
||||||
"ButtonDelete": "Obriši",
|
"ButtonDelete": "Obriši",
|
||||||
|
"ButtonDownloadQueue": "Queue",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Uredi poglavlja",
|
"ButtonEditChapters": "Uredi poglavlja",
|
||||||
"ButtonEditPodcast": "Uredi podcast",
|
"ButtonEditPodcast": "Uredi podcast",
|
||||||
@@ -92,7 +93,9 @@
|
|||||||
"HeaderCollection": "Kolekcija",
|
"HeaderCollection": "Kolekcija",
|
||||||
"HeaderCollectionItems": "Stvari u kolekciji",
|
"HeaderCollectionItems": "Stvari u kolekciji",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Detalji",
|
"HeaderDetails": "Detalji",
|
||||||
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEpisodes": "Epizode",
|
"HeaderEpisodes": "Epizode",
|
||||||
"HeaderFiles": "Datoteke",
|
"HeaderFiles": "Datoteke",
|
||||||
"HeaderFindChapters": "Pronađi poglavlja",
|
"HeaderFindChapters": "Pronađi poglavlja",
|
||||||
@@ -126,6 +129,7 @@
|
|||||||
"HeaderPreviewCover": "Pregledaj Cover",
|
"HeaderPreviewCover": "Pregledaj Cover",
|
||||||
"HeaderRemoveEpisode": "Ukloni epizodu",
|
"HeaderRemoveEpisode": "Ukloni epizodu",
|
||||||
"HeaderRemoveEpisodes": "Ukloni {0} epizoda/-e",
|
"HeaderRemoveEpisodes": "Ukloni {0} epizoda/-e",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed je otvoren",
|
"HeaderRSSFeedIsOpen": "RSS Feed je otvoren",
|
||||||
"HeaderSavedMediaProgress": "Spremljen Media Progress",
|
"HeaderSavedMediaProgress": "Spremljen Media Progress",
|
||||||
"HeaderSchedule": "Schedule",
|
"HeaderSchedule": "Schedule",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"HeaderSettingsGeneral": "Opčenito",
|
"HeaderSettingsGeneral": "Opčenito",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Sleep Timer",
|
"HeaderSleepTimer": "Sleep Timer",
|
||||||
|
"HeaderStatsLargestItems": "Largest Items",
|
||||||
"HeaderStatsLongestItems": "Najduže stavke (sati)",
|
"HeaderStatsLongestItems": "Najduže stavke (sati)",
|
||||||
"HeaderStatsMinutesListeningChart": "Minuta odslušanih (posljednjih 7 dana)",
|
"HeaderStatsMinutesListeningChart": "Minuta odslušanih (posljednjih 7 dana)",
|
||||||
"HeaderStatsRecentSessions": "Nedavne sesije",
|
"HeaderStatsRecentSessions": "Nedavne sesije",
|
||||||
@@ -150,6 +155,7 @@
|
|||||||
"HeaderUpdateLibrary": "Aktualiziraj biblioteku",
|
"HeaderUpdateLibrary": "Aktualiziraj biblioteku",
|
||||||
"HeaderUsers": "Korinici",
|
"HeaderUsers": "Korinici",
|
||||||
"HeaderYourStats": "Tvoja statistika",
|
"HeaderYourStats": "Tvoja statistika",
|
||||||
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Vrsta korisničkog računa",
|
"LabelAccountType": "Vrsta korisničkog računa",
|
||||||
"LabelAccountTypeAdmin": "Administrator",
|
"LabelAccountTypeAdmin": "Administrator",
|
||||||
"LabelAccountTypeGuest": "Gost",
|
"LabelAccountTypeGuest": "Gost",
|
||||||
@@ -162,6 +168,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "All",
|
||||||
"LabelAllUsers": "Svi korisnici",
|
"LabelAllUsers": "Svi korisnici",
|
||||||
|
"LabelAlreadyInYourLibrary": "Already in your library",
|
||||||
"LabelAppend": "Append",
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Author (First Last)",
|
"LabelAuthorFirstLast": "Author (First Last)",
|
||||||
@@ -192,6 +199,7 @@
|
|||||||
"LabelCronExpression": "Cron Expression",
|
"LabelCronExpression": "Cron Expression",
|
||||||
"LabelCurrent": "Trenutan",
|
"LabelCurrent": "Trenutan",
|
||||||
"LabelCurrently": "Trenutno:",
|
"LabelCurrently": "Trenutno:",
|
||||||
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Datetime",
|
"LabelDatetime": "Datetime",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
"LabelDeselectAll": "Odznači sve",
|
"LabelDeselectAll": "Odznači sve",
|
||||||
@@ -209,6 +217,7 @@
|
|||||||
"LabelEpisode": "Epizoda",
|
"LabelEpisode": "Epizoda",
|
||||||
"LabelEpisodeTitle": "Naslov epizode",
|
"LabelEpisodeTitle": "Naslov epizode",
|
||||||
"LabelEpisodeType": "Vrsta epizode",
|
"LabelEpisodeType": "Vrsta epizode",
|
||||||
|
"LabelExample": "Example",
|
||||||
"LabelExplicit": "Explicit",
|
"LabelExplicit": "Explicit",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datoteka",
|
"LabelFile": "Datoteka",
|
||||||
@@ -270,6 +279,8 @@
|
|||||||
"LabelNewestAuthors": "Najnoviji autori",
|
"LabelNewestAuthors": "Najnoviji autori",
|
||||||
"LabelNewestEpisodes": "Najnovije epizode",
|
"LabelNewestEpisodes": "Najnovije epizode",
|
||||||
"LabelNewPassword": "Nova lozinka",
|
"LabelNewPassword": "Nova lozinka",
|
||||||
|
"LabelNextBackupDate": "Next backup date",
|
||||||
|
"LabelNextScheduledRun": "Next scheduled run",
|
||||||
"LabelNotes": "Bilješke",
|
"LabelNotes": "Bilješke",
|
||||||
"LabelNotFinished": "Nedovršeno",
|
"LabelNotFinished": "Nedovršeno",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -300,7 +311,9 @@
|
|||||||
"LabelPlayMethod": "Vrsta reprodukcije",
|
"LabelPlayMethod": "Vrsta reprodukcije",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
|
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
|
||||||
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Napredak",
|
"LabelProgress": "Napredak",
|
||||||
"LabelProvider": "Dobavljač",
|
"LabelProvider": "Dobavljač",
|
||||||
"LabelPubDate": "Datam izdavanja",
|
"LabelPubDate": "Datam izdavanja",
|
||||||
@@ -312,7 +325,10 @@
|
|||||||
"LabelRegion": "Regija",
|
"LabelRegion": "Regija",
|
||||||
"LabelReleaseDate": "Datum izlaska",
|
"LabelReleaseDate": "Datum izlaska",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Open",
|
"LabelRSSFeedOpen": "RSS Feed Open",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Traži pojam",
|
"LabelSearchTerm": "Traži pojam",
|
||||||
@@ -357,6 +373,7 @@
|
|||||||
"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",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku",
|
"LabelSettingsStoreMetadataWithItem": "Spremi metapodatke uz stavku",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke. Koristi .abs ekstenziju.",
|
"LabelSettingsStoreMetadataWithItemHelp": "Po defaultu metapodatci su spremljeni u /metadata/items, uključujućite li ovu postavku, metapodatci će biti spremljeni u folderima od biblioteke. Koristi .abs ekstenziju.",
|
||||||
|
"LabelSettingsTimeFormat": "Time Format",
|
||||||
"LabelShowAll": "Prikaži sve",
|
"LabelShowAll": "Prikaži sve",
|
||||||
"LabelSize": "Veličina",
|
"LabelSize": "Veličina",
|
||||||
"LabelSleepTimer": "Sleep timer",
|
"LabelSleepTimer": "Sleep timer",
|
||||||
@@ -384,6 +401,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
||||||
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeListened": "Vremena odslušano",
|
"LabelTimeListened": "Vremena odslušano",
|
||||||
"LabelTimeListenedToday": "Vremena odslušano danas",
|
"LabelTimeListenedToday": "Vremena odslušano danas",
|
||||||
"LabelTimeRemaining": "{0} preostalo",
|
"LabelTimeRemaining": "{0} preostalo",
|
||||||
@@ -403,6 +421,7 @@
|
|||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Tip",
|
"LabelType": "Tip",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "Nepoznato",
|
"LabelUnknown": "Nepoznato",
|
||||||
"LabelUpdateCover": "Aktualiziraj Cover",
|
"LabelUpdateCover": "Aktualiziraj Cover",
|
||||||
"LabelUpdateCoverHelp": "Dozvoli postavljanje novog covera za odabrane knjige nakon što je match pronađen.",
|
"LabelUpdateCoverHelp": "Dozvoli postavljanje novog covera za odabrane knjige nakon što je match pronađen.",
|
||||||
@@ -485,6 +504,8 @@
|
|||||||
"MessageNoCollections": "Nema kolekcija",
|
"MessageNoCollections": "Nema kolekcija",
|
||||||
"MessageNoCoversFound": "Covers nisu pronađeni",
|
"MessageNoCoversFound": "Covers nisu pronađeni",
|
||||||
"MessageNoDescription": "Nema opisa",
|
"MessageNoDescription": "Nema opisa",
|
||||||
|
"MessageNoDownloadsInProgress": "No downloads currently in progress",
|
||||||
|
"MessageNoDownloadsQueued": "No downloads queued",
|
||||||
"MessageNoEpisodeMatchesFound": "Nijedna epizoda pronađena",
|
"MessageNoEpisodeMatchesFound": "Nijedna epizoda pronađena",
|
||||||
"MessageNoEpisodes": "Nema epizoda",
|
"MessageNoEpisodes": "Nema epizoda",
|
||||||
"MessageNoFoldersAvailable": "Nema dostupnih foldera",
|
"MessageNoFoldersAvailable": "Nema dostupnih foldera",
|
||||||
@@ -501,6 +522,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
|
"MessageNoSearchResultsFor": "Nema rezultata pretragee za \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "No Series",
|
||||||
"MessageNoTags": "No Tags",
|
"MessageNoTags": "No Tags",
|
||||||
|
"MessageNoTasksRunning": "No Tasks Running",
|
||||||
"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",
|
||||||
@@ -546,6 +568,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Nova folder putanja",
|
"PlaceholderNewFolderPath": "Nova folder putanja",
|
||||||
"PlaceholderNewPlaylist": "New playlist name",
|
"PlaceholderNewPlaylist": "New playlist name",
|
||||||
"PlaceholderSearch": "Traži...",
|
"PlaceholderSearch": "Traži...",
|
||||||
|
"PlaceholderSearchEpisode": "Search episode...",
|
||||||
"ToastAccountUpdateFailed": "Neuspješno aktualiziranje korisničkog računa",
|
"ToastAccountUpdateFailed": "Neuspješno aktualiziranje korisničkog računa",
|
||||||
"ToastAccountUpdateSuccess": "Korisnički račun aktualiziran",
|
"ToastAccountUpdateSuccess": "Korisnički račun aktualiziran",
|
||||||
"ToastAuthorImageRemoveFailed": "Neuspješno uklanjanje slike",
|
"ToastAuthorImageRemoveFailed": "Neuspješno uklanjanje slike",
|
||||||
|
|||||||
+29
-6
@@ -20,7 +20,8 @@
|
|||||||
"ButtonCreate": "Crea",
|
"ButtonCreate": "Crea",
|
||||||
"ButtonCreateBackup": "Crea un Backup",
|
"ButtonCreateBackup": "Crea un Backup",
|
||||||
"ButtonDelete": "Elimina",
|
"ButtonDelete": "Elimina",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonDownloadQueue": "Coda",
|
||||||
|
"ButtonEdit": "Modifica",
|
||||||
"ButtonEditChapters": "Modifica Capitoli",
|
"ButtonEditChapters": "Modifica Capitoli",
|
||||||
"ButtonEditPodcast": "Modifica Podcast",
|
"ButtonEditPodcast": "Modifica Podcast",
|
||||||
"ButtonForceReScan": "Forza Re-Scan",
|
"ButtonForceReScan": "Forza Re-Scan",
|
||||||
@@ -92,7 +93,9 @@
|
|||||||
"HeaderCollection": "Raccolta",
|
"HeaderCollection": "Raccolta",
|
||||||
"HeaderCollectionItems": "Elementi della Raccolta",
|
"HeaderCollectionItems": "Elementi della Raccolta",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Cover",
|
||||||
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Dettagli",
|
"HeaderDetails": "Dettagli",
|
||||||
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEpisodes": "Episodi",
|
"HeaderEpisodes": "Episodi",
|
||||||
"HeaderFiles": "File",
|
"HeaderFiles": "File",
|
||||||
"HeaderFindChapters": "Trova Capitoli",
|
"HeaderFindChapters": "Trova Capitoli",
|
||||||
@@ -126,6 +129,7 @@
|
|||||||
"HeaderPreviewCover": "Anteprima Cover",
|
"HeaderPreviewCover": "Anteprima Cover",
|
||||||
"HeaderRemoveEpisode": "Rimuovi Episodi",
|
"HeaderRemoveEpisode": "Rimuovi Episodi",
|
||||||
"HeaderRemoveEpisodes": "Rimuovi {0} Episodi",
|
"HeaderRemoveEpisodes": "Rimuovi {0} Episodi",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
||||||
"HeaderSavedMediaProgress": "Progressi salvati",
|
"HeaderSavedMediaProgress": "Progressi salvati",
|
||||||
"HeaderSchedule": "Schedula",
|
"HeaderSchedule": "Schedula",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"HeaderSettingsGeneral": "Generale",
|
"HeaderSettingsGeneral": "Generale",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Sveglia",
|
"HeaderSleepTimer": "Sveglia",
|
||||||
|
"HeaderStatsLargestItems": "Largest Items",
|
||||||
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
"HeaderStatsLongestItems": "libri più lunghi (ore)",
|
||||||
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
|
"HeaderStatsMinutesListeningChart": "Minuti ascoltati (Ultimi 7 Giorni)",
|
||||||
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
"HeaderStatsRecentSessions": "Sessioni Recenti",
|
||||||
@@ -150,6 +155,7 @@
|
|||||||
"HeaderUpdateLibrary": "Aggiorna Libreria",
|
"HeaderUpdateLibrary": "Aggiorna Libreria",
|
||||||
"HeaderUsers": "Utenti",
|
"HeaderUsers": "Utenti",
|
||||||
"HeaderYourStats": "Statistiche Personali",
|
"HeaderYourStats": "Statistiche Personali",
|
||||||
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Tipo di Account",
|
"LabelAccountType": "Tipo di Account",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Ospite",
|
"LabelAccountTypeGuest": "Ospite",
|
||||||
@@ -162,6 +168,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
||||||
"LabelAll": "Tutti",
|
"LabelAll": "Tutti",
|
||||||
"LabelAllUsers": "Tutti gli Utenti",
|
"LabelAllUsers": "Tutti gli Utenti",
|
||||||
|
"LabelAlreadyInYourLibrary": "Già esistente nella libreria",
|
||||||
"LabelAppend": "Appese",
|
"LabelAppend": "Appese",
|
||||||
"LabelAuthor": "Autore",
|
"LabelAuthor": "Autore",
|
||||||
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
||||||
@@ -192,6 +199,7 @@
|
|||||||
"LabelCronExpression": "Espressione Cron",
|
"LabelCronExpression": "Espressione Cron",
|
||||||
"LabelCurrent": "Attuale",
|
"LabelCurrent": "Attuale",
|
||||||
"LabelCurrently": "Attualmente:",
|
"LabelCurrently": "Attualmente:",
|
||||||
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Data & Ora",
|
"LabelDatetime": "Data & Ora",
|
||||||
"LabelDescription": "Descrizione",
|
"LabelDescription": "Descrizione",
|
||||||
"LabelDeselectAll": "Deseleziona Tutto",
|
"LabelDeselectAll": "Deseleziona Tutto",
|
||||||
@@ -209,6 +217,7 @@
|
|||||||
"LabelEpisode": "Episodio",
|
"LabelEpisode": "Episodio",
|
||||||
"LabelEpisodeTitle": "Titolo Episodio",
|
"LabelEpisodeTitle": "Titolo Episodio",
|
||||||
"LabelEpisodeType": "Tipo Episodio",
|
"LabelEpisodeType": "Tipo Episodio",
|
||||||
|
"LabelExample": "Example",
|
||||||
"LabelExplicit": "Esplicito",
|
"LabelExplicit": "Esplicito",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "File",
|
"LabelFile": "File",
|
||||||
@@ -230,7 +239,7 @@
|
|||||||
"LabelInProgress": "In Corso",
|
"LabelInProgress": "In Corso",
|
||||||
"LabelInterval": "Intervallo",
|
"LabelInterval": "Intervallo",
|
||||||
"LabelIntervalCustomDailyWeekly": "Personalizza giorni/settimane",
|
"LabelIntervalCustomDailyWeekly": "Personalizza giorni/settimane",
|
||||||
"LabelIntervalEvery12Hours": "EOgni 12 Ore",
|
"LabelIntervalEvery12Hours": "Ogni 12 Ore",
|
||||||
"LabelIntervalEvery15Minutes": "Ogni 15 Minuti",
|
"LabelIntervalEvery15Minutes": "Ogni 15 Minuti",
|
||||||
"LabelIntervalEvery2Hours": "Ogni 2 Ore",
|
"LabelIntervalEvery2Hours": "Ogni 2 Ore",
|
||||||
"LabelIntervalEvery30Minutes": "Ogni 30 Minuti",
|
"LabelIntervalEvery30Minutes": "Ogni 30 Minuti",
|
||||||
@@ -270,6 +279,8 @@
|
|||||||
"LabelNewestAuthors": "Autori Recenti",
|
"LabelNewestAuthors": "Autori Recenti",
|
||||||
"LabelNewestEpisodes": "Episodi Recenti",
|
"LabelNewestEpisodes": "Episodi Recenti",
|
||||||
"LabelNewPassword": "Nuova Password",
|
"LabelNewPassword": "Nuova Password",
|
||||||
|
"LabelNextBackupDate": "Data Prossimo Backup",
|
||||||
|
"LabelNextScheduledRun": "Data prossima esecuzione schedulata",
|
||||||
"LabelNotes": "Note",
|
"LabelNotes": "Note",
|
||||||
"LabelNotFinished": "Da Completare",
|
"LabelNotFinished": "Da Completare",
|
||||||
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
"LabelNotificationAppriseURL": "Apprendi URL(s)",
|
||||||
@@ -285,7 +296,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",
|
"LabelOverwrite": "Sovrascrivi",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Percorso",
|
"LabelPath": "Percorso",
|
||||||
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
"LabelPermissionsAccessAllLibraries": "Può accedere a tutte le librerie",
|
||||||
@@ -300,7 +311,9 @@
|
|||||||
"LabelPlayMethod": "Metodo di riproduzione",
|
"LabelPlayMethod": "Metodo di riproduzione",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastType": "Timo di Podcast",
|
||||||
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||||
|
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
||||||
"LabelProgress": "Cominciati",
|
"LabelProgress": "Cominciati",
|
||||||
"LabelProvider": "Provider",
|
"LabelProvider": "Provider",
|
||||||
"LabelPubDate": "Data Pubblicazione",
|
"LabelPubDate": "Data Pubblicazione",
|
||||||
@@ -308,11 +321,14 @@
|
|||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Raccomandati",
|
||||||
"LabelRegion": "Regione",
|
"LabelRegion": "Regione",
|
||||||
"LabelReleaseDate": "Data Release",
|
"LabelReleaseDate": "Data Release",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Rimuovi cover",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Aperto",
|
"LabelRSSFeedOpen": "RSS Feed Aperto",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Ricerca",
|
"LabelSearchTerm": "Ricerca",
|
||||||
@@ -357,6 +373,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"",
|
"LabelSettingsStoreCoversWithItemHelp": "Di default, le immagini di copertina sono salvate dentro /metadata/items, abilitando questa opzione le copertine saranno archiviate nella cartella della libreria corrispondente. Verrà conservato solo un file denominato \"cover\"",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file",
|
"LabelSettingsStoreMetadataWithItem": "Archivia i metadata con il file",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs",
|
"LabelSettingsStoreMetadataWithItemHelp": "Di default, i metadati sono salvati dentro /metadata/items, abilitando questa opzione si memorizzeranno i metadata nella cartella della libreria. I file avranno estensione .abs",
|
||||||
|
"LabelSettingsTimeFormat": "Formato Ora",
|
||||||
"LabelShowAll": "Mostra Tutto",
|
"LabelShowAll": "Mostra Tutto",
|
||||||
"LabelSize": "Dimensione",
|
"LabelSize": "Dimensione",
|
||||||
"LabelSleepTimer": "Sleep timer",
|
"LabelSleepTimer": "Sleep timer",
|
||||||
@@ -384,6 +401,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||||
|
"LabelTasks": "Processi in esecuzione",
|
||||||
"LabelTimeListened": "Tempo di Ascolto",
|
"LabelTimeListened": "Tempo di Ascolto",
|
||||||
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
||||||
"LabelTimeRemaining": "{0} rimanente",
|
"LabelTimeRemaining": "{0} rimanente",
|
||||||
@@ -403,6 +421,7 @@
|
|||||||
"LabelTracksMultiTrack": "Multi-traccia",
|
"LabelTracksMultiTrack": "Multi-traccia",
|
||||||
"LabelTracksSingleTrack": "Traccia-singola",
|
"LabelTracksSingleTrack": "Traccia-singola",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "Sconosciuto",
|
"LabelUnknown": "Sconosciuto",
|
||||||
"LabelUpdateCover": "Aggiornamento Cover",
|
"LabelUpdateCover": "Aggiornamento Cover",
|
||||||
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
|
"LabelUpdateCoverHelp": "Consenti la sovrascrittura delle copertine esistenti per i libri selezionati quando viene trovata una corrispondenza",
|
||||||
@@ -485,6 +504,8 @@
|
|||||||
"MessageNoCollections": "Nessuna Raccolta",
|
"MessageNoCollections": "Nessuna Raccolta",
|
||||||
"MessageNoCoversFound": "Nessuna Cover Trovata",
|
"MessageNoCoversFound": "Nessuna Cover Trovata",
|
||||||
"MessageNoDescription": "Nessuna descrizione",
|
"MessageNoDescription": "Nessuna descrizione",
|
||||||
|
"MessageNoDownloadsInProgress": "Nessun download attualmente in corso",
|
||||||
|
"MessageNoDownloadsQueued": "Nessuna coda di download",
|
||||||
"MessageNoEpisodeMatchesFound": "Nessun episodio corrispondente trovato",
|
"MessageNoEpisodeMatchesFound": "Nessun episodio corrispondente trovato",
|
||||||
"MessageNoEpisodes": "Nessun Episodio",
|
"MessageNoEpisodes": "Nessun Episodio",
|
||||||
"MessageNoFoldersAvailable": "Nessuna Cartella disponibile",
|
"MessageNoFoldersAvailable": "Nessuna Cartella disponibile",
|
||||||
@@ -501,6 +522,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
|
||||||
"MessageNoSeries": "Nessuna Serie",
|
"MessageNoSeries": "Nessuna Serie",
|
||||||
"MessageNoTags": "No Tags",
|
"MessageNoTags": "No Tags",
|
||||||
|
"MessageNoTasksRunning": "Nessun processo in esecuzione",
|
||||||
"MessageNotYetImplemented": "Non Ancora Implementato",
|
"MessageNotYetImplemented": "Non Ancora Implementato",
|
||||||
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
|
||||||
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
|
||||||
@@ -546,6 +568,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
"PlaceholderNewFolderPath": "Nuovo percorso Cartella",
|
||||||
"PlaceholderNewPlaylist": "Nome nuova playlist",
|
"PlaceholderNewPlaylist": "Nome nuova playlist",
|
||||||
"PlaceholderSearch": "Cerca..",
|
"PlaceholderSearch": "Cerca..",
|
||||||
|
"PlaceholderSearchEpisode": "Cerca Episodio..",
|
||||||
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
"ToastAccountUpdateFailed": "Aggiornamento Account Fallito",
|
||||||
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
"ToastAccountUpdateSuccess": "Account Aggiornato",
|
||||||
"ToastAuthorImageRemoveFailed": "Rimozione immagine autore Fallita",
|
"ToastAuthorImageRemoveFailed": "Rimozione immagine autore Fallita",
|
||||||
@@ -606,7 +629,7 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||||
"ToastSeriesUpdateFailed": "Aggiornaemnto Serie Fallito",
|
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
|
||||||
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Utwórz",
|
"ButtonCreate": "Utwórz",
|
||||||
"ButtonCreateBackup": "Utwórz kopię zapasową",
|
"ButtonCreateBackup": "Utwórz kopię zapasową",
|
||||||
"ButtonDelete": "Usuń",
|
"ButtonDelete": "Usuń",
|
||||||
|
"ButtonDownloadQueue": "Queue",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edytuj rozdziały",
|
"ButtonEditChapters": "Edytuj rozdziały",
|
||||||
"ButtonEditPodcast": "Edytuj podcast",
|
"ButtonEditPodcast": "Edytuj podcast",
|
||||||
@@ -92,7 +93,9 @@
|
|||||||
"HeaderCollection": "Kolekcja",
|
"HeaderCollection": "Kolekcja",
|
||||||
"HeaderCollectionItems": "Elementy kolekcji",
|
"HeaderCollectionItems": "Elementy kolekcji",
|
||||||
"HeaderCover": "Okładka",
|
"HeaderCover": "Okładka",
|
||||||
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Szczegóły",
|
"HeaderDetails": "Szczegóły",
|
||||||
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
"HeaderEpisodes": "Rozdziały",
|
"HeaderEpisodes": "Rozdziały",
|
||||||
"HeaderFiles": "Pliki",
|
"HeaderFiles": "Pliki",
|
||||||
"HeaderFindChapters": "Wyszukaj rozdziały",
|
"HeaderFindChapters": "Wyszukaj rozdziały",
|
||||||
@@ -126,6 +129,7 @@
|
|||||||
"HeaderPreviewCover": "Podgląd okładki",
|
"HeaderPreviewCover": "Podgląd okładki",
|
||||||
"HeaderRemoveEpisode": "Usuń odcinek",
|
"HeaderRemoveEpisode": "Usuń odcinek",
|
||||||
"HeaderRemoveEpisodes": "Usuń {0} odcinków",
|
"HeaderRemoveEpisodes": "Usuń {0} odcinków",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
|
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
|
||||||
"HeaderSavedMediaProgress": "Zapisany postęp",
|
"HeaderSavedMediaProgress": "Zapisany postęp",
|
||||||
"HeaderSchedule": "Harmonogram",
|
"HeaderSchedule": "Harmonogram",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"HeaderSettingsGeneral": "Ogólne",
|
"HeaderSettingsGeneral": "Ogólne",
|
||||||
"HeaderSettingsScanner": "Skanowanie",
|
"HeaderSettingsScanner": "Skanowanie",
|
||||||
"HeaderSleepTimer": "Wyłącznik czasowy",
|
"HeaderSleepTimer": "Wyłącznik czasowy",
|
||||||
|
"HeaderStatsLargestItems": "Largest Items",
|
||||||
"HeaderStatsLongestItems": "Najdłuższe pozycje (hrs)",
|
"HeaderStatsLongestItems": "Najdłuższe pozycje (hrs)",
|
||||||
"HeaderStatsMinutesListeningChart": "Czas słuchania w minutach (ostatnie 7 dni)",
|
"HeaderStatsMinutesListeningChart": "Czas słuchania w minutach (ostatnie 7 dni)",
|
||||||
"HeaderStatsRecentSessions": "Ostatnie sesje",
|
"HeaderStatsRecentSessions": "Ostatnie sesje",
|
||||||
@@ -150,6 +155,7 @@
|
|||||||
"HeaderUpdateLibrary": "Zaktualizuj bibliotekę",
|
"HeaderUpdateLibrary": "Zaktualizuj bibliotekę",
|
||||||
"HeaderUsers": "Użytkownicy",
|
"HeaderUsers": "Użytkownicy",
|
||||||
"HeaderYourStats": "Twoje statystyki",
|
"HeaderYourStats": "Twoje statystyki",
|
||||||
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "Typ konta",
|
"LabelAccountType": "Typ konta",
|
||||||
"LabelAccountTypeAdmin": "Administrator",
|
"LabelAccountTypeAdmin": "Administrator",
|
||||||
"LabelAccountTypeGuest": "Gość",
|
"LabelAccountTypeGuest": "Gość",
|
||||||
@@ -162,6 +168,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",
|
||||||
|
"LabelAlreadyInYourLibrary": "Already in your library",
|
||||||
"LabelAppend": "Append",
|
"LabelAppend": "Append",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Rosnąco)",
|
"LabelAuthorFirstLast": "Autor (Rosnąco)",
|
||||||
@@ -192,6 +199,7 @@
|
|||||||
"LabelCronExpression": "Wyrażenie CRON",
|
"LabelCronExpression": "Wyrażenie CRON",
|
||||||
"LabelCurrent": "Aktualny",
|
"LabelCurrent": "Aktualny",
|
||||||
"LabelCurrently": "Obecnie:",
|
"LabelCurrently": "Obecnie:",
|
||||||
|
"LabelCustomCronExpression": "Custom Cron Expression:",
|
||||||
"LabelDatetime": "Data i godzina",
|
"LabelDatetime": "Data i godzina",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
"LabelDeselectAll": "Odznacz wszystko",
|
"LabelDeselectAll": "Odznacz wszystko",
|
||||||
@@ -209,6 +217,7 @@
|
|||||||
"LabelEpisode": "Odcinek",
|
"LabelEpisode": "Odcinek",
|
||||||
"LabelEpisodeTitle": "Tytuł odcinka",
|
"LabelEpisodeTitle": "Tytuł odcinka",
|
||||||
"LabelEpisodeType": "Typ odcinka",
|
"LabelEpisodeType": "Typ odcinka",
|
||||||
|
"LabelExample": "Example",
|
||||||
"LabelExplicit": "Nieprzyzwoite",
|
"LabelExplicit": "Nieprzyzwoite",
|
||||||
"LabelFeedURL": "URL kanału",
|
"LabelFeedURL": "URL kanału",
|
||||||
"LabelFile": "Plik",
|
"LabelFile": "Plik",
|
||||||
@@ -270,6 +279,8 @@
|
|||||||
"LabelNewestAuthors": "Najnowsi autorzy",
|
"LabelNewestAuthors": "Najnowsi autorzy",
|
||||||
"LabelNewestEpisodes": "Najnowsze odcinki",
|
"LabelNewestEpisodes": "Najnowsze odcinki",
|
||||||
"LabelNewPassword": "Nowe hasło",
|
"LabelNewPassword": "Nowe hasło",
|
||||||
|
"LabelNextBackupDate": "Next backup date",
|
||||||
|
"LabelNextScheduledRun": "Next scheduled run",
|
||||||
"LabelNotes": "Uwagi",
|
"LabelNotes": "Uwagi",
|
||||||
"LabelNotFinished": "Nieukończone",
|
"LabelNotFinished": "Nieukończone",
|
||||||
"LabelNotificationAppriseURL": "URLe Apprise",
|
"LabelNotificationAppriseURL": "URLe Apprise",
|
||||||
@@ -300,7 +311,9 @@
|
|||||||
"LabelPlayMethod": "Metoda odtwarzania",
|
"LabelPlayMethod": "Metoda odtwarzania",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasty",
|
"LabelPodcasts": "Podcasty",
|
||||||
|
"LabelPodcastType": "Podcast Type",
|
||||||
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
|
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
|
||||||
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Postęp",
|
"LabelProgress": "Postęp",
|
||||||
"LabelProvider": "Dostawca",
|
"LabelProvider": "Dostawca",
|
||||||
"LabelPubDate": "Data publikacji",
|
"LabelPubDate": "Data publikacji",
|
||||||
@@ -312,7 +325,10 @@
|
|||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Data wydania",
|
"LabelReleaseDate": "Data wydania",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
||||||
"LabelRSSFeedOpen": "RSS Feed otwarty",
|
"LabelRSSFeedOpen": "RSS Feed otwarty",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
"LabelRSSFeedURL": "URL kanały RSS",
|
"LabelRSSFeedURL": "URL kanały RSS",
|
||||||
"LabelSearchTerm": "Wyszukiwanie frazy",
|
"LabelSearchTerm": "Wyszukiwanie frazy",
|
||||||
@@ -357,6 +373,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "Domyślnie okładki są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana.",
|
"LabelSettingsStoreCoversWithItemHelp": "Domyślnie okładki są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Przechowuj metadane w folderze książki",
|
"LabelSettingsStoreMetadataWithItem": "Przechowuj metadane w folderze książki",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana. Rozszerzenie pliku metadanych: .abs",
|
"LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana. Rozszerzenie pliku metadanych: .abs",
|
||||||
|
"LabelSettingsTimeFormat": "Time Format",
|
||||||
"LabelShowAll": "Pokaż wszystko",
|
"LabelShowAll": "Pokaż wszystko",
|
||||||
"LabelSize": "Rozmiar",
|
"LabelSize": "Rozmiar",
|
||||||
"LabelSleepTimer": "Wyłącznik czasowy",
|
"LabelSleepTimer": "Wyłącznik czasowy",
|
||||||
@@ -384,6 +401,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tagi",
|
"LabelTags": "Tagi",
|
||||||
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
||||||
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeListened": "Czas odtwarzania",
|
"LabelTimeListened": "Czas odtwarzania",
|
||||||
"LabelTimeListenedToday": "Czas odtwarzania dzisiaj",
|
"LabelTimeListenedToday": "Czas odtwarzania dzisiaj",
|
||||||
"LabelTimeRemaining": "Pozostało {0}",
|
"LabelTimeRemaining": "Pozostało {0}",
|
||||||
@@ -403,6 +421,7 @@
|
|||||||
"LabelTracksMultiTrack": "Multi-track",
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
"LabelTracksSingleTrack": "Single-track",
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
"LabelType": "Typ",
|
"LabelType": "Typ",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "Nieznany",
|
"LabelUnknown": "Nieznany",
|
||||||
"LabelUpdateCover": "Zaktalizuj odkładkę",
|
"LabelUpdateCover": "Zaktalizuj odkładkę",
|
||||||
"LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania",
|
"LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania",
|
||||||
@@ -485,6 +504,8 @@
|
|||||||
"MessageNoCollections": "Brak kolekcji",
|
"MessageNoCollections": "Brak kolekcji",
|
||||||
"MessageNoCoversFound": "Okładki nieznalezione",
|
"MessageNoCoversFound": "Okładki nieznalezione",
|
||||||
"MessageNoDescription": "Brak opisu",
|
"MessageNoDescription": "Brak opisu",
|
||||||
|
"MessageNoDownloadsInProgress": "No downloads currently in progress",
|
||||||
|
"MessageNoDownloadsQueued": "No downloads queued",
|
||||||
"MessageNoEpisodeMatchesFound": "Nie znaleziono pasujących odcinków",
|
"MessageNoEpisodeMatchesFound": "Nie znaleziono pasujących odcinków",
|
||||||
"MessageNoEpisodes": "Brak odcinków",
|
"MessageNoEpisodes": "Brak odcinków",
|
||||||
"MessageNoFoldersAvailable": "Brak dostępnych folderów",
|
"MessageNoFoldersAvailable": "Brak dostępnych folderów",
|
||||||
@@ -501,6 +522,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
|
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
|
||||||
"MessageNoSeries": "No Series",
|
"MessageNoSeries": "No Series",
|
||||||
"MessageNoTags": "No Tags",
|
"MessageNoTags": "No Tags",
|
||||||
|
"MessageNoTasksRunning": "No Tasks Running",
|
||||||
"MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
|
"MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
|
||||||
"MessageNoUpdateNecessary": "Brak konieczności aktualizacji",
|
"MessageNoUpdateNecessary": "Brak konieczności aktualizacji",
|
||||||
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
|
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
|
||||||
@@ -546,6 +568,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Nowa ścieżka folderu",
|
"PlaceholderNewFolderPath": "Nowa ścieżka folderu",
|
||||||
"PlaceholderNewPlaylist": "New playlist name",
|
"PlaceholderNewPlaylist": "New playlist name",
|
||||||
"PlaceholderSearch": "Szukanie..",
|
"PlaceholderSearch": "Szukanie..",
|
||||||
|
"PlaceholderSearchEpisode": "Search episode..",
|
||||||
"ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
|
"ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
|
||||||
"ToastAccountUpdateSuccess": "Zaktualizowano konto",
|
"ToastAccountUpdateSuccess": "Zaktualizowano konto",
|
||||||
"ToastAuthorImageRemoveFailed": "Nie udało się usunąć obrazu",
|
"ToastAuthorImageRemoveFailed": "Nie udało się usunąć obrazu",
|
||||||
|
|||||||
+215
-192
@@ -1,30 +1,31 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Добавить",
|
"ButtonAdd": "Добавить",
|
||||||
"ButtonAddChapters": "Добавить Главы",
|
"ButtonAddChapters": "Добавить главы",
|
||||||
"ButtonAddPodcasts": "Добавить Подкасты",
|
"ButtonAddPodcasts": "Добавить подкасты",
|
||||||
"ButtonAddYourFirstLibrary": "Добавьте Вашу первую библиотеку",
|
"ButtonAddYourFirstLibrary": "Добавьте Вашу первую библиотеку",
|
||||||
"ButtonApply": "Применить",
|
"ButtonApply": "Применить",
|
||||||
"ButtonApplyChapters": "Применить Главы",
|
"ButtonApplyChapters": "Применить главы",
|
||||||
"ButtonAuthors": "Авторы",
|
"ButtonAuthors": "Авторы",
|
||||||
"ButtonBrowseForFolder": "Выбрать Папку",
|
"ButtonBrowseForFolder": "Выбрать папку",
|
||||||
"ButtonCancel": "Отмена",
|
"ButtonCancel": "Отмена",
|
||||||
"ButtonCancelEncode": "Отменить Кодирование",
|
"ButtonCancelEncode": "Отменить кодирование",
|
||||||
"ButtonChangeRootPassword": "Поменять Мастер Пароль",
|
"ButtonChangeRootPassword": "Поменять мастер пароль",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Проверка и Загрузка Новых Эпизодов",
|
"ButtonCheckAndDownloadNewEpisodes": "Проверка и Загрузка новых эпизодов",
|
||||||
"ButtonChooseAFolder": "Выбор папки",
|
"ButtonChooseAFolder": "Выбор папки",
|
||||||
"ButtonChooseFiles": "Выбор файлов",
|
"ButtonChooseFiles": "Выбор файлов",
|
||||||
"ButtonClearFilter": "Очистить Фильтр",
|
"ButtonClearFilter": "Очистить фильтр",
|
||||||
"ButtonCloseFeed": "Закрыть Канал",
|
"ButtonCloseFeed": "Закрыть канал",
|
||||||
"ButtonCollections": "Коллекции",
|
"ButtonCollections": "Коллекции",
|
||||||
"ButtonConfigureScanner": "Конфигурация Сканера",
|
"ButtonConfigureScanner": "Конфигурация сканера",
|
||||||
"ButtonCreate": "Создать",
|
"ButtonCreate": "Создать",
|
||||||
"ButtonCreateBackup": "Создать бэкап",
|
"ButtonCreateBackup": "Создать бэкап",
|
||||||
"ButtonDelete": "Удалить",
|
"ButtonDelete": "Удалить",
|
||||||
|
"ButtonDownloadQueue": "Очередь",
|
||||||
"ButtonEdit": "Редактировать",
|
"ButtonEdit": "Редактировать",
|
||||||
"ButtonEditChapters": "Редактировать Главы",
|
"ButtonEditChapters": "Редактировать главы",
|
||||||
"ButtonEditPodcast": "Редактировать Подкаст",
|
"ButtonEditPodcast": "Редактировать подкаст",
|
||||||
"ButtonForceReScan": "Принудительно Пере сканировать",
|
"ButtonForceReScan": "Принудительно пересканировать",
|
||||||
"ButtonFullPath": "Полный Путь",
|
"ButtonFullPath": "Полный путь",
|
||||||
"ButtonHide": "Скрыть",
|
"ButtonHide": "Скрыть",
|
||||||
"ButtonHome": "Домой",
|
"ButtonHome": "Домой",
|
||||||
"ButtonIssues": "Проблемы",
|
"ButtonIssues": "Проблемы",
|
||||||
@@ -32,143 +33,149 @@
|
|||||||
"ButtonLibrary": "Библиотека",
|
"ButtonLibrary": "Библиотека",
|
||||||
"ButtonLogout": "Выход",
|
"ButtonLogout": "Выход",
|
||||||
"ButtonLookup": "Найти",
|
"ButtonLookup": "Найти",
|
||||||
"ButtonManageTracks": "Управление Треками",
|
"ButtonManageTracks": "Управление треками",
|
||||||
"ButtonMapChapterTitles": "Найти Названия Глав",
|
"ButtonMapChapterTitles": "Найти названия глав",
|
||||||
"ButtonMatchAllAuthors": "Найти Всех Авторов",
|
"ButtonMatchAllAuthors": "Найти всех авторов",
|
||||||
"ButtonMatchBooks": "Найти Книги",
|
"ButtonMatchBooks": "Найти книги",
|
||||||
"ButtonNevermind": "Не важно",
|
"ButtonNevermind": "Не важно",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Открыть Канал",
|
"ButtonOpenFeed": "Открыть канал",
|
||||||
"ButtonOpenManager": "Открыть Менеджер",
|
"ButtonOpenManager": "Открыть менеджер",
|
||||||
"ButtonPlay": "Слушать",
|
"ButtonPlay": "Слушать",
|
||||||
"ButtonPlaying": "Проигрывается",
|
"ButtonPlaying": "Проигрывается",
|
||||||
"ButtonPlaylists": "Плейлисты",
|
"ButtonPlaylists": "Плейлисты",
|
||||||
"ButtonPurgeAllCache": "Очистить Весь Кэш",
|
"ButtonPurgeAllCache": "Очистить весь кэш",
|
||||||
"ButtonPurgeItemsCache": "Очистить Кэш Элементов",
|
"ButtonPurgeItemsCache": "Очистить кэш элементов",
|
||||||
"ButtonPurgeMediaProgress": "Очистить Прогресс Медиа",
|
"ButtonPurgeMediaProgress": "Очистить прогресс медиа",
|
||||||
"ButtonQueueAddItem": "Добавить в очередь",
|
"ButtonQueueAddItem": "Добавить в очередь",
|
||||||
"ButtonQueueRemoveItem": "Удалить из очереди",
|
"ButtonQueueRemoveItem": "Удалить из очереди",
|
||||||
"ButtonQuickMatch": "Быстрый Поиск",
|
"ButtonQuickMatch": "Быстрый поиск",
|
||||||
"ButtonRead": "Читать",
|
"ButtonRead": "Читать",
|
||||||
"ButtonRemove": "Удалить",
|
"ButtonRemove": "Удалить",
|
||||||
"ButtonRemoveAll": "Удалить Всё",
|
"ButtonRemoveAll": "Удалить всё",
|
||||||
"ButtonRemoveAllLibraryItems": "Удалить Все Элементы Библиотеки",
|
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
||||||
"ButtonRemoveFromContinueListening": "Удалить из Продолжить Слушать",
|
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Удалить Серию из Продолжить Серию",
|
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
||||||
"ButtonReScan": "Пере сканировать",
|
"ButtonReScan": "Пересканировать",
|
||||||
"ButtonReset": "Сбросить",
|
"ButtonReset": "Сбросить",
|
||||||
"ButtonRestore": "Восстановить",
|
"ButtonRestore": "Восстановить",
|
||||||
"ButtonSave": "Сохранить",
|
"ButtonSave": "Сохранить",
|
||||||
"ButtonSaveAndClose": "Сохранить и Закрыть",
|
"ButtonSaveAndClose": "Сохранить и закрыть",
|
||||||
"ButtonSaveTracklist": "Сохранить Список треков",
|
"ButtonSaveTracklist": "Сохранить список треков",
|
||||||
"ButtonScan": "Сканировать",
|
"ButtonScan": "Сканировать",
|
||||||
"ButtonScanLibrary": "Сканировать Библиотеку",
|
"ButtonScanLibrary": "Сканировать библиотеку",
|
||||||
"ButtonSearch": "Поиск",
|
"ButtonSearch": "Поиск",
|
||||||
"ButtonSelectFolderPath": "Выберите Путь Папки",
|
"ButtonSelectFolderPath": "Выберите путь папки",
|
||||||
"ButtonSeries": "Серии",
|
"ButtonSeries": "Серии",
|
||||||
"ButtonSetChaptersFromTracks": "Установить главы из треков",
|
"ButtonSetChaptersFromTracks": "Установить главы из треков",
|
||||||
"ButtonShiftTimes": "Смещение",
|
"ButtonShiftTimes": "Смещение",
|
||||||
"ButtonShow": "Показать",
|
"ButtonShow": "Показать",
|
||||||
"ButtonStartM4BEncode": "Начать Кодирование M4B",
|
"ButtonStartM4BEncode": "Начать кодирование M4B",
|
||||||
"ButtonStartMetadataEmbed": "Начать Встраивание Метаданных",
|
"ButtonStartMetadataEmbed": "Начать встраивание метаданных",
|
||||||
"ButtonSubmit": "Применить",
|
"ButtonSubmit": "Применить",
|
||||||
"ButtonUpload": "Загрузить",
|
"ButtonUpload": "Загрузить",
|
||||||
"ButtonUploadBackup": "Загрузить Бэкап",
|
"ButtonUploadBackup": "Загрузить бэкап",
|
||||||
"ButtonUploadCover": "Загрузить Обложку",
|
"ButtonUploadCover": "Загрузить обложку",
|
||||||
"ButtonUploadOPMLFile": "Загрузить Файл OPML",
|
"ButtonUploadOPMLFile": "Загрузить Файл OPML",
|
||||||
"ButtonUserDelete": "Удалить пользователя {0}",
|
"ButtonUserDelete": "Удалить пользователя {0}",
|
||||||
"ButtonUserEdit": "Редактировать пользователя {0}",
|
"ButtonUserEdit": "Редактировать пользователя {0}",
|
||||||
"ButtonViewAll": "Посмотреть Все",
|
"ButtonViewAll": "Посмотреть все",
|
||||||
"ButtonYes": "Да",
|
"ButtonYes": "Да",
|
||||||
"HeaderAccount": "Учетная запись",
|
"HeaderAccount": "Учетная запись",
|
||||||
"HeaderAdvanced": "Дополнительно",
|
"HeaderAdvanced": "Дополнительно",
|
||||||
"HeaderAppriseNotificationSettings": "Настройки Оповещений",
|
"HeaderAppriseNotificationSettings": "Настройки оповещений",
|
||||||
"HeaderAudiobookTools": "Инструменты Файлов Аудиокниг",
|
"HeaderAudiobookTools": "Инструменты файлов аудиокниг",
|
||||||
"HeaderAudioTracks": "Аудио Треки",
|
"HeaderAudioTracks": "Аудио треки",
|
||||||
"HeaderBackups": "Бэкапы",
|
"HeaderBackups": "Бэкапы",
|
||||||
"HeaderChangePassword": "Изменить Пароль",
|
"HeaderChangePassword": "Изменить пароль",
|
||||||
"HeaderChapters": "Главы",
|
"HeaderChapters": "Главы",
|
||||||
"HeaderChooseAFolder": "Выберите Папку",
|
"HeaderChooseAFolder": "Выберите папку",
|
||||||
"HeaderCollection": "Коллекция",
|
"HeaderCollection": "Коллекция",
|
||||||
"HeaderCollectionItems": "Элементы Коллекции",
|
"HeaderCollectionItems": "Элементы коллекции",
|
||||||
"HeaderCover": "Обложка",
|
"HeaderCover": "Обложка",
|
||||||
|
"HeaderCurrentDownloads": "Текущие закачки",
|
||||||
"HeaderDetails": "Подробности",
|
"HeaderDetails": "Подробности",
|
||||||
|
"HeaderDownloadQueue": "Очередь скачивания",
|
||||||
"HeaderEpisodes": "Эпизоды",
|
"HeaderEpisodes": "Эпизоды",
|
||||||
"HeaderFiles": "Файлы",
|
"HeaderFiles": "Файлы",
|
||||||
"HeaderFindChapters": "Найти Главы",
|
"HeaderFindChapters": "Найти главы",
|
||||||
"HeaderIgnoredFiles": "Игнорируемые Файлы",
|
"HeaderIgnoredFiles": "Игнорируемые Файлы",
|
||||||
"HeaderItemFiles": "Файлы Элемента",
|
"HeaderItemFiles": "Файлы элемента",
|
||||||
"HeaderItemMetadataUtils": "Утилиты",
|
"HeaderItemMetadataUtils": "Утилиты",
|
||||||
"HeaderLastListeningSession": "Последний Сеанс Прослушивания",
|
"HeaderLastListeningSession": "Последний сеанс прослушивания",
|
||||||
"HeaderLatestEpisodes": "Последние эпизоды",
|
"HeaderLatestEpisodes": "Последние эпизоды",
|
||||||
"HeaderLibraries": "Библиотеки",
|
"HeaderLibraries": "Библиотеки",
|
||||||
"HeaderLibraryFiles": "Файлы Библиотеки",
|
"HeaderLibraryFiles": "Файлы библиотеки",
|
||||||
"HeaderLibraryStats": "Статистика Библиотеки",
|
"HeaderLibraryStats": "Статистика библиотеки",
|
||||||
"HeaderListeningSessions": "Сеансы",
|
"HeaderListeningSessions": "Сеансы",
|
||||||
"HeaderListeningStats": "Статистика Прослушивания",
|
"HeaderListeningStats": "Статистика прослушивания",
|
||||||
"HeaderLogin": "Логин",
|
"HeaderLogin": "Логин",
|
||||||
"HeaderLogs": "Логи",
|
"HeaderLogs": "Логи",
|
||||||
"HeaderManageGenres": "Редактировать Жанры",
|
"HeaderManageGenres": "Редактировать жанры",
|
||||||
"HeaderManageTags": "Редактировать Теги",
|
"HeaderManageTags": "Редактировать теги",
|
||||||
"HeaderMapDetails": "Найти подробности",
|
"HeaderMapDetails": "Найти подробности",
|
||||||
"HeaderMatch": "Поиск",
|
"HeaderMatch": "Поиск",
|
||||||
"HeaderMetadataToEmbed": "Метаинформация для встраивания",
|
"HeaderMetadataToEmbed": "Метаинформация для встраивания",
|
||||||
"HeaderNewAccount": "Новая Учетная запись",
|
"HeaderNewAccount": "Новая учетная запись",
|
||||||
"HeaderNewLibrary": "Новая Библиотека",
|
"HeaderNewLibrary": "Новая библиотека",
|
||||||
"HeaderNotifications": "Уведомления",
|
"HeaderNotifications": "Уведомления",
|
||||||
"HeaderOpenRSSFeed": "Открыть RSS-канал",
|
"HeaderOpenRSSFeed": "Открыть RSS-канал",
|
||||||
"HeaderOtherFiles": "Другие Файлы",
|
"HeaderOtherFiles": "Другие файлы",
|
||||||
"HeaderPermissions": "Разрешения",
|
"HeaderPermissions": "Разрешения",
|
||||||
"HeaderPlayerQueue": "Очередь Воспроизведения",
|
"HeaderPlayerQueue": "Очередь воспроизведения",
|
||||||
"HeaderPlaylist": "Плейлист",
|
"HeaderPlaylist": "Плейлист",
|
||||||
"HeaderPlaylistItems": "Элементы Списка Воспроизведения",
|
"HeaderPlaylistItems": "Элементы списка воспроизведения",
|
||||||
"HeaderPodcastsToAdd": "Подкасты для Добавления",
|
"HeaderPodcastsToAdd": "Подкасты для добавления",
|
||||||
"HeaderPreviewCover": "Предпросмотр Обложки",
|
"HeaderPreviewCover": "Предпросмотр обложки",
|
||||||
"HeaderRemoveEpisode": "Удалить Эпизод",
|
"HeaderRemoveEpisode": "Удалить эпизод",
|
||||||
"HeaderRemoveEpisodes": "Удалить {0} Эпизодов",
|
"HeaderRemoveEpisodes": "Удалить {0} эпизодов",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-канал Открыт",
|
"HeaderRSSFeedGeneral": "Сведения о RSS",
|
||||||
"HeaderSavedMediaProgress": "Прогресс Медиа Сохранен",
|
"HeaderRSSFeedIsOpen": "RSS-канал открыт",
|
||||||
|
"HeaderSavedMediaProgress": "Прогресс медиа сохранен",
|
||||||
"HeaderSchedule": "Планировщик",
|
"HeaderSchedule": "Планировщик",
|
||||||
"HeaderScheduleLibraryScans": "Планировщик Автоматического Сканирования Библиотеки",
|
"HeaderScheduleLibraryScans": "Планировщик автоматического сканирования библиотеки",
|
||||||
"HeaderSession": "Сеансы",
|
"HeaderSession": "Сеансы",
|
||||||
"HeaderSetBackupSchedule": "Установить Планировщик Бэкапов",
|
"HeaderSetBackupSchedule": "Установить планировщик бэкапов",
|
||||||
"HeaderSettings": "Настройки",
|
"HeaderSettings": "Настройки",
|
||||||
"HeaderSettingsDisplay": "Дисплей",
|
"HeaderSettingsDisplay": "Дисплей",
|
||||||
"HeaderSettingsExperimental": "Экспериментальные Функции",
|
"HeaderSettingsExperimental": "Экспериментальные функции",
|
||||||
"HeaderSettingsGeneral": "Основные",
|
"HeaderSettingsGeneral": "Основные",
|
||||||
"HeaderSettingsScanner": "Сканер",
|
"HeaderSettingsScanner": "Сканер",
|
||||||
"HeaderSleepTimer": "Таймер Сна",
|
"HeaderSleepTimer": "Таймер сна",
|
||||||
"HeaderStatsLongestItems": "Самые Длинные Книги (часов)",
|
"HeaderStatsLargestItems": "Самые большые элементы",
|
||||||
|
"HeaderStatsLongestItems": "Самые длинные элементы (часов)",
|
||||||
"HeaderStatsMinutesListeningChart": "Минут прослушивания (последние 7 дней)",
|
"HeaderStatsMinutesListeningChart": "Минут прослушивания (последние 7 дней)",
|
||||||
"HeaderStatsRecentSessions": "Последние Сеансы",
|
"HeaderStatsRecentSessions": "Последние сеансы",
|
||||||
"HeaderStatsTop10Authors": "Топ 10 Авторов",
|
"HeaderStatsTop10Authors": "Топ 10 авторов",
|
||||||
"HeaderStatsTop5Genres": "Топ 5 Жанров",
|
"HeaderStatsTop5Genres": "Топ 5 жанров",
|
||||||
"HeaderTools": "Инструменты",
|
"HeaderTools": "Инструменты",
|
||||||
"HeaderUpdateAccount": "Обновить Учетную запись",
|
"HeaderUpdateAccount": "Обновить учетную запись",
|
||||||
"HeaderUpdateAuthor": "Обновить Автора",
|
"HeaderUpdateAuthor": "Обновить автора",
|
||||||
"HeaderUpdateDetails": "Обновить Детали",
|
"HeaderUpdateDetails": "Обновить детали",
|
||||||
"HeaderUpdateLibrary": "Обновить Библиотеку",
|
"HeaderUpdateLibrary": "Обновить библиотеку",
|
||||||
"HeaderUsers": "Пользователи",
|
"HeaderUsers": "Пользователи",
|
||||||
"HeaderYourStats": "Ваша Статистика",
|
"HeaderYourStats": "Ваша статистика",
|
||||||
"LabelAccountType": "Тип Учетной записи",
|
"LabelAbridged": "Abridged",
|
||||||
|
"LabelAccountType": "Тип учетной записи",
|
||||||
"LabelAccountTypeAdmin": "Администратор",
|
"LabelAccountTypeAdmin": "Администратор",
|
||||||
"LabelAccountTypeGuest": "Гость",
|
"LabelAccountTypeGuest": "Гость",
|
||||||
"LabelAccountTypeUser": "Пользователь",
|
"LabelAccountTypeUser": "Пользователь",
|
||||||
"LabelActivity": "Активность",
|
"LabelActivity": "Активность",
|
||||||
"LabelAddedAt": "Добавить В",
|
"LabelAddedAt": "Дата добавления",
|
||||||
"LabelAddToCollection": "Добавить в Коллекцию",
|
"LabelAddToCollection": "Добавить в коллекцию",
|
||||||
"LabelAddToCollectionBatch": "Добавить {0} Книг в Коллекцию",
|
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
|
||||||
"LabelAddToPlaylist": "Добавить в Плейлист",
|
"LabelAddToPlaylist": "Добавить в плейлист",
|
||||||
"LabelAddToPlaylistBatch": "Добавить {0} Элементов в Плейлист",
|
"LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист",
|
||||||
"LabelAll": "Все",
|
"LabelAll": "Все",
|
||||||
"LabelAllUsers": "Все пользователи",
|
"LabelAllUsers": "Все пользователи",
|
||||||
|
"LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке",
|
||||||
"LabelAppend": "Добавить",
|
"LabelAppend": "Добавить",
|
||||||
"LabelAuthor": "Автор",
|
"LabelAuthor": "Автор",
|
||||||
"LabelAuthorFirstLast": "Автор (Имя Фамилия)",
|
"LabelAuthorFirstLast": "Автор (Имя Фамилия)",
|
||||||
"LabelAuthorLastFirst": "Автор (Фамилия, Имя)",
|
"LabelAuthorLastFirst": "Автор (Фамилия, Имя)",
|
||||||
"LabelAuthors": "Авторы",
|
"LabelAuthors": "Авторы",
|
||||||
"LabelAutoDownloadEpisodes": "Скачивать Эпизоды Автоматически",
|
"LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически",
|
||||||
"LabelBackToUser": "Назад к Пользователю",
|
"LabelBackToUser": "Назад к пользователю",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование",
|
"LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Бэкапы сохраняются в /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Бэкапы сохраняются в /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Максимальный размер бэкапа (в GB)",
|
"LabelBackupsMaxBackupSize": "Максимальный размер бэкапа (в GB)",
|
||||||
@@ -176,27 +183,28 @@
|
|||||||
"LabelBackupsNumberToKeep": "Сохранять бэкапов",
|
"LabelBackupsNumberToKeep": "Сохранять бэкапов",
|
||||||
"LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.",
|
"LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.",
|
||||||
"LabelBooks": "Книги",
|
"LabelBooks": "Книги",
|
||||||
"LabelChangePassword": "Изменить Пароль",
|
"LabelChangePassword": "Изменить пароль",
|
||||||
"LabelChaptersFound": "глав найдено",
|
"LabelChaptersFound": "глав найдено",
|
||||||
"LabelChapterTitle": "Название Главы",
|
"LabelChapterTitle": "Название главы",
|
||||||
"LabelClosePlayer": "Закрыть проигрыватель",
|
"LabelClosePlayer": "Закрыть проигрыватель",
|
||||||
"LabelCollapseSeries": "Свернуть Серии",
|
"LabelCollapseSeries": "Свернуть серии",
|
||||||
"LabelCollections": "Коллекции",
|
"LabelCollections": "Коллекции",
|
||||||
"LabelComplete": "Завершить",
|
"LabelComplete": "Завершить",
|
||||||
"LabelConfirmPassword": "Подтвердить Пароль",
|
"LabelConfirmPassword": "Подтвердить пароль",
|
||||||
"LabelContinueListening": "Продолжить Слушать",
|
"LabelContinueListening": "Продолжить слушать",
|
||||||
"LabelContinueSeries": "Продолжить Серию",
|
"LabelContinueSeries": "Продолжить серию",
|
||||||
"LabelCover": "Обложка",
|
"LabelCover": "Обложка",
|
||||||
"LabelCoverImageURL": "URL Изображения Обложки",
|
"LabelCoverImageURL": "URL изображения обложки",
|
||||||
"LabelCreatedAt": "Создано",
|
"LabelCreatedAt": "Создано",
|
||||||
"LabelCronExpression": "Выражение Cron",
|
"LabelCronExpression": "Выражение Cron",
|
||||||
"LabelCurrent": "Текущий",
|
"LabelCurrent": "Текущий",
|
||||||
"LabelCurrently": "Текущее:",
|
"LabelCurrently": "Текущее:",
|
||||||
|
"LabelCustomCronExpression": "Пользовательское выражение Cron:",
|
||||||
"LabelDatetime": "Дата и время",
|
"LabelDatetime": "Дата и время",
|
||||||
"LabelDescription": "Описание",
|
"LabelDescription": "Описание",
|
||||||
"LabelDeselectAll": "Снять Выделение",
|
"LabelDeselectAll": "Снять выделение",
|
||||||
"LabelDevice": "Устройство",
|
"LabelDevice": "Устройство",
|
||||||
"LabelDeviceInfo": "Информация об Устройстве",
|
"LabelDeviceInfo": "Информация об устройстве",
|
||||||
"LabelDirectory": "Каталог",
|
"LabelDirectory": "Каталог",
|
||||||
"LabelDiscFromFilename": "Диск из Имени файла",
|
"LabelDiscFromFilename": "Диск из Имени файла",
|
||||||
"LabelDiscFromMetadata": "Диск из Метаданных",
|
"LabelDiscFromMetadata": "Диск из Метаданных",
|
||||||
@@ -207,16 +215,17 @@
|
|||||||
"LabelEnable": "Включить",
|
"LabelEnable": "Включить",
|
||||||
"LabelEnd": "Конец",
|
"LabelEnd": "Конец",
|
||||||
"LabelEpisode": "Эпизод",
|
"LabelEpisode": "Эпизод",
|
||||||
"LabelEpisodeTitle": "Имя Эпизода",
|
"LabelEpisodeTitle": "Имя эпизода",
|
||||||
"LabelEpisodeType": "Тип Эпизода",
|
"LabelEpisodeType": "Тип эпизода",
|
||||||
|
"LabelExample": "Пример",
|
||||||
"LabelExplicit": "Явный",
|
"LabelExplicit": "Явный",
|
||||||
"LabelFeedURL": "URL Канала",
|
"LabelFeedURL": "URL канала",
|
||||||
"LabelFile": "Файл",
|
"LabelFile": "Файл",
|
||||||
"LabelFileBirthtime": "Дата Создания",
|
"LabelFileBirthtime": "Дата создания",
|
||||||
"LabelFileModified": "Дата Модификации",
|
"LabelFileModified": "Дата модификации",
|
||||||
"LabelFilename": "Имя файла",
|
"LabelFilename": "Имя файла",
|
||||||
"LabelFilterByUser": "Фильтр по Пользователю",
|
"LabelFilterByUser": "Фильтр по пользователю",
|
||||||
"LabelFindEpisodes": "Найти Эпизоды",
|
"LabelFindEpisodes": "Найти эпизоды",
|
||||||
"LabelFinished": "Закончен",
|
"LabelFinished": "Закончен",
|
||||||
"LabelFolder": "Папка",
|
"LabelFolder": "Папка",
|
||||||
"LabelFolders": "Папки",
|
"LabelFolders": "Папки",
|
||||||
@@ -225,7 +234,7 @@
|
|||||||
"LabelHardDeleteFile": "Жесткое удаление файла",
|
"LabelHardDeleteFile": "Жесткое удаление файла",
|
||||||
"LabelHour": "Часы",
|
"LabelHour": "Часы",
|
||||||
"LabelIcon": "Иконка",
|
"LabelIcon": "Иконка",
|
||||||
"LabelIncludeInTracklist": "Включать в Список воспроизведения",
|
"LabelIncludeInTracklist": "Включать в список воспроизведения",
|
||||||
"LabelIncomplete": "Не завершен",
|
"LabelIncomplete": "Не завершен",
|
||||||
"LabelInProgress": "В процессе",
|
"LabelInProgress": "В процессе",
|
||||||
"LabelInterval": "Интервал",
|
"LabelInterval": "Интервал",
|
||||||
@@ -237,96 +246,103 @@
|
|||||||
"LabelIntervalEvery6Hours": "Каждые 6 часов",
|
"LabelIntervalEvery6Hours": "Каждые 6 часов",
|
||||||
"LabelIntervalEveryDay": "Каждый день",
|
"LabelIntervalEveryDay": "Каждый день",
|
||||||
"LabelIntervalEveryHour": "Каждый час",
|
"LabelIntervalEveryHour": "Каждый час",
|
||||||
"LabelInvalidParts": "Неверные Части",
|
"LabelInvalidParts": "Неверные части",
|
||||||
"LabelItem": "Элемент",
|
"LabelItem": "Элемент",
|
||||||
"LabelLanguage": "Язык",
|
"LabelLanguage": "Язык",
|
||||||
"LabelLanguageDefaultServer": "Язык Сервера по Умолчанию",
|
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
|
||||||
"LabelLastSeen": "Последнее Сканирование",
|
"LabelLastSeen": "Последнее сканирование",
|
||||||
"LabelLastTime": "Последний по Времени",
|
"LabelLastTime": "Последний по времени",
|
||||||
"LabelLastUpdate": "Последний Обновленный",
|
"LabelLastUpdate": "Последний обновленный",
|
||||||
"LabelLess": "Менее",
|
"LabelLess": "Менее",
|
||||||
"LabelLibrariesAccessibleToUser": "Библиотеки Доступные для Пользователя",
|
"LabelLibrariesAccessibleToUser": "Библиотеки доступные для пользователя",
|
||||||
"LabelLibrary": "Библиотека",
|
"LabelLibrary": "Библиотека",
|
||||||
"LabelLibraryItem": "Элемент Библиотеки",
|
"LabelLibraryItem": "Элемент библиотеки",
|
||||||
"LabelLibraryName": "Имя Библиотеки",
|
"LabelLibraryName": "Имя библиотеки",
|
||||||
"LabelLimit": "Лимит",
|
"LabelLimit": "Лимит",
|
||||||
"LabelListenAgain": "Послушать Снова",
|
"LabelListenAgain": "Послушать снова",
|
||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты",
|
"LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты",
|
||||||
"LabelMediaPlayer": "Медиа Проигрыватель",
|
"LabelMediaPlayer": "Медиа проигрыватель",
|
||||||
"LabelMediaType": "Тип Медиа",
|
"LabelMediaType": "Тип медиа",
|
||||||
"LabelMetadataProvider": "Провайдер",
|
"LabelMetadataProvider": "Провайдер",
|
||||||
"LabelMetaTag": "Мета Тег",
|
"LabelMetaTag": "Мета тег",
|
||||||
"LabelMinute": "Минуты",
|
"LabelMinute": "Минуты",
|
||||||
"LabelMissing": "Потеряно",
|
"LabelMissing": "Потеряно",
|
||||||
"LabelMissingParts": "Потерянные Части",
|
"LabelMissingParts": "Потерянные части",
|
||||||
"LabelMore": "Еще",
|
"LabelMore": "Еще",
|
||||||
"LabelName": "Имя",
|
"LabelName": "Имя",
|
||||||
"LabelNarrator": "Читает",
|
"LabelNarrator": "Читает",
|
||||||
"LabelNarrators": "Чтецы",
|
"LabelNarrators": "Чтецы",
|
||||||
"LabelNew": "Новый",
|
"LabelNew": "Новый",
|
||||||
"LabelNewestAuthors": "Новые Авторы",
|
"LabelNewestAuthors": "Новые авторы",
|
||||||
"LabelNewestEpisodes": "Новые Эпизоды",
|
"LabelNewestEpisodes": "Новые эпизоды",
|
||||||
"LabelNewPassword": "Новый Пароль",
|
"LabelNewPassword": "Новый пароль",
|
||||||
|
"LabelNextBackupDate": "Следующая дата бэкапирования",
|
||||||
|
"LabelNextScheduledRun": "Следущий запланированный запуск",
|
||||||
"LabelNotes": "Заметки",
|
"LabelNotes": "Заметки",
|
||||||
"LabelNotFinished": "Не Завершено",
|
"LabelNotFinished": "Не завершено",
|
||||||
"LabelNotificationAppriseURL": "URL(ы) для извещений",
|
"LabelNotificationAppriseURL": "URL(ы) для извещений",
|
||||||
"LabelNotificationAvailableVariables": "Доступные переменные",
|
"LabelNotificationAvailableVariables": "Доступные переменные",
|
||||||
"LabelNotificationBodyTemplate": "Шаблон Тела",
|
"LabelNotificationBodyTemplate": "Шаблон тела",
|
||||||
"LabelNotificationEvent": "Событие Оповещения",
|
"LabelNotificationEvent": "Событие оповещения",
|
||||||
"LabelNotificationsMaxFailedAttempts": "Макс. попыток",
|
"LabelNotificationsMaxFailedAttempts": "Макс. попыток",
|
||||||
"LabelNotificationsMaxFailedAttemptsHelp": "Уведомления будут выключены если произойдет ошибка отправки данное количество раз",
|
"LabelNotificationsMaxFailedAttemptsHelp": "Уведомления будут выключены если произойдет ошибка отправки данное количество раз",
|
||||||
"LabelNotificationsMaxQueueSize": "Макс. размер очереди для событий уведомлений",
|
"LabelNotificationsMaxQueueSize": "Макс. размер очереди для событий уведомлений",
|
||||||
"LabelNotificationsMaxQueueSizeHelp": "События ограничены 1 в секунду. События будут игнорированы если в очереди максимальное количество. Это предотвращает спам сообщениями.",
|
"LabelNotificationsMaxQueueSizeHelp": "События ограничены 1 в секунду. События будут игнорированы если в очереди максимальное количество. Это предотвращает спам сообщениями.",
|
||||||
"LabelNotificationTitleTemplate": "Шаблон Заголовка",
|
"LabelNotificationTitleTemplate": "Шаблон заголовка",
|
||||||
"LabelNotStarted": "Не Запущено",
|
"LabelNotStarted": "Не запущено",
|
||||||
"LabelNumberOfBooks": "Количество Книг",
|
"LabelNumberOfBooks": "Количество книг",
|
||||||
"LabelNumberOfEpisodes": "# Эпизодов",
|
"LabelNumberOfEpisodes": "# Эпизодов",
|
||||||
"LabelOpenRSSFeed": "Открыть RSS-канал",
|
"LabelOpenRSSFeed": "Открыть RSS-канал",
|
||||||
"LabelOverwrite": "Перезаписать",
|
"LabelOverwrite": "Перезаписать",
|
||||||
"LabelPassword": "Пароль",
|
"LabelPassword": "Пароль",
|
||||||
"LabelPath": "Путь",
|
"LabelPath": "Путь",
|
||||||
"LabelPermissionsAccessAllLibraries": "Есть Доступ ко всем Библиотекам",
|
"LabelPermissionsAccessAllLibraries": "Есть доступ ко всем библиотекам",
|
||||||
"LabelPermissionsAccessAllTags": "Есть Доступ ко всем Тегам",
|
"LabelPermissionsAccessAllTags": "Есть доступ ко всем тегам",
|
||||||
"LabelPermissionsAccessExplicitContent": "Есть Доступ к Явному Содержимому",
|
"LabelPermissionsAccessExplicitContent": "Есть доступ к явному содержимому",
|
||||||
"LabelPermissionsDelete": "Может Удалять",
|
"LabelPermissionsDelete": "Может удалять",
|
||||||
"LabelPermissionsDownload": "Может Скачивать",
|
"LabelPermissionsDownload": "Может скачивать",
|
||||||
"LabelPermissionsUpdate": "Может Обновлять",
|
"LabelPermissionsUpdate": "Может обновлять",
|
||||||
"LabelPermissionsUpload": "Может Закачивать",
|
"LabelPermissionsUpload": "Может закачивать",
|
||||||
"LabelPhotoPathURL": "Путь к Фото/URL",
|
"LabelPhotoPathURL": "Путь к фото/URL",
|
||||||
"LabelPlaylists": "Плейлисты",
|
"LabelPlaylists": "Плейлисты",
|
||||||
"LabelPlayMethod": "Метод Воспроизведения",
|
"LabelPlayMethod": "Метод воспроизведения",
|
||||||
"LabelPodcast": "Подкаст",
|
"LabelPodcast": "Подкаст",
|
||||||
"LabelPodcasts": "Подкасты",
|
"LabelPodcasts": "Подкасты",
|
||||||
"LabelPrefixesToIgnore": "Игнорируемые Префиксы (без учета регистра)",
|
"LabelPodcastType": "Тип подкаста",
|
||||||
|
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
||||||
|
"LabelPreventIndexing": "Запретить индексацию фида каталогами подкастов iTunes и Google",
|
||||||
"LabelProgress": "Прогресс",
|
"LabelProgress": "Прогресс",
|
||||||
"LabelProvider": "Провайдер",
|
"LabelProvider": "Провайдер",
|
||||||
"LabelPubDate": "Дата Публикации",
|
"LabelPubDate": "Дата публикации",
|
||||||
"LabelPublisher": "Издатель",
|
"LabelPublisher": "Издатель",
|
||||||
"LabelPublishYear": "Год Публикации",
|
"LabelPublishYear": "Год публикации",
|
||||||
"LabelRecentlyAdded": "Недавно Добавленные",
|
"LabelRecentlyAdded": "Недавно добавленные",
|
||||||
"LabelRecentSeries": "Последние Серии",
|
"LabelRecentSeries": "Последние серии",
|
||||||
"LabelRecommended": "Рекомендованное",
|
"LabelRecommended": "Рекомендованное",
|
||||||
"LabelRegion": "Регион",
|
"LabelRegion": "Регион",
|
||||||
"LabelReleaseDate": "Дата Выхода",
|
"LabelReleaseDate": "Дата выхода",
|
||||||
"LabelRemoveCover": "Удалить обложку",
|
"LabelRemoveCover": "Удалить обложку",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца",
|
||||||
"LabelRSSFeedOpen": "Открыть RSS-канал",
|
"LabelRSSFeedOpen": "Открыть RSS-канал",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Запретить индексирование",
|
||||||
"LabelRSSFeedSlug": "Встроить RSS-канал",
|
"LabelRSSFeedSlug": "Встроить RSS-канал",
|
||||||
"LabelRSSFeedURL": "URL RSS-канала",
|
"LabelRSSFeedURL": "URL RSS-канала",
|
||||||
"LabelSearchTerm": "Поисковый Запрос",
|
"LabelSearchTerm": "Поисковый запрос",
|
||||||
"LabelSearchTitle": "Поиск по Названию",
|
"LabelSearchTitle": "Поиск по названию",
|
||||||
"LabelSearchTitleOrASIN": "Поиск по Названию или ASIN",
|
"LabelSearchTitleOrASIN": "Поиск по названию или ASIN",
|
||||||
"LabelSeason": "Сезон",
|
"LabelSeason": "Сезон",
|
||||||
"LabelSequence": "Последовательность",
|
"LabelSequence": "Последовательность",
|
||||||
"LabelSeries": "Серия",
|
"LabelSeries": "Серия",
|
||||||
"LabelSeriesName": "Имя Серии",
|
"LabelSeriesName": "Имя серии",
|
||||||
"LabelSeriesProgress": "Прогресс Серии",
|
"LabelSeriesProgress": "Прогресс серии",
|
||||||
"LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками",
|
"LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками",
|
||||||
"LabelSettingsChromecastSupport": "Поддержка Chromecast",
|
"LabelSettingsChromecastSupport": "Поддержка Chromecast",
|
||||||
"LabelSettingsDateFormat": "Формат Даты",
|
"LabelSettingsDateFormat": "Формат даты",
|
||||||
"LabelSettingsDisableWatcher": "Отключить Отслеживание",
|
"LabelSettingsDisableWatcher": "Отключить отслеживание",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Отключить отслеживание для библиотеки",
|
"LabelSettingsDisableWatcherForLibrary": "Отключить отслеживание для библиотеки",
|
||||||
"LabelSettingsDisableWatcherHelp": "Отключает автоматическое добавление/обновление элементов, когда обнаружено изменение файлов. *Требуется перезапуск сервера",
|
"LabelSettingsDisableWatcherHelp": "Отключает автоматическое добавление/обновление элементов, когда обнаружено изменение файлов. *Требуется перезапуск сервера",
|
||||||
"LabelSettingsEnableEReader": "Включить e-reader для всех пользователей",
|
"LabelSettingsEnableEReader": "Включить e-reader для всех пользователей",
|
||||||
@@ -335,7 +351,7 @@
|
|||||||
"LabelSettingsExperimentalFeaturesHelp": "Функционал в разработке на который Вы могли бы дать отзыв или помочь в тестировании. Нажмите для открытия обсуждения на github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Функционал в разработке на который Вы могли бы дать отзыв или помочь в тестировании. Нажмите для открытия обсуждения на github.",
|
||||||
"LabelSettingsFindCovers": "Найти обложки",
|
"LabelSettingsFindCovers": "Найти обложки",
|
||||||
"LabelSettingsFindCoversHelp": "Если у Ваших аудиокниг нет встроенной обложки или файла обложки в папке книги, то сканер попробует найти обложку.<br>Примечание: Это увеличит время сканирования",
|
"LabelSettingsFindCoversHelp": "Если у Ваших аудиокниг нет встроенной обложки или файла обложки в папке книги, то сканер попробует найти обложку.<br>Примечание: Это увеличит время сканирования",
|
||||||
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней Странице",
|
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице",
|
||||||
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
|
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Overdrive Media Markers для глав",
|
"LabelSettingsOverdriveMediaMarkers": "Overdrive Media Markers для глав",
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 файлы из Overdrive поставляется с таймингами глав, встроенными в виде пользовательских метаданных. При включении этого параметра эти теги будут автоматически использоваться для таймингов глав",
|
"LabelSettingsOverdriveMediaMarkersHelp": "MP3 файлы из Overdrive поставляется с таймингами глав, встроенными в виде пользовательских метаданных. При включении этого параметра эти теги будут автоматически использоваться для таймингов глав",
|
||||||
@@ -357,57 +373,60 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"",
|
"LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом",
|
"LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента. Используется расширение файла .abs",
|
"LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента. Используется расширение файла .abs",
|
||||||
"LabelShowAll": "Показать Все",
|
"LabelSettingsTimeFormat": "Формат времени",
|
||||||
|
"LabelShowAll": "Показать все",
|
||||||
"LabelSize": "Размер",
|
"LabelSize": "Размер",
|
||||||
"LabelSleepTimer": "Таймер сна",
|
"LabelSleepTimer": "Таймер сна",
|
||||||
"LabelStart": "Начало",
|
"LabelStart": "Начало",
|
||||||
"LabelStarted": "Начат",
|
"LabelStarted": "Начат",
|
||||||
"LabelStartedAt": "Начато В",
|
"LabelStartedAt": "Начато В",
|
||||||
"LabelStartTime": "Время Начала",
|
"LabelStartTime": "Время начала",
|
||||||
"LabelStatsAudioTracks": "Аудио Треки",
|
"LabelStatsAudioTracks": "Аудио треки",
|
||||||
"LabelStatsAuthors": "Авторы",
|
"LabelStatsAuthors": "Авторы",
|
||||||
"LabelStatsBestDay": "Лучший День",
|
"LabelStatsBestDay": "Лучший День",
|
||||||
"LabelStatsDailyAverage": "В среднем в День",
|
"LabelStatsDailyAverage": "В среднем в день",
|
||||||
"LabelStatsDays": "Дней",
|
"LabelStatsDays": "Дней",
|
||||||
"LabelStatsDaysListened": "Дней Прослушано",
|
"LabelStatsDaysListened": "Дней прослушано",
|
||||||
"LabelStatsHours": "Часов",
|
"LabelStatsHours": "Часов",
|
||||||
"LabelStatsInARow": "в строке",
|
"LabelStatsInARow": "беспрерывно",
|
||||||
"LabelStatsItemsFinished": "Элементов Завершено",
|
"LabelStatsItemsFinished": "Элементов завершено",
|
||||||
"LabelStatsItemsInLibrary": "Элементов в Библиотеке",
|
"LabelStatsItemsInLibrary": "Элементов в библиотеке",
|
||||||
"LabelStatsMinutes": "минут",
|
"LabelStatsMinutes": "минут",
|
||||||
"LabelStatsMinutesListening": "Минут Прослушано",
|
"LabelStatsMinutesListening": "Минут прослушано",
|
||||||
"LabelStatsOverallDays": "Всего Дней",
|
"LabelStatsOverallDays": "Всего дней",
|
||||||
"LabelStatsOverallHours": "Всего Часов",
|
"LabelStatsOverallHours": "Всего сасов",
|
||||||
"LabelStatsWeekListening": "Недель Прослушано",
|
"LabelStatsWeekListening": "Недель прослушано",
|
||||||
"LabelSubtitle": "Подзаголовок",
|
"LabelSubtitle": "Подзаголовок",
|
||||||
"LabelSupportedFileTypes": "Поддерживаемые типы файлов",
|
"LabelSupportedFileTypes": "Поддерживаемые типы файлов",
|
||||||
"LabelTag": "Тег",
|
"LabelTag": "Тег",
|
||||||
"LabelTags": "Теги",
|
"LabelTags": "Теги",
|
||||||
"LabelTagsAccessibleToUser": "Теги Доступные для Пользователя",
|
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
||||||
"LabelTimeListened": "Время Прослушивания",
|
"LabelTasks": "Запущенные задачи",
|
||||||
"LabelTimeListenedToday": "Время Прослушивания Сегодня",
|
"LabelTimeListened": "Время прослушивания",
|
||||||
|
"LabelTimeListenedToday": "Время прослушивания сегодня",
|
||||||
"LabelTimeRemaining": "{0} осталось",
|
"LabelTimeRemaining": "{0} осталось",
|
||||||
"LabelTimeToShift": "Время смещения в сек.",
|
"LabelTimeToShift": "Время смещения в сек.",
|
||||||
"LabelTitle": "Название",
|
"LabelTitle": "Название",
|
||||||
"LabelToolsEmbedMetadata": "Встроить Метаданные",
|
"LabelToolsEmbedMetadata": "Встроить метаданные",
|
||||||
"LabelToolsEmbedMetadataDescription": "Встроить метаданные в аудио файлы, включая обложку и главы.",
|
"LabelToolsEmbedMetadataDescription": "Встроить метаданные в аудио файлы, включая обложку и главы.",
|
||||||
"LabelToolsMakeM4b": "Создать M4B Файл Аудиокниги",
|
"LabelToolsMakeM4b": "Создать M4B файл аудиокниги",
|
||||||
"LabelToolsMakeM4bDescription": "Создает .M4B файл аудиокниги с встроенными метаданными, обложкой и главами.",
|
"LabelToolsMakeM4bDescription": "Создает .M4B файл аудиокниги с встроенными метаданными, обложкой и главами.",
|
||||||
"LabelToolsSplitM4b": "Разделить M4B на MP3 файлы",
|
"LabelToolsSplitM4b": "Разделить M4B на MP3 файлы",
|
||||||
"LabelToolsSplitM4bDescription": "Создает MP3 файла из M4B, разделяет на главы с встроенными метаданными, обложкой и главами.",
|
"LabelToolsSplitM4bDescription": "Создает MP3 файла из M4B, разделяет на главы с встроенными метаданными, обложкой и главами.",
|
||||||
"LabelTotalDuration": "Общая Длина",
|
"LabelTotalDuration": "Общая длина",
|
||||||
"LabelTotalTimeListened": "Всего Прослушано",
|
"LabelTotalTimeListened": "Всего прослушано",
|
||||||
"LabelTrackFromFilename": "Трек из Имени файла",
|
"LabelTrackFromFilename": "Трек из Имени файла",
|
||||||
"LabelTrackFromMetadata": "Трек из Метаданных",
|
"LabelTrackFromMetadata": "Трек из Метаданных",
|
||||||
"LabelTracks": "Треков",
|
"LabelTracks": "Треков",
|
||||||
"LabelTracksMultiTrack": "Мультитрек",
|
"LabelTracksMultiTrack": "Мультитрек",
|
||||||
"LabelTracksSingleTrack": "Один трек",
|
"LabelTracksSingleTrack": "Один трек",
|
||||||
"LabelType": "Тип",
|
"LabelType": "Тип",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "Неизвестно",
|
"LabelUnknown": "Неизвестно",
|
||||||
"LabelUpdateCover": "Обновить Обложку",
|
"LabelUpdateCover": "Обновить обложку",
|
||||||
"LabelUpdateCoverHelp": "Позволяет перезаписывать существующие обложки для выбранных книг если будут найдены",
|
"LabelUpdateCoverHelp": "Позволяет перезаписывать существующие обложки для выбранных книг если будут найдены",
|
||||||
"LabelUpdatedAt": "Обновлено в",
|
"LabelUpdatedAt": "Обновлено в",
|
||||||
"LabelUpdateDetails": "Обновить Подробности",
|
"LabelUpdateDetails": "Обновить подробности",
|
||||||
"LabelUpdateDetailsHelp": "Позволяет перезаписывать текущие подробности для выбранных книг если будут найдены",
|
"LabelUpdateDetailsHelp": "Позволяет перезаписывать текущие подробности для выбранных книг если будут найдены",
|
||||||
"LabelUploaderDragAndDrop": "Перетащите файлы или каталоги",
|
"LabelUploaderDragAndDrop": "Перетащите файлы или каталоги",
|
||||||
"LabelUploaderDropFiles": "Перетащите файлы",
|
"LabelUploaderDropFiles": "Перетащите файлы",
|
||||||
@@ -422,10 +441,10 @@
|
|||||||
"LabelViewQueue": "Очередь воспроизведения",
|
"LabelViewQueue": "Очередь воспроизведения",
|
||||||
"LabelVolume": "Громкость",
|
"LabelVolume": "Громкость",
|
||||||
"LabelWeekdaysToRun": "Дни недели для запуска",
|
"LabelWeekdaysToRun": "Дни недели для запуска",
|
||||||
"LabelYourAudiobookDuration": "Продолжительность Вашей Книги",
|
"LabelYourAudiobookDuration": "Продолжительность Вашей книги",
|
||||||
"LabelYourBookmarks": "Ваши Закладки",
|
"LabelYourBookmarks": "Ваши закладки",
|
||||||
"LabelYourPlaylists": "Ваши Плейлисты",
|
"LabelYourPlaylists": "Ваши плейлисты",
|
||||||
"LabelYourProgress": "Ваш Прогресс",
|
"LabelYourProgress": "Ваш прогресс",
|
||||||
"MessageAddToPlayerQueue": "Добавить в очередь проигрывателя",
|
"MessageAddToPlayerQueue": "Добавить в очередь проигрывателя",
|
||||||
"MessageAppriseDescription": "Для использования этой функции необходимо иметь запущенный экземпляр <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или api которое обрабатывает те же самые запросы. <br />URL-адрес API Apprise должен быть полным URL-адресом для отправки уведомления, т.е., если API запущено по адресу <code>http://192.168.1.1:8337</code> тогда нужно указать <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Для использования этой функции необходимо иметь запущенный экземпляр <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или api которое обрабатывает те же самые запросы. <br />URL-адрес API Apprise должен быть полным URL-адресом для отправки уведомления, т.е., если API запущено по адресу <code>http://192.168.1.1:8337</code> тогда нужно указать <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Бэкап включает пользователей, прогресс пользователей, данные элементов библиотеки, настройки сервера и изображения хранящиеся в <code>/metadata/items</code> и <code>/metadata/authors</code>. Бэкапы <strong>НЕ</strong> сохраняют файлы из папок библиотек.",
|
"MessageBackupsDescription": "Бэкап включает пользователей, прогресс пользователей, данные элементов библиотеки, настройки сервера и изображения хранящиеся в <code>/metadata/items</code> и <code>/metadata/authors</code>. Бэкапы <strong>НЕ</strong> сохраняют файлы из папок библиотек.",
|
||||||
@@ -465,8 +484,8 @@
|
|||||||
"MessageForceReScanDescription": "будет сканировать все файлы снова, как свежее сканирование. Теги ID3 аудиофайлов, OPF-файлы и текстовые файлы будут сканироваться как новые.",
|
"MessageForceReScanDescription": "будет сканировать все файлы снова, как свежее сканирование. Теги ID3 аудиофайлов, OPF-файлы и текстовые файлы будут сканироваться как новые.",
|
||||||
"MessageImportantNotice": "Важное замечание!",
|
"MessageImportantNotice": "Важное замечание!",
|
||||||
"MessageInsertChapterBelow": "Вставить главу ниже",
|
"MessageInsertChapterBelow": "Вставить главу ниже",
|
||||||
"MessageItemsSelected": "{0} Элементов Выделено",
|
"MessageItemsSelected": "{0} Элементов выделено",
|
||||||
"MessageItemsUpdated": "{0} Элементов Обновлено",
|
"MessageItemsUpdated": "{0} Элементов обновлено",
|
||||||
"MessageJoinUsOn": "Присоединяйтесь к нам в",
|
"MessageJoinUsOn": "Присоединяйтесь к нам в",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} сеансов прослушивания в прошлом году",
|
"MessageListeningSessionsInTheLastYear": "{0} сеансов прослушивания в прошлом году",
|
||||||
"MessageLoading": "Загрузка...",
|
"MessageLoading": "Загрузка...",
|
||||||
@@ -474,33 +493,36 @@
|
|||||||
"MessageM4BFailed": "M4B Ошибка!",
|
"MessageM4BFailed": "M4B Ошибка!",
|
||||||
"MessageM4BFinished": "M4B Завершено!",
|
"MessageM4BFinished": "M4B Завершено!",
|
||||||
"MessageMapChapterTitles": "Сопоставление названий глав с существующими главами аудиокниги без корректировки временных меток",
|
"MessageMapChapterTitles": "Сопоставление названий глав с существующими главами аудиокниги без корректировки временных меток",
|
||||||
"MessageMarkAsFinished": "Отметить, как Завершенную",
|
"MessageMarkAsFinished": "Отметить, как завершенную",
|
||||||
"MessageMarkAsNotFinished": "Отметить, как Не Завершенную",
|
"MessageMarkAsNotFinished": "Отметить, как не завершенную",
|
||||||
"MessageMatchBooksDescription": "попытается сопоставить книги в библиотеке с книгой из выбранного поставщика поиска и заполнить пустые детали и обложку. Не перезаписывает сведения.",
|
"MessageMatchBooksDescription": "попытается сопоставить книги в библиотеке с книгой из выбранного поставщика поиска и заполнить пустые детали и обложку. Не перезаписывает сведения.",
|
||||||
"MessageNoAudioTracks": "Нет аудио треков",
|
"MessageNoAudioTracks": "Нет аудио треков",
|
||||||
"MessageNoAuthors": "Нет Авторов",
|
"MessageNoAuthors": "Нет авторов",
|
||||||
"MessageNoBackups": "Нет Бэкапов",
|
"MessageNoBackups": "Нет бэкапов",
|
||||||
"MessageNoBookmarks": "Нет Закладок",
|
"MessageNoBookmarks": "Нет закладок",
|
||||||
"MessageNoChapters": "Нет Глав",
|
"MessageNoChapters": "Нет глав",
|
||||||
"MessageNoCollections": "Нет Коллекций",
|
"MessageNoCollections": "Нет коллекций",
|
||||||
"MessageNoCoversFound": "Обложек не найдено",
|
"MessageNoCoversFound": "Обложек не найдено",
|
||||||
"MessageNoDescription": "Нет описания",
|
"MessageNoDescription": "Нет описания",
|
||||||
|
"MessageNoDownloadsInProgress": "В настоящее время загрузка не выполняется",
|
||||||
|
"MessageNoDownloadsQueued": "Нет загрузок в очереди",
|
||||||
"MessageNoEpisodeMatchesFound": "Совпадения эпизодов не найдены",
|
"MessageNoEpisodeMatchesFound": "Совпадения эпизодов не найдены",
|
||||||
"MessageNoEpisodes": "Нет Эпизодов",
|
"MessageNoEpisodes": "Нет эпизодов",
|
||||||
"MessageNoFoldersAvailable": "Нет доступных папок",
|
"MessageNoFoldersAvailable": "Нет доступных папок",
|
||||||
"MessageNoGenres": "Нет Жанров",
|
"MessageNoGenres": "Нет жанров",
|
||||||
"MessageNoIssues": "Нет Проблем",
|
"MessageNoIssues": "Нет проблем",
|
||||||
"MessageNoItems": "Нет Элементов",
|
"MessageNoItems": "Нет элементов",
|
||||||
"MessageNoItemsFound": "Элементы не найдены",
|
"MessageNoItemsFound": "Элементы не найдены",
|
||||||
"MessageNoListeningSessions": "Нет Сеансов Прослушивания",
|
"MessageNoListeningSessions": "Нет сеансов прослушивания",
|
||||||
"MessageNoLogs": "Нет Логов",
|
"MessageNoLogs": "Нет логов",
|
||||||
"MessageNoMediaProgress": "Нет Прогресса Медиа",
|
"MessageNoMediaProgress": "Нет прогресса медиа",
|
||||||
"MessageNoNotifications": "Нет Уведомлений",
|
"MessageNoNotifications": "Нет уведомлений",
|
||||||
"MessageNoPodcastsFound": "Подкасты не найдены",
|
"MessageNoPodcastsFound": "Подкасты не найдены",
|
||||||
"MessageNoResults": "Нет Результатов",
|
"MessageNoResults": "Нет результатов",
|
||||||
"MessageNoSearchResultsFor": "Нет результатов поиска для \"{0}\"",
|
"MessageNoSearchResultsFor": "Нет результатов поиска для \"{0}\"",
|
||||||
"MessageNoSeries": "Нет Серий",
|
"MessageNoSeries": "Нет серий",
|
||||||
"MessageNoTags": "Нет Тегов",
|
"MessageNoTags": "Нет тегов",
|
||||||
|
"MessageNoTasksRunning": "Нет выполняемых задач",
|
||||||
"MessageNotYetImplemented": "Пока не реализовано",
|
"MessageNotYetImplemented": "Пока не реализовано",
|
||||||
"MessageNoUpdateNecessary": "Обновление не требуется",
|
"MessageNoUpdateNecessary": "Обновление не требуется",
|
||||||
"MessageNoUpdatesWereNecessary": "Обновления не требовались",
|
"MessageNoUpdatesWereNecessary": "Обновления не требовались",
|
||||||
@@ -526,7 +548,7 @@
|
|||||||
"MessageStartPlaybackAtTime": "Начать воспроизведение для \"{0}\" с {1}?",
|
"MessageStartPlaybackAtTime": "Начать воспроизведение для \"{0}\" с {1}?",
|
||||||
"MessageThinking": "Думаю...",
|
"MessageThinking": "Думаю...",
|
||||||
"MessageUploaderItemFailed": "Не удалось загрузить",
|
"MessageUploaderItemFailed": "Не удалось загрузить",
|
||||||
"MessageUploaderItemSuccess": "Успешно Загружено!",
|
"MessageUploaderItemSuccess": "Успешно загружено!",
|
||||||
"MessageUploading": "Загрузка...",
|
"MessageUploading": "Загрузка...",
|
||||||
"MessageValidCronExpression": "Верное cron выражение",
|
"MessageValidCronExpression": "Верное cron выражение",
|
||||||
"MessageWatcherIsDisabledGlobally": "Наблюдатель отключен глобально в настройках сервера",
|
"MessageWatcherIsDisabledGlobally": "Наблюдатель отключен глобально в настройках сервера",
|
||||||
@@ -546,6 +568,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Путь к новой папке",
|
"PlaceholderNewFolderPath": "Путь к новой папке",
|
||||||
"PlaceholderNewPlaylist": "Новое название плейлиста",
|
"PlaceholderNewPlaylist": "Новое название плейлиста",
|
||||||
"PlaceholderSearch": "Поиск...",
|
"PlaceholderSearch": "Поиск...",
|
||||||
|
"PlaceholderSearchEpisode": "Search episode...",
|
||||||
"ToastAccountUpdateFailed": "Не удалось обновить учетную запись",
|
"ToastAccountUpdateFailed": "Не удалось обновить учетную запись",
|
||||||
"ToastAccountUpdateSuccess": "Учетная запись обновлена",
|
"ToastAccountUpdateSuccess": "Учетная запись обновлена",
|
||||||
"ToastAuthorImageRemoveFailed": "Не удалось удалить изображение",
|
"ToastAuthorImageRemoveFailed": "Не удалось удалить изображение",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "创建",
|
"ButtonCreate": "创建",
|
||||||
"ButtonCreateBackup": "创建备份",
|
"ButtonCreateBackup": "创建备份",
|
||||||
"ButtonDelete": "删除",
|
"ButtonDelete": "删除",
|
||||||
|
"ButtonDownloadQueue": "下载队列",
|
||||||
"ButtonEdit": "编辑",
|
"ButtonEdit": "编辑",
|
||||||
"ButtonEditChapters": "编辑章节",
|
"ButtonEditChapters": "编辑章节",
|
||||||
"ButtonEditPodcast": "编辑播客",
|
"ButtonEditPodcast": "编辑播客",
|
||||||
@@ -92,13 +93,15 @@
|
|||||||
"HeaderCollection": "收藏",
|
"HeaderCollection": "收藏",
|
||||||
"HeaderCollectionItems": "收藏项目",
|
"HeaderCollectionItems": "收藏项目",
|
||||||
"HeaderCover": "封面",
|
"HeaderCover": "封面",
|
||||||
|
"HeaderCurrentDownloads": "当前下载",
|
||||||
"HeaderDetails": "详情",
|
"HeaderDetails": "详情",
|
||||||
|
"HeaderDownloadQueue": "下载队列",
|
||||||
"HeaderEpisodes": "剧集",
|
"HeaderEpisodes": "剧集",
|
||||||
"HeaderFiles": "文件",
|
"HeaderFiles": "文件",
|
||||||
"HeaderFindChapters": "查找章节",
|
"HeaderFindChapters": "查找章节",
|
||||||
"HeaderIgnoredFiles": "忽略的文件",
|
"HeaderIgnoredFiles": "忽略的文件",
|
||||||
"HeaderItemFiles": "项目文件",
|
"HeaderItemFiles": "项目文件",
|
||||||
"HeaderItemMetadataUtils": "项目元数据管理程序",
|
"HeaderItemMetadataUtils": "项目元数据管理",
|
||||||
"HeaderLastListeningSession": "最后一次收听会话",
|
"HeaderLastListeningSession": "最后一次收听会话",
|
||||||
"HeaderLatestEpisodes": "最新剧集",
|
"HeaderLatestEpisodes": "最新剧集",
|
||||||
"HeaderLibraries": "媒体库",
|
"HeaderLibraries": "媒体库",
|
||||||
@@ -126,6 +129,7 @@
|
|||||||
"HeaderPreviewCover": "预览封面",
|
"HeaderPreviewCover": "预览封面",
|
||||||
"HeaderRemoveEpisode": "移除剧集",
|
"HeaderRemoveEpisode": "移除剧集",
|
||||||
"HeaderRemoveEpisodes": "移除 {0} 剧集",
|
"HeaderRemoveEpisodes": "移除 {0} 剧集",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS 详细信息",
|
||||||
"HeaderRSSFeedIsOpen": "RSS 源已打开",
|
"HeaderRSSFeedIsOpen": "RSS 源已打开",
|
||||||
"HeaderSavedMediaProgress": "保存媒体进度",
|
"HeaderSavedMediaProgress": "保存媒体进度",
|
||||||
"HeaderSchedule": "计划任务",
|
"HeaderSchedule": "计划任务",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"HeaderSettingsGeneral": "通用",
|
"HeaderSettingsGeneral": "通用",
|
||||||
"HeaderSettingsScanner": "扫描",
|
"HeaderSettingsScanner": "扫描",
|
||||||
"HeaderSleepTimer": "睡眠计时",
|
"HeaderSleepTimer": "睡眠计时",
|
||||||
|
"HeaderStatsLargestItems": "最大的项目",
|
||||||
"HeaderStatsLongestItems": "项目时长(小时)",
|
"HeaderStatsLongestItems": "项目时长(小时)",
|
||||||
"HeaderStatsMinutesListeningChart": "收听分钟数(最近7天)",
|
"HeaderStatsMinutesListeningChart": "收听分钟数(最近7天)",
|
||||||
"HeaderStatsRecentSessions": "历史会话",
|
"HeaderStatsRecentSessions": "历史会话",
|
||||||
@@ -150,6 +155,7 @@
|
|||||||
"HeaderUpdateLibrary": "更新媒体库",
|
"HeaderUpdateLibrary": "更新媒体库",
|
||||||
"HeaderUsers": "用户",
|
"HeaderUsers": "用户",
|
||||||
"HeaderYourStats": "你的统计数据",
|
"HeaderYourStats": "你的统计数据",
|
||||||
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAccountType": "帐户类型",
|
"LabelAccountType": "帐户类型",
|
||||||
"LabelAccountTypeAdmin": "管理员",
|
"LabelAccountTypeAdmin": "管理员",
|
||||||
"LabelAccountTypeGuest": "来宾",
|
"LabelAccountTypeGuest": "来宾",
|
||||||
@@ -162,6 +168,7 @@
|
|||||||
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
|
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
|
||||||
"LabelAll": "全部",
|
"LabelAll": "全部",
|
||||||
"LabelAllUsers": "所有用户",
|
"LabelAllUsers": "所有用户",
|
||||||
|
"LabelAlreadyInYourLibrary": "已存在你的库中",
|
||||||
"LabelAppend": "附加",
|
"LabelAppend": "附加",
|
||||||
"LabelAuthor": "作者",
|
"LabelAuthor": "作者",
|
||||||
"LabelAuthorFirstLast": "作者 (姓 名)",
|
"LabelAuthorFirstLast": "作者 (姓 名)",
|
||||||
@@ -192,6 +199,7 @@
|
|||||||
"LabelCronExpression": "计划任务表达式",
|
"LabelCronExpression": "计划任务表达式",
|
||||||
"LabelCurrent": "当前",
|
"LabelCurrent": "当前",
|
||||||
"LabelCurrently": "当前:",
|
"LabelCurrently": "当前:",
|
||||||
|
"LabelCustomCronExpression": "自定义计划任务表达式:",
|
||||||
"LabelDatetime": "日期时间",
|
"LabelDatetime": "日期时间",
|
||||||
"LabelDescription": "描述",
|
"LabelDescription": "描述",
|
||||||
"LabelDeselectAll": "全部取消选择",
|
"LabelDeselectAll": "全部取消选择",
|
||||||
@@ -209,6 +217,7 @@
|
|||||||
"LabelEpisode": "剧集",
|
"LabelEpisode": "剧集",
|
||||||
"LabelEpisodeTitle": "剧集标题",
|
"LabelEpisodeTitle": "剧集标题",
|
||||||
"LabelEpisodeType": "剧集类型",
|
"LabelEpisodeType": "剧集类型",
|
||||||
|
"LabelExample": "示例",
|
||||||
"LabelExplicit": "信息准确",
|
"LabelExplicit": "信息准确",
|
||||||
"LabelFeedURL": "源 URL",
|
"LabelFeedURL": "源 URL",
|
||||||
"LabelFile": "文件",
|
"LabelFile": "文件",
|
||||||
@@ -270,6 +279,8 @@
|
|||||||
"LabelNewestAuthors": "最新作者",
|
"LabelNewestAuthors": "最新作者",
|
||||||
"LabelNewestEpisodes": "最新剧集",
|
"LabelNewestEpisodes": "最新剧集",
|
||||||
"LabelNewPassword": "新密码",
|
"LabelNewPassword": "新密码",
|
||||||
|
"LabelNextBackupDate": "下次备份日期",
|
||||||
|
"LabelNextScheduledRun": "下次任务运行",
|
||||||
"LabelNotes": "注释",
|
"LabelNotes": "注释",
|
||||||
"LabelNotFinished": "未听完",
|
"LabelNotFinished": "未听完",
|
||||||
"LabelNotificationAppriseURL": "通知 URL(s)",
|
"LabelNotificationAppriseURL": "通知 URL(s)",
|
||||||
@@ -300,7 +311,9 @@
|
|||||||
"LabelPlayMethod": "播放方法",
|
"LabelPlayMethod": "播放方法",
|
||||||
"LabelPodcast": "播客",
|
"LabelPodcast": "播客",
|
||||||
"LabelPodcasts": "播客",
|
"LabelPodcasts": "播客",
|
||||||
|
"LabelPodcastType": "播客类型",
|
||||||
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
||||||
|
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引",
|
||||||
"LabelProgress": "进度",
|
"LabelProgress": "进度",
|
||||||
"LabelProvider": "供应商",
|
"LabelProvider": "供应商",
|
||||||
"LabelPubDate": "出版日期",
|
"LabelPubDate": "出版日期",
|
||||||
@@ -312,7 +325,10 @@
|
|||||||
"LabelRegion": "区域",
|
"LabelRegion": "区域",
|
||||||
"LabelReleaseDate": "发布日期",
|
"LabelReleaseDate": "发布日期",
|
||||||
"LabelRemoveCover": "移除封面",
|
"LabelRemoveCover": "移除封面",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
|
||||||
"LabelRSSFeedOpen": "打开 RSS 源",
|
"LabelRSSFeedOpen": "打开 RSS 源",
|
||||||
|
"LabelRSSFeedPreventIndexing": "防止索引",
|
||||||
"LabelRSSFeedSlug": "RSS 源段",
|
"LabelRSSFeedSlug": "RSS 源段",
|
||||||
"LabelRSSFeedURL": "RSS 源 URL",
|
"LabelRSSFeedURL": "RSS 源 URL",
|
||||||
"LabelSearchTerm": "搜索项",
|
"LabelSearchTerm": "搜索项",
|
||||||
@@ -357,6 +373,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件",
|
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件",
|
||||||
"LabelSettingsStoreMetadataWithItem": "存储项目元数据",
|
"LabelSettingsStoreMetadataWithItem": "存储项目元数据",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名",
|
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名",
|
||||||
|
"LabelSettingsTimeFormat": "时间格式",
|
||||||
"LabelShowAll": "全部显示",
|
"LabelShowAll": "全部显示",
|
||||||
"LabelSize": "文件大小",
|
"LabelSize": "文件大小",
|
||||||
"LabelSleepTimer": "睡眠定时",
|
"LabelSleepTimer": "睡眠定时",
|
||||||
@@ -384,6 +401,7 @@
|
|||||||
"LabelTag": "标签",
|
"LabelTag": "标签",
|
||||||
"LabelTags": "标签",
|
"LabelTags": "标签",
|
||||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||||
|
"LabelTasks": "正在运行的任务",
|
||||||
"LabelTimeListened": "收听时间",
|
"LabelTimeListened": "收听时间",
|
||||||
"LabelTimeListenedToday": "今日收听的时间",
|
"LabelTimeListenedToday": "今日收听的时间",
|
||||||
"LabelTimeRemaining": "剩余 {0}",
|
"LabelTimeRemaining": "剩余 {0}",
|
||||||
@@ -403,6 +421,7 @@
|
|||||||
"LabelTracksMultiTrack": "多轨",
|
"LabelTracksMultiTrack": "多轨",
|
||||||
"LabelTracksSingleTrack": "单轨",
|
"LabelTracksSingleTrack": "单轨",
|
||||||
"LabelType": "类型",
|
"LabelType": "类型",
|
||||||
|
"LabelUnabridged": "Unabridged",
|
||||||
"LabelUnknown": "未知",
|
"LabelUnknown": "未知",
|
||||||
"LabelUpdateCover": "更新封面",
|
"LabelUpdateCover": "更新封面",
|
||||||
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
||||||
@@ -485,6 +504,8 @@
|
|||||||
"MessageNoCollections": "没有收藏",
|
"MessageNoCollections": "没有收藏",
|
||||||
"MessageNoCoversFound": "没有找到封面",
|
"MessageNoCoversFound": "没有找到封面",
|
||||||
"MessageNoDescription": "没有描述",
|
"MessageNoDescription": "没有描述",
|
||||||
|
"MessageNoDownloadsInProgress": "当前没有正在进行的下载",
|
||||||
|
"MessageNoDownloadsQueued": "下载队列无任务",
|
||||||
"MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项",
|
"MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项",
|
||||||
"MessageNoEpisodes": "没有剧集",
|
"MessageNoEpisodes": "没有剧集",
|
||||||
"MessageNoFoldersAvailable": "没有可用文件夹",
|
"MessageNoFoldersAvailable": "没有可用文件夹",
|
||||||
@@ -501,6 +522,7 @@
|
|||||||
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
|
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
|
||||||
"MessageNoSeries": "无系列",
|
"MessageNoSeries": "无系列",
|
||||||
"MessageNoTags": "无标签",
|
"MessageNoTags": "无标签",
|
||||||
|
"MessageNoTasksRunning": "没有正在运行的任务",
|
||||||
"MessageNotYetImplemented": "尚未实施",
|
"MessageNotYetImplemented": "尚未实施",
|
||||||
"MessageNoUpdateNecessary": "无需更新",
|
"MessageNoUpdateNecessary": "无需更新",
|
||||||
"MessageNoUpdatesWereNecessary": "无需更新",
|
"MessageNoUpdatesWereNecessary": "无需更新",
|
||||||
@@ -546,6 +568,7 @@
|
|||||||
"PlaceholderNewFolderPath": "输入文件夹路径",
|
"PlaceholderNewFolderPath": "输入文件夹路径",
|
||||||
"PlaceholderNewPlaylist": "输入播放列表名称",
|
"PlaceholderNewPlaylist": "输入播放列表名称",
|
||||||
"PlaceholderSearch": "查找..",
|
"PlaceholderSearch": "查找..",
|
||||||
|
"PlaceholderSearchEpisode": "Search episode..",
|
||||||
"ToastAccountUpdateFailed": "账户更新失败",
|
"ToastAccountUpdateFailed": "账户更新失败",
|
||||||
"ToastAccountUpdateSuccess": "帐户已更新",
|
"ToastAccountUpdateSuccess": "帐户已更新",
|
||||||
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
|
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 221 KiB |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.15",
|
"version": "2.2.18",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.15",
|
"version": "2.2.18",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.15",
|
"version": "2.2.18",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ Check out the [API documentation](https://api.audiobookshelf.org/)
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<img alt="Library Screenshot" src="https://github.com/advplyr/audiobookshelf/raw/master/images/LibraryStreamSquare.png" />
|
<img alt="Library Screenshot" src="https://github.com/advplyr/audiobookshelf/raw/master/images/DemoLibrary.png" />
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ class Server {
|
|||||||
this.abMergeManager = new AbMergeManager(this.db, this.taskManager)
|
this.abMergeManager = new AbMergeManager(this.db, this.taskManager)
|
||||||
this.playbackSessionManager = new PlaybackSessionManager(this.db)
|
this.playbackSessionManager = new PlaybackSessionManager(this.db)
|
||||||
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
||||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager)
|
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager, this.taskManager)
|
||||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
||||||
this.rssFeedManager = new RssFeedManager(this.db)
|
this.rssFeedManager = new RssFeedManager(this.db)
|
||||||
this.eBookManager = new EBookManager(this.db)
|
this.eBookManager = new EBookManager(this.db)
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ class LibraryController {
|
|||||||
return res.json(req.library)
|
return res.json(req.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEpisodeDownloadQueue(req, res) {
|
||||||
|
const libraryDownloadQueueDetails = this.podcastManager.getDownloadQueueDetails(req.library.id)
|
||||||
|
return res.json(libraryDownloadQueueDetails)
|
||||||
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const library = req.library
|
const library = req.library
|
||||||
|
|
||||||
@@ -229,6 +234,16 @@ class LibraryController {
|
|||||||
if (payload.sortBy === 'book.volumeNumber') payload.sortBy = null // TODO: Remove temp fix after mobile release 0.9.60
|
if (payload.sortBy === 'book.volumeNumber') payload.sortBy = null // TODO: Remove temp fix after mobile release 0.9.60
|
||||||
if (filterSeries && !payload.sortBy) {
|
if (filterSeries && !payload.sortBy) {
|
||||||
sortArray.push({ asc: (li) => li.media.metadata.getSeries(filterSeries).sequence })
|
sortArray.push({ asc: (li) => li.media.metadata.getSeries(filterSeries).sequence })
|
||||||
|
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
||||||
|
sortArray.push({
|
||||||
|
asc: (li) => {
|
||||||
|
if (this.db.serverSettings.sortingIgnorePrefix) {
|
||||||
|
return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
|
||||||
|
} else {
|
||||||
|
return li.collapsedSeries?.name || li.media.metadata.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.sortBy) {
|
if (payload.sortBy) {
|
||||||
@@ -637,6 +652,7 @@ class LibraryController {
|
|||||||
var authorsWithCount = libraryHelpers.getAuthorsWithCount(libraryItems)
|
var authorsWithCount = libraryHelpers.getAuthorsWithCount(libraryItems)
|
||||||
var genresWithCount = libraryHelpers.getGenresWithCount(libraryItems)
|
var genresWithCount = libraryHelpers.getGenresWithCount(libraryItems)
|
||||||
var durationStats = libraryHelpers.getItemDurationStats(libraryItems)
|
var durationStats = libraryHelpers.getItemDurationStats(libraryItems)
|
||||||
|
var sizeStats = libraryHelpers.getItemSizeStats(libraryItems)
|
||||||
var stats = {
|
var stats = {
|
||||||
totalItems: libraryItems.length,
|
totalItems: libraryItems.length,
|
||||||
totalAuthors: Object.keys(authorsWithCount).length,
|
totalAuthors: Object.keys(authorsWithCount).length,
|
||||||
@@ -645,6 +661,7 @@ class LibraryController {
|
|||||||
longestItems: durationStats.longestItems,
|
longestItems: durationStats.longestItems,
|
||||||
numAudioTracks: durationStats.numAudioTracks,
|
numAudioTracks: durationStats.numAudioTracks,
|
||||||
totalSize: libraryHelpers.getLibraryItemsTotalSize(libraryItems),
|
totalSize: libraryHelpers.getLibraryItemsTotalSize(libraryItems),
|
||||||
|
largestItems: sizeStats.largestItems,
|
||||||
authorsWithCount,
|
authorsWithCount,
|
||||||
genresWithCount
|
genresWithCount
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ class LibraryItemController {
|
|||||||
}).filter(au => au)
|
}).filter(au => au)
|
||||||
}
|
}
|
||||||
} else if (includeEntities.includes('downloads')) {
|
} else if (includeEntities.includes('downloads')) {
|
||||||
var downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(req.libraryItem.id)
|
const downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(req.libraryItem.id)
|
||||||
item.episodesDownloading = downloadsInQueue.map(d => d.toJSONForClient())
|
item.episodeDownloadsQueued = downloadsInQueue.map(d => d.toJSONForClient())
|
||||||
|
if (this.podcastManager.currentDownload?.libraryItemId === req.libraryItem.id) {
|
||||||
|
item.episodesDownloading = [this.podcastManager.currentDownload.toJSONForClient()]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(item)
|
return res.json(item)
|
||||||
|
|||||||
@@ -256,13 +256,13 @@ class MeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/me/items-in-progress
|
// GET: api/me/items-in-progress
|
||||||
async getAllLibraryItemsInProgress(req, res) {
|
getAllLibraryItemsInProgress(req, res) {
|
||||||
const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25
|
const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25
|
||||||
|
|
||||||
var itemsInProgress = []
|
let itemsInProgress = []
|
||||||
for (const mediaProgress of req.user.mediaProgress) {
|
for (const mediaProgress of req.user.mediaProgress) {
|
||||||
if (!mediaProgress.isFinished && mediaProgress.progress > 0) {
|
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
||||||
const libraryItem = await this.db.getLibraryItem(mediaProgress.libraryItemId)
|
const libraryItem = this.db.getLibraryItem(mediaProgress.libraryItemId)
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
||||||
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
||||||
|
|||||||
@@ -90,9 +90,19 @@ class MiscController {
|
|||||||
|
|
||||||
// GET: api/tasks
|
// GET: api/tasks
|
||||||
getTasks(req, res) {
|
getTasks(req, res) {
|
||||||
res.json({
|
const includeArray = (req.query.include || '').split(',')
|
||||||
|
|
||||||
|
const data = {
|
||||||
tasks: this.taskManager.tasks.map(t => t.toJSON())
|
tasks: this.taskManager.tasks.map(t => t.toJSON())
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (includeArray.includes('queue')) {
|
||||||
|
data.queuedTaskData = {
|
||||||
|
embedMetadata: this.audioMetadataManager.getQueuedTaskData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/settings (admin)
|
// PATCH: api/settings (admin)
|
||||||
|
|||||||
@@ -225,6 +225,20 @@ class PodcastController {
|
|||||||
res.json(libraryItem.toJSONExpanded())
|
res.json(libraryItem.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/podcasts/:id/episode/:episodeId
|
||||||
|
async getEpisode(req, res) {
|
||||||
|
const episodeId = req.params.episodeId
|
||||||
|
const libraryItem = req.libraryItem
|
||||||
|
|
||||||
|
const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
|
||||||
|
if (!episode) {
|
||||||
|
Logger.error(`[PodcastController] getEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(episode)
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE: api/podcasts/:id/episode/:episodeId
|
// DELETE: api/podcasts/:id/episode/:episodeId
|
||||||
async removeEpisode(req, res) {
|
async removeEpisode(req, res) {
|
||||||
var episodeId = req.params.episodeId
|
var episodeId = req.params.episodeId
|
||||||
|
|||||||
@@ -3,14 +3,8 @@ const Logger = require('../Logger')
|
|||||||
class ToolsController {
|
class ToolsController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
|
||||||
// POST: api/tools/item/:id/encode-m4b
|
// POST: api/tools/item/:id/encode-m4b
|
||||||
async encodeM4b(req, res) {
|
async encodeM4b(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
|
||||||
Logger.error('[MiscController] encodeM4b: Non-admin user attempting to make m4b', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.libraryItem.isMissing || req.libraryItem.isInvalid) {
|
if (req.libraryItem.isMissing || req.libraryItem.isInvalid) {
|
||||||
Logger.error(`[MiscController] encodeM4b: library item not found or invalid ${req.params.id}`)
|
Logger.error(`[MiscController] encodeM4b: library item not found or invalid ${req.params.id}`)
|
||||||
return res.status(404).send('Audiobook not found')
|
return res.status(404).send('Audiobook not found')
|
||||||
@@ -34,11 +28,6 @@ class ToolsController {
|
|||||||
|
|
||||||
// DELETE: api/tools/item/:id/encode-m4b
|
// DELETE: api/tools/item/:id/encode-m4b
|
||||||
async cancelM4bEncode(req, res) {
|
async cancelM4bEncode(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
|
||||||
Logger.error('[MiscController] cancelM4bEncode: Non-admin user attempting to cancel m4b encode', req.user)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
const workerTask = this.abMergeManager.getPendingTaskByLibraryItemId(req.params.id)
|
const workerTask = this.abMergeManager.getPendingTaskByLibraryItemId(req.params.id)
|
||||||
if (!workerTask) return res.sendStatus(404)
|
if (!workerTask) return res.sendStatus(404)
|
||||||
|
|
||||||
@@ -49,14 +38,14 @@ class ToolsController {
|
|||||||
|
|
||||||
// POST: api/tools/item/:id/embed-metadata
|
// POST: api/tools/item/:id/embed-metadata
|
||||||
async embedAudioFileMetadata(req, res) {
|
async embedAudioFileMetadata(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
|
||||||
Logger.error(`[LibraryItemController] Non-root user attempted to update audio metadata`, req.user)
|
Logger.error(`[ToolsController] Invalid library item`)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
|
if (this.audioMetadataManager.getIsLibraryItemQueuedOrProcessing(req.libraryItem.id)) {
|
||||||
Logger.error(`[LibraryItemController] Invalid library item`)
|
Logger.error(`[ToolsController] Library item (${req.libraryItem.id}) is already in queue or processing`)
|
||||||
return res.sendStatus(500)
|
return res.status(500).send('Library item is already in queue or processing')
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -67,16 +56,66 @@ class ToolsController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
itemMiddleware(req, res, next) {
|
// POST: api/tools/batch/embed-metadata
|
||||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
async batchEmbedMetadata(req, res) {
|
||||||
if (!item || !item.media) return res.sendStatus(404)
|
const libraryItemIds = req.body.libraryItemIds || []
|
||||||
|
if (!libraryItemIds.length) {
|
||||||
|
return res.status(400).send('Invalid request payload')
|
||||||
|
}
|
||||||
|
|
||||||
// Check user can access this library item
|
const libraryItems = []
|
||||||
if (!req.user.checkCanAccessLibraryItem(item)) {
|
for (const libraryItemId of libraryItemIds) {
|
||||||
|
const libraryItem = this.db.getLibraryItem(libraryItemId)
|
||||||
|
if (!libraryItem) {
|
||||||
|
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not found`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user can access this library item
|
||||||
|
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||||
|
Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (libraryItem.isMissing || !libraryItem.hasAudioFiles || !libraryItem.isBook) {
|
||||||
|
Logger.error(`[ToolsController] Batch embed invalid library item (${libraryItemId})`)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.audioMetadataManager.getIsLibraryItemQueuedOrProcessing(libraryItemId)) {
|
||||||
|
Logger.error(`[ToolsController] Batch embed library item (${libraryItemId}) is already in queue or processing`)
|
||||||
|
return res.status(500).send('Library item is already in queue or processing')
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryItems.push(libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
forceEmbedChapters: req.query.forceEmbedChapters === '1',
|
||||||
|
backup: req.query.backup === '1'
|
||||||
|
}
|
||||||
|
this.audioMetadataManager.handleBatchEmbed(req.user, libraryItems, options)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.libraryItem = item
|
if (req.params.id) {
|
||||||
|
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
|
if (!item || !item.media) return res.sendStatus(404)
|
||||||
|
|
||||||
|
// Check user can access this library item
|
||||||
|
if (!req.user.checkCanAccessLibraryItem(item)) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.libraryItem = item
|
||||||
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ class UserController {
|
|||||||
findAll(req, res) {
|
findAll(req, res) {
|
||||||
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))
|
|
||||||
res.json({
|
res.json({
|
||||||
users: users
|
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
||||||
|
users: this.db.users.map(u => u.toJSONForBrowser(hideRootToken, true))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,42 @@ const Logger = require('../Logger')
|
|||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
const { secondsToTimestamp } = require('../utils/index')
|
|
||||||
const toneHelpers = require('../utils/toneHelpers')
|
const toneHelpers = require('../utils/toneHelpers')
|
||||||
const filePerms = require('../utils/filePerms')
|
|
||||||
|
const Task = require('../objects/Task')
|
||||||
|
|
||||||
class AudioMetadataMangaer {
|
class AudioMetadataMangaer {
|
||||||
constructor(db, taskManager) {
|
constructor(db, taskManager) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.taskManager = taskManager
|
this.taskManager = taskManager
|
||||||
|
|
||||||
|
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||||
|
|
||||||
|
this.MAX_CONCURRENT_TASKS = 1
|
||||||
|
this.tasksRunning = []
|
||||||
|
this.tasksQueued = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get queued task data
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
getQueuedTaskData() {
|
||||||
|
return this.tasksQueued.map(t => t.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
getIsLibraryItemQueuedOrProcessing(libraryItemId) {
|
||||||
|
return this.tasksQueued.some(t => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some(t => t.data.libraryItemId === libraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
getToneMetadataObjectForApi(libraryItem) {
|
getToneMetadataObjectForApi(libraryItem) {
|
||||||
return toneHelpers.getToneMetadataObject(libraryItem)
|
return toneHelpers.getToneMetadataObject(libraryItem, libraryItem.media.chapters, libraryItem.media.tracks.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBatchEmbed(user, libraryItems, options = {}) {
|
||||||
|
libraryItems.forEach((li) => {
|
||||||
|
this.updateMetadataForItem(user, li, options)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMetadataForItem(user, libraryItem, options = {}) {
|
async updateMetadataForItem(user, libraryItem, options = {}) {
|
||||||
@@ -25,99 +49,144 @@ class AudioMetadataMangaer {
|
|||||||
|
|
||||||
const audioFiles = libraryItem.media.includedAudioFiles
|
const audioFiles = libraryItem.media.includedAudioFiles
|
||||||
|
|
||||||
const itemAudioMetadataPayload = {
|
const task = new Task()
|
||||||
userId: user.id,
|
|
||||||
|
const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id)
|
||||||
|
|
||||||
|
// Only writing chapters for single file audiobooks
|
||||||
|
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters.map(c => ({ ...c })) : null
|
||||||
|
|
||||||
|
// Create task
|
||||||
|
const taskData = {
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
startedAt: Date.now(),
|
libraryItemPath: libraryItem.path,
|
||||||
audioFiles: audioFiles.map(af => ({ index: af.index, ino: af.ino, filename: af.metadata.filename }))
|
userId: user.id,
|
||||||
|
audioFiles: audioFiles.map(af => (
|
||||||
|
{
|
||||||
|
index: af.index,
|
||||||
|
ino: af.ino,
|
||||||
|
filename: af.metadata.filename,
|
||||||
|
path: af.metadata.path,
|
||||||
|
cachePath: Path.join(itemCachePath, af.metadata.filename)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
coverPath: libraryItem.media.coverPath,
|
||||||
|
metadataObject: toneHelpers.getToneMetadataObject(libraryItem, chapters, audioFiles.length),
|
||||||
|
itemCachePath,
|
||||||
|
chapters,
|
||||||
|
options: {
|
||||||
|
forceEmbedChapters,
|
||||||
|
backupFiles
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const taskDescription = `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".`
|
||||||
|
task.setData('embed-metadata', 'Embedding Metadata', taskDescription, taskData)
|
||||||
|
|
||||||
SocketAuthority.emitter('audio_metadata_started', itemAudioMetadataPayload)
|
if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) {
|
||||||
|
Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.metadata.title}"`)
|
||||||
|
SocketAuthority.adminEmitter('metadata_embed_queue_update', {
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
queued: true
|
||||||
|
})
|
||||||
|
this.tasksQueued.push(task)
|
||||||
|
} else {
|
||||||
|
this.runMetadataEmbed(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure folder for backup files
|
async runMetadataEmbed(task) {
|
||||||
const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${libraryItem.id}`)
|
this.tasksRunning.push(task)
|
||||||
|
this.taskManager.addTask(task)
|
||||||
|
|
||||||
|
Logger.info(`[AudioMetadataManager] Starting metadata embed task`, task.description)
|
||||||
|
|
||||||
|
// Ensure item cache dir exists
|
||||||
let cacheDirCreated = false
|
let cacheDirCreated = false
|
||||||
if (!await fs.pathExists(itemCacheDir)) {
|
if (!await fs.pathExists(task.data.itemCachePath)) {
|
||||||
await fs.mkdir(itemCacheDir)
|
await fs.mkdir(task.data.itemCachePath)
|
||||||
await filePerms.setDefault(itemCacheDir, true)
|
|
||||||
cacheDirCreated = true
|
cacheDirCreated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write chapters file
|
// Create metadata json file
|
||||||
const toneJsonPath = Path.join(itemCacheDir, 'metadata.json')
|
const toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters : null
|
await fs.writeFile(toneJsonPath, JSON.stringify({ meta: task.data.metadataObject }, null, 2))
|
||||||
await toneHelpers.writeToneMetadataJsonFile(libraryItem, chapters, toneJsonPath, audioFiles.length)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error)
|
Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error)
|
||||||
|
task.setFailed('Failed to write metadata.json')
|
||||||
itemAudioMetadataPayload.failed = true
|
this.handleTaskFinished(task)
|
||||||
itemAudioMetadataPayload.error = 'Failed to write metadata.json'
|
|
||||||
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = []
|
// Tag audio files
|
||||||
for (const af of audioFiles) {
|
for (const af of task.data.audioFiles) {
|
||||||
const result = await this.updateAudioFileMetadataWithTone(libraryItem, af, toneJsonPath, itemCacheDir, backupFiles)
|
SocketAuthority.adminEmitter('audiofile_metadata_started', {
|
||||||
results.push(result)
|
libraryItemId: task.data.libraryItemId,
|
||||||
|
ino: af.ino
|
||||||
|
})
|
||||||
|
|
||||||
|
// Backup audio file
|
||||||
|
if (task.data.options.backupFiles) {
|
||||||
|
try {
|
||||||
|
const backupFilePath = Path.join(task.data.itemCachePath, af.filename)
|
||||||
|
await fs.copy(af.path, backupFilePath)
|
||||||
|
Logger.debug(`[AudioMetadataManager] Backed up audio file at "${backupFilePath}"`)
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`[AudioMetadataManager] Failed to backup audio file "${af.path}"`, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _toneMetadataObject = {
|
||||||
|
'ToneJsonFile': toneJsonPath,
|
||||||
|
'TrackNumber': af.index,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.data.coverPath) {
|
||||||
|
_toneMetadataObject['CoverFile'] = task.data.coverPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await toneHelpers.tagAudioFile(af.path, _toneMetadataObject)
|
||||||
|
if (success) {
|
||||||
|
Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${af.path}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
SocketAuthority.adminEmitter('audiofile_metadata_finished', {
|
||||||
|
libraryItemId: task.data.libraryItemId,
|
||||||
|
ino: af.ino
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove temp cache file/folder if not backing up
|
// Remove temp cache file/folder if not backing up
|
||||||
if (!backupFiles) {
|
if (!task.data.options.backupFiles) {
|
||||||
// If cache dir was created from this then remove it
|
// If cache dir was created from this then remove it
|
||||||
if (cacheDirCreated) {
|
if (cacheDirCreated) {
|
||||||
await fs.remove(itemCacheDir)
|
await fs.remove(task.data.itemCachePath)
|
||||||
} else {
|
} else {
|
||||||
await fs.remove(toneJsonPath)
|
await fs.remove(toneJsonPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = Date.now() - itemAudioMetadataPayload.startedAt
|
task.setFinished()
|
||||||
Logger.debug(`[AudioMetadataManager] Elapsed ${secondsToTimestamp(elapsed / 1000, true)}`)
|
this.handleTaskFinished(task)
|
||||||
itemAudioMetadataPayload.results = results
|
|
||||||
itemAudioMetadataPayload.elapsed = elapsed
|
|
||||||
itemAudioMetadataPayload.finishedAt = Date.now()
|
|
||||||
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAudioFileMetadataWithTone(libraryItem, audioFile, toneJsonPath, itemCacheDir, backupFiles) {
|
handleTaskFinished(task) {
|
||||||
const resultPayload = {
|
this.taskManager.taskFinished(task)
|
||||||
libraryItemId: libraryItem.id,
|
this.tasksRunning = this.tasksRunning.filter(t => t.id !== task.id)
|
||||||
index: audioFile.index,
|
|
||||||
ino: audioFile.ino,
|
|
||||||
filename: audioFile.metadata.filename
|
|
||||||
}
|
|
||||||
SocketAuthority.emitter('audiofile_metadata_started', resultPayload)
|
|
||||||
|
|
||||||
// Backup audio file
|
if (this.tasksRunning.length < this.MAX_CONCURRENT_TASKS && this.tasksQueued.length) {
|
||||||
if (backupFiles) {
|
Logger.info(`[AudioMetadataManager] Task finished and dequeueing next task. ${this.tasksQueued} tasks queued.`)
|
||||||
try {
|
const nextTask = this.tasksQueued.shift()
|
||||||
const backupFilePath = Path.join(itemCacheDir, audioFile.metadata.filename)
|
SocketAuthority.emitter('metadata_embed_queue_update', {
|
||||||
await fs.copy(audioFile.metadata.path, backupFilePath)
|
libraryItemId: nextTask.data.libraryItemId,
|
||||||
Logger.debug(`[AudioMetadataManager] Backed up audio file at "${backupFilePath}"`)
|
queued: false
|
||||||
} catch (err) {
|
})
|
||||||
Logger.error(`[AudioMetadataManager] Failed to backup audio file "${audioFile.metadata.path}"`, err)
|
this.runMetadataEmbed(nextTask)
|
||||||
}
|
} else if (this.tasksRunning.length > 0) {
|
||||||
|
Logger.debug(`[AudioMetadataManager] Task finished but not dequeueing. Currently running ${this.tasksRunning.length} tasks. ${this.tasksQueued.length} tasks queued.`)
|
||||||
|
} else {
|
||||||
|
Logger.debug(`[AudioMetadataManager] Task finished and no tasks remain in queue`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const _toneMetadataObject = {
|
|
||||||
'ToneJsonFile': toneJsonPath,
|
|
||||||
'TrackNumber': audioFile.index,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (libraryItem.media.coverPath) {
|
|
||||||
_toneMetadataObject['CoverFile'] = libraryItem.media.coverPath
|
|
||||||
}
|
|
||||||
|
|
||||||
resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject)
|
|
||||||
if (resultPayload.success) {
|
|
||||||
Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${audioFile.metadata.path}"`)
|
|
||||||
}
|
|
||||||
|
|
||||||
SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
|
|
||||||
return resultPayload
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = AudioMetadataMangaer
|
module.exports = AudioMetadataMangaer
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ class NotificationManager {
|
|||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
libraryId: libraryItem.libraryId,
|
libraryId: libraryItem.libraryId,
|
||||||
libraryName: library ? library.name : 'Unknown',
|
libraryName: library ? library.name : 'Unknown',
|
||||||
|
mediaTags: (libraryItem.media.tags || []).join(', '),
|
||||||
podcastTitle: libraryItem.media.metadata.title,
|
podcastTitle: libraryItem.media.metadata.title,
|
||||||
|
podcastAuthor: libraryItem.media.metadata.author || '',
|
||||||
|
podcastDescription: libraryItem.media.metadata.description || '',
|
||||||
|
podcastGenres: (libraryItem.media.metadata.genres || []).join(', '),
|
||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
episodeTitle: episode.title
|
episodeTitle: episode.title,
|
||||||
|
episodeSubtitle: episode.subtitle || '',
|
||||||
|
episodeDescription: episode.description || ''
|
||||||
}
|
}
|
||||||
this.triggerNotification('onPodcastEpisodeDownloaded', eventData)
|
this.triggerNotification('onPodcastEpisodeDownloaded', eventData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,22 +4,25 @@ const SocketAuthority = require('../SocketAuthority')
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
const { getPodcastFeed } = require('../utils/podcastUtils')
|
const { getPodcastFeed } = require('../utils/podcastUtils')
|
||||||
const { downloadFile, removeFile } = require('../utils/fileUtils')
|
const { removeFile, downloadFile } = require('../utils/fileUtils')
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const opmlParser = require('../utils/parsers/parseOPML')
|
const opmlParser = require('../utils/parsers/parseOPML')
|
||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
|
const ffmpegHelpers = require('../utils/ffmpegHelpers')
|
||||||
|
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
||||||
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
|
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
|
||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
|
const Task = require("../objects/Task")
|
||||||
|
|
||||||
class PodcastManager {
|
class PodcastManager {
|
||||||
constructor(db, watcher, notificationManager) {
|
constructor(db, watcher, notificationManager, taskManager) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.watcher = watcher
|
this.watcher = watcher
|
||||||
this.notificationManager = notificationManager
|
this.notificationManager = notificationManager
|
||||||
|
this.taskManager = taskManager
|
||||||
|
|
||||||
this.downloadQueue = []
|
this.downloadQueue = []
|
||||||
this.currentDownload = null
|
this.currentDownload = null
|
||||||
@@ -56,18 +59,28 @@ class PodcastManager {
|
|||||||
newPe.setData(ep, index++)
|
newPe.setData(ep, index++)
|
||||||
newPe.libraryItemId = libraryItem.id
|
newPe.libraryItemId = libraryItem.id
|
||||||
var newPeDl = new PodcastEpisodeDownload()
|
var newPeDl = new PodcastEpisodeDownload()
|
||||||
newPeDl.setData(newPe, libraryItem, isAutoDownload)
|
newPeDl.setData(newPe, libraryItem, isAutoDownload, libraryItem.libraryId)
|
||||||
this.startPodcastEpisodeDownload(newPeDl)
|
this.startPodcastEpisodeDownload(newPeDl)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
|
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
|
||||||
|
SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails())
|
||||||
if (this.currentDownload) {
|
if (this.currentDownload) {
|
||||||
this.downloadQueue.push(podcastEpisodeDownload)
|
this.downloadQueue.push(podcastEpisodeDownload)
|
||||||
SocketAuthority.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient())
|
SocketAuthority.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const task = new Task()
|
||||||
|
const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".`
|
||||||
|
const taskData = {
|
||||||
|
libraryId: podcastEpisodeDownload.libraryId,
|
||||||
|
libraryItemId: podcastEpisodeDownload.libraryItemId,
|
||||||
|
}
|
||||||
|
task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, taskData)
|
||||||
|
this.taskManager.addTask(task)
|
||||||
|
|
||||||
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
||||||
this.currentDownload = podcastEpisodeDownload
|
this.currentDownload = podcastEpisodeDownload
|
||||||
|
|
||||||
@@ -81,24 +94,42 @@ class PodcastManager {
|
|||||||
await filePerms.setDefault(this.currentDownload.libraryItem.path)
|
await filePerms.setDefault(this.currentDownload.libraryItem.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath).then(() => true).catch((error) => {
|
|
||||||
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
|
let success = false
|
||||||
return false
|
if (this.currentDownload.urlFileExtension === 'mp3') {
|
||||||
})
|
// Download episode and tag it
|
||||||
|
success = await ffmpegHelpers.downloadPodcastEpisode(this.currentDownload).catch((error) => {
|
||||||
|
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Download episode only
|
||||||
|
success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath).then(() => true).catch((error) => {
|
||||||
|
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
success = await this.scanAddPodcastEpisodeAudioFile()
|
success = await this.scanAddPodcastEpisodeAudioFile()
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await fs.remove(this.currentDownload.targetPath)
|
await fs.remove(this.currentDownload.targetPath)
|
||||||
this.currentDownload.setFinished(false)
|
this.currentDownload.setFinished(false)
|
||||||
|
task.setFailed('Failed to download episode')
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[PodcastManager] Successfully downloaded podcast episode "${this.currentDownload.podcastEpisode.title}"`)
|
Logger.info(`[PodcastManager] Successfully downloaded podcast episode "${this.currentDownload.podcastEpisode.title}"`)
|
||||||
this.currentDownload.setFinished(true)
|
this.currentDownload.setFinished(true)
|
||||||
|
task.setFinished()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
task.setFailed('Failed to download episode')
|
||||||
this.currentDownload.setFinished(false)
|
this.currentDownload.setFinished(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.taskManager.taskFinished(task)
|
||||||
|
|
||||||
SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
|
SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
|
||||||
|
SocketAuthority.emitter('episode_download_queue_updated', this.getDownloadQueueDetails())
|
||||||
|
|
||||||
this.watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
|
this.watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
|
||||||
this.currentDownload = null
|
this.currentDownload = null
|
||||||
@@ -108,22 +139,22 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanAddPodcastEpisodeAudioFile() {
|
async scanAddPodcastEpisodeAudioFile() {
|
||||||
var libraryFile = await this.getLibraryFile(this.currentDownload.targetPath, this.currentDownload.targetRelPath)
|
const libraryFile = await this.getLibraryFile(this.currentDownload.targetPath, this.currentDownload.targetRelPath)
|
||||||
|
|
||||||
// TODO: Set meta tags on new audio file
|
// TODO: Set meta tags on new audio file
|
||||||
|
|
||||||
var audioFile = await this.probeAudioFile(libraryFile)
|
const audioFile = await this.probeAudioFile(libraryFile)
|
||||||
if (!audioFile) {
|
if (!audioFile) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
const libraryItem = this.db.libraryItems.find(li => li.id === this.currentDownload.libraryItem.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
|
Logger.error(`[PodcastManager] Podcast Episode finished but library item was not found ${this.currentDownload.libraryItem.id}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var podcastEpisode = this.currentDownload.podcastEpisode
|
const podcastEpisode = this.currentDownload.podcastEpisode
|
||||||
podcastEpisode.audioFile = audioFile
|
podcastEpisode.audioFile = audioFile
|
||||||
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
||||||
if (libraryItem.isInvalid) {
|
if (libraryItem.isInvalid) {
|
||||||
@@ -329,5 +360,15 @@ class PodcastManager {
|
|||||||
feeds: rssFeedData
|
feeds: rssFeedData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDownloadQueueDetails(libraryId = null) {
|
||||||
|
let _currentDownload = this.currentDownload
|
||||||
|
if (libraryId && _currentDownload?.libraryId !== libraryId) _currentDownload = null
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentDownload: _currentDownload?.toJSONForClient(),
|
||||||
|
queue: this.downloadQueue.filter(item => !libraryId || item.libraryId === libraryId).map(item => item.toJSONForClient())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastManager
|
module.exports = PodcastManager
|
||||||
@@ -188,9 +188,12 @@ class RssFeedManager {
|
|||||||
async openFeedForItem(user, libraryItem, options) {
|
async openFeedForItem(user, libraryItem, options) {
|
||||||
const serverAddress = options.serverAddress
|
const serverAddress = options.serverAddress
|
||||||
const slug = options.slug
|
const slug = options.slug
|
||||||
|
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
|
||||||
|
const ownerName = options.metadataDetails?.ownerName
|
||||||
|
const ownerEmail = options.metadataDetails?.ownerEmail
|
||||||
|
|
||||||
const feed = new Feed()
|
const feed = new Feed()
|
||||||
feed.setFromItem(user.id, slug, libraryItem, serverAddress)
|
feed.setFromItem(user.id, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail)
|
||||||
this.feeds[feed.id] = feed
|
this.feeds[feed.id] = feed
|
||||||
|
|
||||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||||
@@ -202,9 +205,12 @@ class RssFeedManager {
|
|||||||
async openFeedForCollection(user, collectionExpanded, options) {
|
async openFeedForCollection(user, collectionExpanded, options) {
|
||||||
const serverAddress = options.serverAddress
|
const serverAddress = options.serverAddress
|
||||||
const slug = options.slug
|
const slug = options.slug
|
||||||
|
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
|
||||||
|
const ownerName = options.metadataDetails?.ownerName
|
||||||
|
const ownerEmail = options.metadataDetails?.ownerEmail
|
||||||
|
|
||||||
const feed = new Feed()
|
const feed = new Feed()
|
||||||
feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress)
|
feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
|
||||||
this.feeds[feed.id] = feed
|
this.feeds[feed.id] = feed
|
||||||
|
|
||||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||||
@@ -216,9 +222,12 @@ class RssFeedManager {
|
|||||||
async openFeedForSeries(user, seriesExpanded, options) {
|
async openFeedForSeries(user, seriesExpanded, options) {
|
||||||
const serverAddress = options.serverAddress
|
const serverAddress = options.serverAddress
|
||||||
const slug = options.slug
|
const slug = options.slug
|
||||||
|
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
|
||||||
|
const ownerName = options.metadataDetails?.ownerName
|
||||||
|
const ownerEmail = options.metadataDetails?.ownerEmail
|
||||||
|
|
||||||
const feed = new Feed()
|
const feed = new Feed()
|
||||||
feed.setFromSeries(user.id, slug, seriesExpanded, serverAddress)
|
feed.setFromSeries(user.id, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
|
||||||
this.feeds[feed.id] = feed
|
this.feeds[feed.id] = feed
|
||||||
|
|
||||||
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
Logger.debug(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
||||||
|
|||||||
+11
-2
@@ -70,17 +70,19 @@ class Feed {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
entityType: this.entityType,
|
entityType: this.entityType,
|
||||||
entityId: this.entityId,
|
entityId: this.entityId,
|
||||||
feedUrl: this.feedUrl
|
feedUrl: this.feedUrl,
|
||||||
|
meta: this.meta.toJSONMinified(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEpisodePath(id) {
|
getEpisodePath(id) {
|
||||||
var episode = this.episodes.find(ep => ep.id === id)
|
var episode = this.episodes.find(ep => ep.id === id)
|
||||||
|
console.log('getEpisodePath=', id, episode)
|
||||||
if (!episode) return null
|
if (!episode) return null
|
||||||
return episode.fullPath
|
return episode.fullPath
|
||||||
}
|
}
|
||||||
|
|
||||||
setFromItem(userId, slug, libraryItem, serverAddress) {
|
setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) {
|
||||||
const media = libraryItem.media
|
const media = libraryItem.media
|
||||||
const mediaMetadata = media.metadata
|
const mediaMetadata = media.metadata
|
||||||
const isPodcast = libraryItem.mediaType === 'podcast'
|
const isPodcast = libraryItem.mediaType === 'podcast'
|
||||||
@@ -106,6 +108,11 @@ class Feed {
|
|||||||
this.meta.feedUrl = feedUrl
|
this.meta.feedUrl = feedUrl
|
||||||
this.meta.link = `${serverAddress}/item/${libraryItem.id}`
|
this.meta.link = `${serverAddress}/item/${libraryItem.id}`
|
||||||
this.meta.explicit = !!mediaMetadata.explicit
|
this.meta.explicit = !!mediaMetadata.explicit
|
||||||
|
this.meta.type = mediaMetadata.type
|
||||||
|
this.meta.language = mediaMetadata.language
|
||||||
|
this.meta.preventIndexing = preventIndexing
|
||||||
|
this.meta.ownerName = ownerName
|
||||||
|
this.meta.ownerEmail = ownerEmail
|
||||||
|
|
||||||
this.episodes = []
|
this.episodes = []
|
||||||
if (isPodcast) { // PODCAST EPISODES
|
if (isPodcast) { // PODCAST EPISODES
|
||||||
@@ -142,6 +149,8 @@ class Feed {
|
|||||||
this.meta.author = author
|
this.meta.author = author
|
||||||
this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png`
|
this.meta.imageUrl = media.coverPath ? `${this.serverAddress}/feed/${this.slug}/cover` : `${this.serverAddress}/Logo.png`
|
||||||
this.meta.explicit = !!mediaMetadata.explicit
|
this.meta.explicit = !!mediaMetadata.explicit
|
||||||
|
this.meta.type = mediaMetadata.type
|
||||||
|
this.meta.language = mediaMetadata.language
|
||||||
|
|
||||||
this.episodes = []
|
this.episodes = []
|
||||||
if (isPodcast) { // PODCAST EPISODES
|
if (isPodcast) { // PODCAST EPISODES
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ class FeedEpisode {
|
|||||||
this.author = null
|
this.author = null
|
||||||
this.explicit = null
|
this.explicit = null
|
||||||
this.duration = null
|
this.duration = null
|
||||||
|
this.season = null
|
||||||
|
this.episode = null
|
||||||
|
this.episodeType = null
|
||||||
|
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
this.episodeId = null
|
this.episodeId = null
|
||||||
@@ -35,6 +38,9 @@ class FeedEpisode {
|
|||||||
this.author = episode.author
|
this.author = episode.author
|
||||||
this.explicit = episode.explicit
|
this.explicit = episode.explicit
|
||||||
this.duration = episode.duration
|
this.duration = episode.duration
|
||||||
|
this.season = episode.season
|
||||||
|
this.episode = episode.episode
|
||||||
|
this.episodeType = episode.episodeType
|
||||||
this.libraryItemId = episode.libraryItemId
|
this.libraryItemId = episode.libraryItemId
|
||||||
this.episodeId = episode.episodeId || null
|
this.episodeId = episode.episodeId || null
|
||||||
this.trackIndex = episode.trackIndex || 0
|
this.trackIndex = episode.trackIndex || 0
|
||||||
@@ -52,6 +58,9 @@ class FeedEpisode {
|
|||||||
author: this.author,
|
author: this.author,
|
||||||
explicit: this.explicit,
|
explicit: this.explicit,
|
||||||
duration: this.duration,
|
duration: this.duration,
|
||||||
|
season: this.season,
|
||||||
|
episode: this.episode,
|
||||||
|
episodeType: this.episodeType,
|
||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
episodeId: this.episodeId,
|
episodeId: this.episodeId,
|
||||||
trackIndex: this.trackIndex,
|
trackIndex: this.trackIndex,
|
||||||
@@ -77,25 +86,31 @@ class FeedEpisode {
|
|||||||
this.author = meta.author
|
this.author = meta.author
|
||||||
this.explicit = mediaMetadata.explicit
|
this.explicit = mediaMetadata.explicit
|
||||||
this.duration = episode.duration
|
this.duration = episode.duration
|
||||||
|
this.season = episode.season
|
||||||
|
this.episode = episode.episode
|
||||||
|
this.episodeType = episode.episodeType
|
||||||
this.libraryItemId = libraryItem.id
|
this.libraryItemId = libraryItem.id
|
||||||
this.episodeId = episode.id
|
this.episodeId = episode.id
|
||||||
this.trackIndex = 0
|
this.trackIndex = 0
|
||||||
this.fullPath = episode.audioFile.metadata.path
|
this.fullPath = episode.audioFile.metadata.path
|
||||||
}
|
}
|
||||||
|
|
||||||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = 0) {
|
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = null) {
|
||||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||||
let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
||||||
|
let episodeId = String(audioTrack.index)
|
||||||
|
|
||||||
// Additional offset can be used for collections/series
|
// Additional offset can be used for collections/series
|
||||||
if (additionalOffset && !isNaN(additionalOffset)) {
|
if (additionalOffset !== null && !isNaN(additionalOffset)) {
|
||||||
timeOffset += Number(additionalOffset) * 1000
|
timeOffset += Number(additionalOffset) * 1000
|
||||||
|
|
||||||
|
episodeId = String(additionalOffset) + '-' + episodeId
|
||||||
}
|
}
|
||||||
|
|
||||||
// e.g. Track 1 will have a pub date before Track 2
|
// 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 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/${episodeId}/${audioTrack.metadata.filename}`
|
||||||
const media = libraryItem.media
|
const media = libraryItem.media
|
||||||
const mediaMetadata = media.metadata
|
const mediaMetadata = media.metadata
|
||||||
|
|
||||||
@@ -110,7 +125,7 @@ class FeedEpisode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.id = String(audioTrack.index)
|
this.id = episodeId
|
||||||
this.title = title
|
this.title = title
|
||||||
this.description = mediaMetadata.description || ''
|
this.description = mediaMetadata.description || ''
|
||||||
this.enclosure = {
|
this.enclosure = {
|
||||||
@@ -144,7 +159,10 @@ class FeedEpisode {
|
|||||||
{ 'itunes:summary': this.description || '' },
|
{ 'itunes:summary': this.description || '' },
|
||||||
{
|
{
|
||||||
"itunes:explicit": !!this.explicit
|
"itunes:explicit": !!this.explicit
|
||||||
}
|
},
|
||||||
|
{ "itunes:episodeType": this.episodeType },
|
||||||
|
{ "itunes:season": this.season },
|
||||||
|
{ "itunes:episode": this.episode }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ class FeedMeta {
|
|||||||
this.feedUrl = null
|
this.feedUrl = null
|
||||||
this.link = null
|
this.link = null
|
||||||
this.explicit = null
|
this.explicit = null
|
||||||
|
this.type = null
|
||||||
|
this.language = null
|
||||||
|
this.preventIndexing = null
|
||||||
|
this.ownerName = null
|
||||||
|
this.ownerEmail = null
|
||||||
|
|
||||||
if (meta) {
|
if (meta) {
|
||||||
this.construct(meta)
|
this.construct(meta)
|
||||||
@@ -21,6 +26,11 @@ class FeedMeta {
|
|||||||
this.feedUrl = meta.feedUrl
|
this.feedUrl = meta.feedUrl
|
||||||
this.link = meta.link
|
this.link = meta.link
|
||||||
this.explicit = meta.explicit
|
this.explicit = meta.explicit
|
||||||
|
this.type = meta.type
|
||||||
|
this.language = meta.language
|
||||||
|
this.preventIndexing = meta.preventIndexing
|
||||||
|
this.ownerName = meta.ownerName
|
||||||
|
this.ownerEmail = meta.ownerEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
@@ -31,7 +41,22 @@ class FeedMeta {
|
|||||||
imageUrl: this.imageUrl,
|
imageUrl: this.imageUrl,
|
||||||
feedUrl: this.feedUrl,
|
feedUrl: this.feedUrl,
|
||||||
link: this.link,
|
link: this.link,
|
||||||
explicit: this.explicit
|
explicit: this.explicit,
|
||||||
|
type: this.type,
|
||||||
|
language: this.language,
|
||||||
|
preventIndexing: this.preventIndexing,
|
||||||
|
ownerName: this.ownerName,
|
||||||
|
ownerEmail: this.ownerEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSONMinified() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
preventIndexing: this.preventIndexing,
|
||||||
|
ownerName: this.ownerName,
|
||||||
|
ownerEmail: this.ownerEmail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,16 +68,18 @@ class FeedMeta {
|
|||||||
feed_url: this.feedUrl,
|
feed_url: this.feedUrl,
|
||||||
site_url: this.link,
|
site_url: this.link,
|
||||||
image_url: this.imageUrl,
|
image_url: this.imageUrl,
|
||||||
language: 'en',
|
|
||||||
custom_namespaces: {
|
custom_namespaces: {
|
||||||
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd',
|
||||||
'psc': 'http://podlove.org/simple-chapters',
|
'psc': 'http://podlove.org/simple-chapters',
|
||||||
'podcast': 'https://podcastindex.org/namespace/1.0'
|
'podcast': 'https://podcastindex.org/namespace/1.0',
|
||||||
|
'googleplay': 'http://www.google.com/schemas/play-podcasts/1.0'
|
||||||
},
|
},
|
||||||
custom_elements: [
|
custom_elements: [
|
||||||
|
{ 'language': this.language || 'en' },
|
||||||
{ 'author': this.author || 'advplyr' },
|
{ 'author': this.author || 'advplyr' },
|
||||||
{ 'itunes:author': this.author || 'advplyr' },
|
{ 'itunes:author': this.author || 'advplyr' },
|
||||||
{ 'itunes:summary': this.description || '' },
|
{ 'itunes:summary': this.description || '' },
|
||||||
|
{ 'itunes:type': this.type },
|
||||||
{
|
{
|
||||||
'itunes:image': {
|
'itunes:image': {
|
||||||
_attr: {
|
_attr: {
|
||||||
@@ -62,13 +89,13 @@ class FeedMeta {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'itunes:owner': [
|
'itunes:owner': [
|
||||||
{ 'itunes:name': this.author || '' },
|
{ 'itunes:name': this.ownerName || this.author || '' },
|
||||||
{ 'itunes:email': '' }
|
{ 'itunes:email': this.ownerEmail || '' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{ 'itunes:explicit': !!this.explicit },
|
||||||
"itunes:explicit": !!this.explicit
|
{ 'itunes:block': this.preventIndexing?"Yes":"No" },
|
||||||
}
|
{ 'googleplay:block': this.preventIndexing?"yes":"no" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,9 +197,15 @@ class LibraryItem {
|
|||||||
if (key === 'libraryFiles') {
|
if (key === 'libraryFiles') {
|
||||||
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
|
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
|
||||||
|
|
||||||
// Use first image library file as cover
|
// Set cover image
|
||||||
const firstImageFile = this.libraryFiles.find(lf => lf.fileType === 'image')
|
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
|
||||||
if (firstImageFile) this.media.coverPath = firstImageFile.metadata.path
|
const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||||
|
if (coverMatch) {
|
||||||
|
this.media.coverPath = coverMatch.metadata.path
|
||||||
|
} else if (imageFiles.length) {
|
||||||
|
this.media.coverPath = imageFiles[0].metadata.path
|
||||||
|
}
|
||||||
|
|
||||||
} else if (this[key] !== undefined && key !== 'media') {
|
} else if (this[key] !== undefined && key !== 'media') {
|
||||||
this[key] = payload[key]
|
this[key] = payload[key]
|
||||||
}
|
}
|
||||||
@@ -330,6 +336,7 @@ class LibraryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataFound.ino !== this.ino) {
|
if (dataFound.ino !== this.ino) {
|
||||||
|
Logger.warn(`[LibraryItem] Check scan item changed inode "${this.ino}" -> "${dataFound.ino}"`)
|
||||||
this.ino = dataFound.ino
|
this.ino = dataFound.ino
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
@@ -341,7 +348,7 @@ class LibraryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dataFound.path !== this.path) {
|
if (dataFound.path !== this.path) {
|
||||||
Logger.warn(`[LibraryItem] Check scan item changed path "${this.path}" -> "${dataFound.path}"`)
|
Logger.warn(`[LibraryItem] Check scan item changed path "${this.path}" -> "${dataFound.path}" (inode ${this.ino})`)
|
||||||
this.path = dataFound.path
|
this.path = dataFound.path
|
||||||
this.relPath = dataFound.relPath
|
this.relPath = dataFound.relPath
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
@@ -444,8 +451,14 @@ class LibraryItem {
|
|||||||
// Set cover image if not set
|
// Set cover image if not set
|
||||||
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
|
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
|
||||||
if (imageFiles.length && !this.media.coverPath) {
|
if (imageFiles.length && !this.media.coverPath) {
|
||||||
this.media.coverPath = imageFiles[0].metadata.path
|
// attempt to find a file called cover.<ext> otherwise just fall back to the first image found
|
||||||
Logger.debug('[LibraryItem] Set media cover path', this.media.coverPath)
|
const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
|
||||||
|
if (coverMatch) {
|
||||||
|
this.media.coverPath = coverMatch.metadata.path
|
||||||
|
} else {
|
||||||
|
this.media.coverPath = imageFiles[0].metadata.path
|
||||||
|
}
|
||||||
|
Logger.info('[LibraryItem] Set media cover path', this.media.coverPath)
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { getId } = require('../utils/index')
|
const { getId } = require('../utils/index')
|
||||||
const { sanitizeFilename } = require('../utils/fileUtils')
|
const { sanitizeFilename } = require('../utils/fileUtils')
|
||||||
|
const globals = require('../utils/globals')
|
||||||
|
|
||||||
class PodcastEpisodeDownload {
|
class PodcastEpisodeDownload {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -8,9 +9,9 @@ class PodcastEpisodeDownload {
|
|||||||
this.podcastEpisode = null
|
this.podcastEpisode = null
|
||||||
this.url = null
|
this.url = null
|
||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
|
this.libraryId = null
|
||||||
|
|
||||||
this.isAutoDownload = false
|
this.isAutoDownload = false
|
||||||
this.isDownloading = false
|
|
||||||
this.isFinished = false
|
this.isFinished = false
|
||||||
this.failed = false
|
this.failed = false
|
||||||
|
|
||||||
@@ -22,20 +23,36 @@ class PodcastEpisodeDownload {
|
|||||||
toJSONForClient() {
|
toJSONForClient() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.title : null,
|
episodeDisplayTitle: this.podcastEpisode?.title ?? null,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
|
libraryItemId: this.libraryItem?.id || null,
|
||||||
isDownloading: this.isDownloading,
|
libraryId: this.libraryId || null,
|
||||||
isFinished: this.isFinished,
|
isFinished: this.isFinished,
|
||||||
failed: this.failed,
|
failed: this.failed,
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
finishedAt: this.finishedAt
|
finishedAt: this.finishedAt,
|
||||||
|
podcastTitle: this.libraryItem?.media.metadata.title ?? null,
|
||||||
|
podcastExplicit: !!this.libraryItem?.media.metadata.explicit,
|
||||||
|
season: this.podcastEpisode?.season ?? null,
|
||||||
|
episode: this.podcastEpisode?.episode ?? null,
|
||||||
|
episodeType: this.podcastEpisode?.episodeType ?? 'full',
|
||||||
|
publishedAt: this.podcastEpisode?.publishedAt ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get urlFileExtension() {
|
||||||
|
const cleanUrl = this.url.split('?')[0] // Remove query string
|
||||||
|
return Path.extname(cleanUrl).substring(1).toLowerCase()
|
||||||
|
}
|
||||||
|
get fileExtension() {
|
||||||
|
const extname = this.urlFileExtension
|
||||||
|
if (globals.SupportedAudioTypes.includes(extname)) return extname
|
||||||
|
return 'mp3'
|
||||||
|
}
|
||||||
|
|
||||||
get targetFilename() {
|
get targetFilename() {
|
||||||
return sanitizeFilename(`${this.podcastEpisode.title}.mp3`)
|
return sanitizeFilename(`${this.podcastEpisode.title}.${this.fileExtension}`)
|
||||||
}
|
}
|
||||||
get targetPath() {
|
get targetPath() {
|
||||||
return Path.join(this.libraryItem.path, this.targetFilename)
|
return Path.join(this.libraryItem.path, this.targetFilename)
|
||||||
@@ -47,13 +64,21 @@ class PodcastEpisodeDownload {
|
|||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(podcastEpisode, libraryItem, isAutoDownload) {
|
setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
|
||||||
this.id = getId('epdl')
|
this.id = getId('epdl')
|
||||||
this.podcastEpisode = podcastEpisode
|
this.podcastEpisode = podcastEpisode
|
||||||
this.url = podcastEpisode.enclosure.url
|
|
||||||
|
const url = podcastEpisode.enclosure.url
|
||||||
|
if (decodeURIComponent(url) !== url) { // Already encoded
|
||||||
|
this.url = url
|
||||||
|
} else {
|
||||||
|
this.url = encodeURI(url)
|
||||||
|
}
|
||||||
|
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.isAutoDownload = isAutoDownload
|
this.isAutoDownload = isAutoDownload
|
||||||
this.createdAt = Date.now()
|
this.createdAt = Date.now()
|
||||||
|
this.libraryId = libraryId
|
||||||
}
|
}
|
||||||
|
|
||||||
setFinished(success) {
|
setFinished(success) {
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ class Stream extends EventEmitter {
|
|||||||
AudioMimeType.WMA,
|
AudioMimeType.WMA,
|
||||||
AudioMimeType.AIFF,
|
AudioMimeType.AIFF,
|
||||||
AudioMimeType.WEBM,
|
AudioMimeType.WEBM,
|
||||||
AudioMimeType.WEBMA
|
AudioMimeType.WEBMA,
|
||||||
|
AudioMimeType.AWB
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
get codecsToForceAAC() {
|
get codecsToForceAAC() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
const Logger = require('../../Logger')
|
||||||
const { getId, cleanStringForSearch } = require('../../utils/index')
|
const { getId, cleanStringForSearch } = require('../../utils/index')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
const AudioTrack = require('../files/AudioTrack')
|
const AudioTrack = require('../files/AudioTrack')
|
||||||
@@ -106,6 +107,10 @@ class PodcastEpisode {
|
|||||||
get enclosureUrl() {
|
get enclosureUrl() {
|
||||||
return this.enclosure ? this.enclosure.url : null
|
return this.enclosure ? this.enclosure.url : null
|
||||||
}
|
}
|
||||||
|
get pubYear() {
|
||||||
|
if (!this.publishedAt) return null
|
||||||
|
return new Date(this.publishedAt).getFullYear()
|
||||||
|
}
|
||||||
|
|
||||||
setData(data, index = 1) {
|
setData(data, index = 1) {
|
||||||
this.id = getId('ep')
|
this.id = getId('ep')
|
||||||
@@ -117,7 +122,7 @@ class PodcastEpisode {
|
|||||||
this.enclosure = data.enclosure ? { ...data.enclosure } : null
|
this.enclosure = data.enclosure ? { ...data.enclosure } : null
|
||||||
this.season = data.season || ''
|
this.season = data.season || ''
|
||||||
this.episode = data.episode || ''
|
this.episode = data.episode || ''
|
||||||
this.episodeType = data.episodeType || ''
|
this.episodeType = data.episodeType || 'full'
|
||||||
this.publishedAt = data.publishedAt || 0
|
this.publishedAt = data.publishedAt || 0
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
@@ -128,6 +133,9 @@ class PodcastEpisode {
|
|||||||
this.audioFile = audioFile
|
this.audioFile = audioFile
|
||||||
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
||||||
this.index = index
|
this.index = index
|
||||||
|
|
||||||
|
this.setDataFromAudioMetaTags(audioFile.metaTags, true)
|
||||||
|
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
@@ -164,5 +172,76 @@ class PodcastEpisode {
|
|||||||
searchQuery(query) {
|
searchQuery(query) {
|
||||||
return cleanStringForSearch(this.title).includes(query)
|
return cleanStringForSearch(this.title).includes(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) {
|
||||||
|
if (!audioFileMetaTags) return false
|
||||||
|
|
||||||
|
const MetadataMapArray = [
|
||||||
|
{
|
||||||
|
tag: 'tagComment',
|
||||||
|
altTag: 'tagSubtitle',
|
||||||
|
key: 'description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagSubtitle',
|
||||||
|
key: 'subtitle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagDate',
|
||||||
|
key: 'pubDate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagDisc',
|
||||||
|
key: 'season',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagTrack',
|
||||||
|
altTag: 'tagSeriesPart',
|
||||||
|
key: 'episode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagTitle',
|
||||||
|
key: 'title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'tagEpisodeType',
|
||||||
|
key: 'episodeType'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
MetadataMapArray.forEach((mapping) => {
|
||||||
|
let value = audioFileMetaTags[mapping.tag]
|
||||||
|
let tagToUse = mapping.tag
|
||||||
|
if (!value && mapping.altTag) {
|
||||||
|
tagToUse = mapping.altTag
|
||||||
|
value = audioFileMetaTags[mapping.altTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'string') {
|
||||||
|
value = value.trim() // Trim whitespace
|
||||||
|
|
||||||
|
if (mapping.key === 'pubDate' && (!this.pubDate || overrideExistingDetails)) {
|
||||||
|
const pubJsDate = new Date(value)
|
||||||
|
if (pubJsDate && !isNaN(pubJsDate)) {
|
||||||
|
this.publishedAt = pubJsDate.valueOf()
|
||||||
|
this.pubDate = value
|
||||||
|
Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`)
|
||||||
|
} else {
|
||||||
|
Logger.warn(`[PodcastEpisode] Mapping pubDate with tag ${tagToUse} has invalid date "${value}"`)
|
||||||
|
}
|
||||||
|
} else if (mapping.key === 'episodeType' && (!this.episodeType || overrideExistingDetails)) {
|
||||||
|
if (['full', 'trailer', 'bonus'].includes(value)) {
|
||||||
|
this.episodeType = value
|
||||||
|
Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`)
|
||||||
|
} else {
|
||||||
|
Logger.warn(`[PodcastEpisode] Mapping episodeType with invalid value "${value}". Must be one of [full, trailer, bonus].`)
|
||||||
|
}
|
||||||
|
} else if (!this[mapping.key] || overrideExistingDetails) {
|
||||||
|
this[mapping.key] = value
|
||||||
|
Logger.debug(`[PodcastEpisode] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${this[mapping.key]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastEpisode
|
module.exports = PodcastEpisode
|
||||||
@@ -296,7 +296,7 @@ class Book {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (key === 'narrators') {
|
} else if (key === 'narrators') {
|
||||||
if (opfMetadata.narrators && opfMetadata.narrators.length && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.narrators?.length && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
|
||||||
metadataUpdatePayload.narrators = opfMetadata.narrators
|
metadataUpdatePayload.narrators = opfMetadata.narrators
|
||||||
}
|
}
|
||||||
} else if (key === 'series') {
|
} else if (key === 'series') {
|
||||||
@@ -356,9 +356,9 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAudioTracks(orderedFileData) {
|
updateAudioTracks(orderedFileData) {
|
||||||
var index = 1
|
let index = 1
|
||||||
this.audioFiles = orderedFileData.map((fileData) => {
|
this.audioFiles = orderedFileData.map((fileData) => {
|
||||||
var audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
const audioFile = this.audioFiles.find(af => af.ino === fileData.ino)
|
||||||
audioFile.manuallyVerified = true
|
audioFile.manuallyVerified = true
|
||||||
audioFile.invalid = false
|
audioFile.invalid = false
|
||||||
audioFile.error = null
|
audioFile.error = null
|
||||||
@@ -376,11 +376,11 @@ class Book {
|
|||||||
this.rebuildTracks()
|
this.rebuildTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildTracks(preferOverdriveMediaMarker) {
|
rebuildTracks() {
|
||||||
Logger.debug(`[Book] Tracks being rebuilt...!`)
|
Logger.debug(`[Book] Tracks being rebuilt...!`)
|
||||||
this.audioFiles.sort((a, b) => a.index - b.index)
|
this.audioFiles.sort((a, b) => a.index - b.index)
|
||||||
this.missingParts = []
|
this.missingParts = []
|
||||||
this.setChapters(preferOverdriveMediaMarker)
|
this.setChapters()
|
||||||
this.checkUpdateMissingTracks()
|
this.checkUpdateMissingTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,14 +412,16 @@ class Book {
|
|||||||
return wasUpdated
|
return wasUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
setChapters(preferOverdriveMediaMarker = false) {
|
setChapters() {
|
||||||
|
const preferOverdriveMediaMarker = !!global.ServerSettings.scannerPreferOverdriveMediaMarker
|
||||||
|
|
||||||
// If 1 audio file without chapters, then no chapters will be set
|
// If 1 audio file without chapters, then no chapters will be set
|
||||||
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
const includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||||
if (!includedAudioFiles.length) return
|
if (!includedAudioFiles.length) return
|
||||||
|
|
||||||
// If overdrive media markers are present and preferred, use those instead
|
// If overdrive media markers are present and preferred, use those instead
|
||||||
if (preferOverdriveMediaMarker) {
|
if (preferOverdriveMediaMarker) {
|
||||||
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
const overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
||||||
if (overdriveChapters) {
|
if (overdriveChapters) {
|
||||||
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
||||||
this.chapters = overdriveChapters
|
this.chapters = overdriveChapters
|
||||||
@@ -460,17 +462,26 @@ class Book {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (includedAudioFiles.length > 1) {
|
} else if (includedAudioFiles.length > 1) {
|
||||||
|
const preferAudioMetadata = !!global.ServerSettings.scannerPreferAudioMetadata
|
||||||
|
|
||||||
// Build chapters from audio files
|
// Build chapters from audio files
|
||||||
this.chapters = []
|
this.chapters = []
|
||||||
var currChapterId = 0
|
let currChapterId = 0
|
||||||
var currStartTime = 0
|
let currStartTime = 0
|
||||||
includedAudioFiles.forEach((file) => {
|
includedAudioFiles.forEach((file) => {
|
||||||
if (file.duration) {
|
if (file.duration) {
|
||||||
|
let title = file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
|
||||||
|
|
||||||
|
// When prefer audio metadata server setting is set then use ID3 title tag as long as it is not the same as the book title
|
||||||
|
if (preferAudioMetadata && file.metaTags?.tagTitle && file.metaTags?.tagTitle !== this.metadata.title) {
|
||||||
|
title = file.metaTags.tagTitle
|
||||||
|
}
|
||||||
|
|
||||||
this.chapters.push({
|
this.chapters.push({
|
||||||
id: currChapterId++,
|
id: currChapterId++,
|
||||||
start: currStartTime,
|
start: currStartTime,
|
||||||
end: currStartTime + file.duration,
|
end: currStartTime + file.duration,
|
||||||
title: file.metadata.filename ? Path.basename(file.metadata.filename, Path.extname(file.metadata.filename)) : `Chapter ${currChapterId}`
|
title
|
||||||
})
|
})
|
||||||
currStartTime += file.duration
|
currStartTime += file.duration
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ class Podcast {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findEpisodeWithInode(inode) {
|
||||||
|
return this.episodes.find(ep => ep.audioFile.ino === inode)
|
||||||
|
}
|
||||||
|
|
||||||
setData(mediaData) {
|
setData(mediaData) {
|
||||||
this.metadata = new PodcastMetadata()
|
this.metadata = new PodcastMetadata()
|
||||||
if (mediaData.metadata) {
|
if (mediaData.metadata) {
|
||||||
@@ -315,5 +319,13 @@ class Podcast {
|
|||||||
getEpisode(episodeId) {
|
getEpisode(episodeId) {
|
||||||
return this.episodes.find(ep => ep.id == episodeId)
|
return this.episodes.find(ep => ep.id == episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audio file metadata tags map to podcast details
|
||||||
|
setMetadataFromAudioFile(overrideExistingDetails = false) {
|
||||||
|
if (!this.episodes.length) return false
|
||||||
|
const audioFile = this.episodes[0].audioFile
|
||||||
|
if (!audioFile?.metaTags) return false
|
||||||
|
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
class AudioMetaTags {
|
class AudioMetaTags {
|
||||||
constructor(metadata) {
|
constructor(metadata) {
|
||||||
this.tagAlbum = null
|
this.tagAlbum = null
|
||||||
|
this.tagAlbumSort = null
|
||||||
this.tagArtist = null
|
this.tagArtist = null
|
||||||
|
this.tagArtistSort = null
|
||||||
this.tagGenre = null
|
this.tagGenre = null
|
||||||
this.tagTitle = null
|
this.tagTitle = null
|
||||||
|
this.tagTitleSort = null
|
||||||
this.tagSeries = null
|
this.tagSeries = null
|
||||||
this.tagSeriesPart = null
|
this.tagSeriesPart = null
|
||||||
this.tagTrack = null
|
this.tagTrack = null
|
||||||
@@ -20,6 +23,9 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = null
|
this.tagIsbn = null
|
||||||
this.tagLanguage = null
|
this.tagLanguage = null
|
||||||
this.tagASIN = null
|
this.tagASIN = null
|
||||||
|
this.tagItunesId = null
|
||||||
|
this.tagPodcastType = null
|
||||||
|
this.tagEpisodeType = null
|
||||||
this.tagOverdriveMediaMarker = null
|
this.tagOverdriveMediaMarker = null
|
||||||
this.tagOriginalYear = null
|
this.tagOriginalYear = null
|
||||||
this.tagReleaseCountry = null
|
this.tagReleaseCountry = null
|
||||||
@@ -94,9 +100,12 @@ class AudioMetaTags {
|
|||||||
|
|
||||||
construct(metadata) {
|
construct(metadata) {
|
||||||
this.tagAlbum = metadata.tagAlbum || null
|
this.tagAlbum = metadata.tagAlbum || null
|
||||||
|
this.tagAlbumSort = metadata.tagAlbumSort || null
|
||||||
this.tagArtist = metadata.tagArtist || null
|
this.tagArtist = metadata.tagArtist || null
|
||||||
|
this.tagArtistSort = metadata.tagArtistSort || null
|
||||||
this.tagGenre = metadata.tagGenre || null
|
this.tagGenre = metadata.tagGenre || null
|
||||||
this.tagTitle = metadata.tagTitle || null
|
this.tagTitle = metadata.tagTitle || null
|
||||||
|
this.tagTitleSort = metadata.tagTitleSort || null
|
||||||
this.tagSeries = metadata.tagSeries || null
|
this.tagSeries = metadata.tagSeries || null
|
||||||
this.tagSeriesPart = metadata.tagSeriesPart || null
|
this.tagSeriesPart = metadata.tagSeriesPart || null
|
||||||
this.tagTrack = metadata.tagTrack || null
|
this.tagTrack = metadata.tagTrack || null
|
||||||
@@ -113,6 +122,9 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = metadata.tagIsbn || null
|
this.tagIsbn = metadata.tagIsbn || null
|
||||||
this.tagLanguage = metadata.tagLanguage || null
|
this.tagLanguage = metadata.tagLanguage || null
|
||||||
this.tagASIN = metadata.tagASIN || null
|
this.tagASIN = metadata.tagASIN || null
|
||||||
|
this.tagItunesId = metadata.tagItunesId || null
|
||||||
|
this.tagPodcastType = metadata.tagPodcastType || null
|
||||||
|
this.tagEpisodeType = metadata.tagEpisodeType || null
|
||||||
this.tagOverdriveMediaMarker = metadata.tagOverdriveMediaMarker || null
|
this.tagOverdriveMediaMarker = metadata.tagOverdriveMediaMarker || null
|
||||||
this.tagOriginalYear = metadata.tagOriginalYear || null
|
this.tagOriginalYear = metadata.tagOriginalYear || null
|
||||||
this.tagReleaseCountry = metadata.tagReleaseCountry || null
|
this.tagReleaseCountry = metadata.tagReleaseCountry || null
|
||||||
@@ -128,9 +140,12 @@ class AudioMetaTags {
|
|||||||
// Data parsed in prober.js
|
// Data parsed in prober.js
|
||||||
setData(payload) {
|
setData(payload) {
|
||||||
this.tagAlbum = payload.file_tag_album || null
|
this.tagAlbum = payload.file_tag_album || null
|
||||||
|
this.tagAlbumSort = payload.file_tag_albumsort || null
|
||||||
this.tagArtist = payload.file_tag_artist || null
|
this.tagArtist = payload.file_tag_artist || null
|
||||||
|
this.tagArtistSort = payload.file_tag_artistsort || null
|
||||||
this.tagGenre = payload.file_tag_genre || null
|
this.tagGenre = payload.file_tag_genre || null
|
||||||
this.tagTitle = payload.file_tag_title || null
|
this.tagTitle = payload.file_tag_title || null
|
||||||
|
this.tagTitleSort = payload.file_tag_titlesort || null
|
||||||
this.tagSeries = payload.file_tag_series || null
|
this.tagSeries = payload.file_tag_series || null
|
||||||
this.tagSeriesPart = payload.file_tag_seriespart || null
|
this.tagSeriesPart = payload.file_tag_seriespart || null
|
||||||
this.tagTrack = payload.file_tag_track || null
|
this.tagTrack = payload.file_tag_track || null
|
||||||
@@ -147,6 +162,9 @@ class AudioMetaTags {
|
|||||||
this.tagIsbn = payload.file_tag_isbn || null
|
this.tagIsbn = payload.file_tag_isbn || null
|
||||||
this.tagLanguage = payload.file_tag_language || null
|
this.tagLanguage = payload.file_tag_language || null
|
||||||
this.tagASIN = payload.file_tag_asin || null
|
this.tagASIN = payload.file_tag_asin || null
|
||||||
|
this.tagItunesId = payload.file_tag_itunesid || null
|
||||||
|
this.tagPodcastType = payload.file_tag_podcasttype || null
|
||||||
|
this.tagEpisodeType = payload.file_tag_episodetype || null
|
||||||
this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null
|
this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null
|
||||||
this.tagOriginalYear = payload.file_tag_originalyear || null
|
this.tagOriginalYear = payload.file_tag_originalyear || null
|
||||||
this.tagReleaseCountry = payload.file_tag_releasecountry || null
|
this.tagReleaseCountry = payload.file_tag_releasecountry || null
|
||||||
@@ -166,9 +184,12 @@ class AudioMetaTags {
|
|||||||
updateData(payload) {
|
updateData(payload) {
|
||||||
const dataMap = {
|
const dataMap = {
|
||||||
tagAlbum: payload.file_tag_album || null,
|
tagAlbum: payload.file_tag_album || null,
|
||||||
|
tagAlbumSort: payload.file_tag_albumsort || null,
|
||||||
tagArtist: payload.file_tag_artist || null,
|
tagArtist: payload.file_tag_artist || null,
|
||||||
|
tagArtistSort: payload.file_tag_artistsort || null,
|
||||||
tagGenre: payload.file_tag_genre || null,
|
tagGenre: payload.file_tag_genre || null,
|
||||||
tagTitle: payload.file_tag_title || null,
|
tagTitle: payload.file_tag_title || null,
|
||||||
|
tagTitleSort: payload.file_tag_titlesort || null,
|
||||||
tagSeries: payload.file_tag_series || null,
|
tagSeries: payload.file_tag_series || null,
|
||||||
tagSeriesPart: payload.file_tag_seriespart || null,
|
tagSeriesPart: payload.file_tag_seriespart || null,
|
||||||
tagTrack: payload.file_tag_track || null,
|
tagTrack: payload.file_tag_track || null,
|
||||||
@@ -185,6 +206,9 @@ class AudioMetaTags {
|
|||||||
tagIsbn: payload.file_tag_isbn || null,
|
tagIsbn: payload.file_tag_isbn || null,
|
||||||
tagLanguage: payload.file_tag_language || null,
|
tagLanguage: payload.file_tag_language || null,
|
||||||
tagASIN: payload.file_tag_asin || null,
|
tagASIN: payload.file_tag_asin || null,
|
||||||
|
tagItunesId: payload.file_tag_itunesid || null,
|
||||||
|
tagPodcastType: payload.file_tag_podcasttype || null,
|
||||||
|
tagEpisodeType: payload.file_tag_episodetype || null,
|
||||||
tagOverdriveMediaMarker: payload.file_tag_overdrive_media_marker || null,
|
tagOverdriveMediaMarker: payload.file_tag_overdrive_media_marker || null,
|
||||||
tagOriginalYear: payload.file_tag_originalyear || null,
|
tagOriginalYear: payload.file_tag_originalyear || null,
|
||||||
tagReleaseCountry: payload.file_tag_releasecountry || null,
|
tagReleaseCountry: payload.file_tag_releasecountry || null,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user