Compare commits

...

75 Commits

Author SHA1 Message Date
advplyr 9b44e36e7b Version bump 2.2.16 2023-03-05 16:28:45 -06:00
advplyr db1ca08c2e Update scanner logs to show inode value on path changes and missing items #1447 2023-03-05 15:38:21 -06:00
advplyr 557d3243c3 Fix:Series & collection rss feeds repeating first book #1531 2023-03-05 15:26:18 -06:00
advplyr 785942b94f Update:Series books page fallback to sort by title/collapsed series name when no sequence #1503 2023-03-05 14:48:20 -06:00
advplyr 3df7caa838 Fix:OPF parser crash when no narrators #1578 2023-03-05 12:40:21 -06:00
advplyr aef2c52630 Merge pull request #1581 from mfcar/improvePodcastEditing
Improve podcast editing
2023-03-05 12:28:12 -06:00
advplyr dccad3055b Remove library item listener from edit episode modal 2023-03-05 12:28:20 -06:00
advplyr c629923a80 Merge pull request #1562 from mfcar/addNextScheduleInfo
Improve dates, times and schedule backup info
2023-03-05 11:44:59 -06:00
advplyr b4f1fd5b25 Remove currently from date/time setting 2023-03-05 11:38:07 -06:00
advplyr 267897ce74 Merge pull request #1559 from mfcar/addDownloadQueue
Add download queue page
2023-03-05 10:48:25 -06:00
advplyr 022bf9d0ef Show current episode download on init and download queue page updates 2023-03-05 10:35:34 -06:00
mfcar 61c759e0c4 Add tasks queue dropdown 2023-03-05 11:15:36 +00:00
mfcar cfb3ce0c60 Merge branch 'master' into addDownloadQueue 2023-03-04 22:00:18 +00:00
mfcar 72396c5a98 Add Prev/Next buttons on podcast editing 2023-03-04 19:04:55 +00:00
mfcar 12f231b886 Add save action without closing the modal 2023-03-04 16:44:52 +00:00
mfcar 6aeed24296 Update example label 2023-03-04 11:51:53 +00:00
mfcar d8b6e09bc0 Merge branch 'master' into addNextScheduleInfo 2023-03-04 11:09:35 +00:00
advplyr d95975cade Fix:Series page progress filter #1577 2023-03-03 17:35:14 -06:00
mfcar c4208a4690 package-lock.json lacking 2023-02-28 17:07:18 +00:00
mfcar 7c7a6df6e4 Using cron-parse lib to parse the cron expression. Cron-parse can handle with more scenarios. 2023-02-28 17:04:46 +00:00
advplyr 791c058ef8 Merge pull request #1563 from mfcar/improvePodcastSearch
Improve podcast search
2023-02-27 16:42:37 -06:00
advplyr c847aea0a4 Merge pull request #1556 from Weldawadyathink/public_rss_feeds
Fix incorrect tags when blocking public feeds
2023-02-27 16:40:18 -06:00
mfcar e56164aa5a Add a new date format 2023-02-27 20:31:38 +00:00
mfcar cfb5e909a9 Improve podcast search 2023-02-27 18:22:17 +00:00
mfcar 071444a9e7 Improve dates, times and schedule backup info 2023-02-27 18:04:26 +00:00
mfcar 34ac972130 Add download queue 2023-02-27 02:56:07 +00:00
advplyr 97b5cf04f5 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-02-25 15:05:49 -06:00
advplyr 0d50d730d9 Update:Html sanitizer to allow br tag 2023-02-25 15:05:44 -06:00
Spenser Bushey 3a7fd0bcc9 Fix incorrect tags when blocking public feeds 2023-02-25 09:00:26 -08:00
advplyr f0edea5d52 Merge pull request #1553 from Smoukus/fix-german-typo
fix german typo
2023-02-25 08:59:05 -06:00
advplyr 9c6b07df99 Merge pull request #1554 from mfcar/blockRssFeed
Add rss feed configuration
2023-02-25 08:56:32 -06:00
advplyr caacf461ab Open rss feed metadataDetails optional 2023-02-25 08:53:09 -06:00
mfcar 5bdbc75522 Fix typo 2023-02-25 13:32:08 +00:00
mfcar 0d3e6b1d0a Add rss details configuration 2023-02-25 13:20:26 +00:00
Smoukus a122e25cba fix german typo 2023-02-25 11:57:07 +01:00
advplyr d7b287bfed Merge pull request #1551 from mfcar/mf/alreadyInYourLibraryIndicator
Improve explicit label and add a AlreadyInYourLibrary indicator
2023-02-24 17:57:44 -06:00
advplyr ba4f585318 Update client/pages/library/_library/podcast/search.vue 2023-02-24 17:57:25 -06:00
mfcar 3f859723a6 Typo 2023-02-24 23:45:06 +00:00
mfcar c820d0e62b Fix truncate hiding explicit icon 2023-02-24 23:36:15 +00:00
mfcar 7a47032a96 Improve explicit label and add a AlreadyInYourLibrary indicator 2023-02-24 23:31:16 +00:00
advplyr 2db4dd6a40 Merge pull request #1539 from Linden-Ryuujin/feature/coverImage
Prefer cover images called cover
2023-02-23 17:55:05 -06:00
advplyr f58e2b6dce Update cover image set on first scan 2023-02-23 17:55:11 -06:00
advplyr 859a53e79a Merge pull request #1536 from mfcar/addSeasonInfo
Adding podcast type, season and episode info to the feed
2023-02-23 17:39:46 -06:00
mfcar ad0edc6329 Fix merge conflicts and add language information on the feed rss 2023-02-23 00:33:04 +00:00
Linden Ryuujin 002fb7a35e When setting the cover image prefer images called "cover", otherwise fallback to original behaviour of first in the list. 2023-02-23 00:09:05 +00:00
mfcar cc62a20a5d Merge branch 'master' into addSeasonInfo
# Conflicts:
#	client/components/modals/podcast/NewModal.vue
2023-02-23 00:06:21 +00:00
advplyr ec7e965dfa Merge pull request #1534 from mfcar/fixExplicitInfo
Fixed explicit/language info import and added Explicit indicator
2023-02-22 17:36:59 -06:00
advplyr 9c3f5406a9 Update client/components/modals/podcast/NewModal.vue 2023-02-22 17:36:42 -06:00
mfcar f4ec6948d2 Add dropown 2023-02-22 19:18:42 +00:00
mfcar 9a51c3be0f Add dropdown to the episode type 2023-02-22 18:48:36 +00:00
mfcar b1ee54522a Add support to podcast type 2023-02-22 18:22:52 +00:00
mfcar c14d13440f Add explicit info 2023-02-22 12:48:12 +00:00
advplyr 8c84640484 Merge pull request #1530 from mfcar/fixingScheduleModal
Fixed schedule info when using Prev/Next button
2023-02-21 16:00:13 -06:00
advplyr 0d8917ced6 Update client/components/widgets/CronExpressionBuilder.vue 2023-02-21 16:00:01 -06:00
mfcar a006eb489d Fix schedule modal info 2023-02-21 21:40:15 +00:00
advplyr f2941e04d3 Merge pull request #1529 from tomazed/translation-fr
update fr locale
2023-02-21 14:51:38 -06:00
advplyr 2728546660 Merge pull request #1528 from Hallo951/master
Update de.json
2023-02-21 14:51:19 -06:00
Tomazed c8c40360ad update HeaderStatsLargestItems 2023-02-21 12:19:31 +01:00
Hallo951 79ab656217 Update de.json
Update german language
2023-02-21 10:14:49 +01:00
advplyr 5c250da388 Merge pull request #1518 from mfcar/addSizeStats
Add largest item stats
2023-02-20 17:41:20 -06:00
advplyr 505e0eb3a2 Update translations 2023-02-20 17:41:26 -06:00
advplyr 388444e51f Merge pull request #1515 from dwtong/encode-podcast-url
Encode podcast url when downloading episode
2023-02-20 17:26:33 -06:00
mfcar 08d7a9aa14 Add size stats 2023-02-19 21:39:28 +00:00
Dan Tong 956678c08c Encode podcast url when downloading episode 2023-02-18 14:21:45 +13:00
advplyr 911c854365 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-02-16 18:01:31 -06:00
advplyr 3c5dc17e3c Fix:Replace unicode x in playback speed control with regular x #1508 2023-02-16 18:01:25 -06:00
advplyr e709cc4cb1 Merge pull request #1468 from lkiesow/integration-test
Integration Test
2023-02-16 17:51:36 -06:00
advplyr da7825e3e3 Merge pull request #1505 from p-rintz/master
Add library tags variable to podcast notifications
2023-02-15 15:58:59 -06:00
advplyr 4039dc7968 Podcast episode download notification adding variables for mediaTags, podcastAuthor, podcastDescription, podcastGenres, episodeTitle, episodeSubtitle, episodeDescription 2023-02-15 15:57:04 -06:00
Philipp Rintz e345c4cc9e Correct the libraryTags variable 2023-02-15 00:00:34 +01:00
Philipp Rintz a08cfa436e Fix code formatting 2023-02-14 16:51:20 +01:00
Philipp Rintz 7207efb4da Add library tags variable to podcast notifications 2023-02-14 16:41:58 +01:00
advplyr 481611ff33 Merge pull request #1500 from Machou/patch-1
Update fr.json
2023-02-12 07:59:41 -06:00
Machou b67cd37a38 Update fr.json 2023-02-12 07:44:08 +01:00
Lars Kiesow d2512d324a Integration Test
This patch adds a minimal integration test building Audiobookshelf as a
binary, running it and checking if the server is available on each push
and pull request.

We can easily extend this with a Selenium or Playwright test later, but
it should already alert us about problems in the build pipeline without
the need for any developer to take a look at the new patches.
2023-02-02 00:48:09 +01:00
82 changed files with 1742 additions and 261 deletions
+44
View File
@@ -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
@@ -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 -->
+12 -1
View File
@@ -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'
}, },
@@ -212,4 +223,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
+13 -7
View File
@@ -11,12 +11,15 @@
</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">,&nbsp;</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">,&nbsp;</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 || {}
}, },
@@ -474,4 +480,4 @@ export default {
#streamContainer { #streamContainer {
box-shadow: 0px -6px 8px #1111113f; box-shadow: 0px -6px 8px #1111113f;
} }
</style> </style>
+6 -2
View File
@@ -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>
@@ -78,4 +82,4 @@ export default {
this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null
} }
} }
</script> </script>
@@ -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>
+11 -5
View File
@@ -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 || '&nbsp;' }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</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>
@@ -193,6 +196,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`
@@ -734,7 +740,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 +864,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
}) })
@@ -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>
+8 -2
View File
@@ -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,
@@ -134,4 +140,4 @@ export default {
} }
} }
} }
</script> </script>
@@ -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: {
@@ -186,4 +192,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
@@ -164,6 +164,13 @@
<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 py-2">
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" checkbox-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 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>
@@ -327,6 +334,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
}) })
} }
@@ -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() {
@@ -176,4 +184,4 @@ export default {
height: calc(100% - 80px); height: calc(100% - 80px);
max-height: calc(100% - 80px); max-height: calc(100% - 80px);
} }
</style> </style>
@@ -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>
@@ -77,4 +198,4 @@ export default {
.tab.tab-selected { .tab.tab-selected {
height: 41px; height: 41px;
} }
</style> </style>
@@ -19,8 +19,15 @@
<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>
+25 -4
View File
@@ -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()
@@ -226,4 +247,4 @@ export default {
#episodes-scroll { #episodes-scroll {
max-height: calc(80vh - 200px); max-height: calc(80vh - 200px);
} }
</style> </style>
@@ -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,28 +130,43 @@ 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() {}
} }
</script> </script>
@@ -14,6 +14,21 @@
<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 +37,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 +57,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 +128,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}`
+10 -4
View File
@@ -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}`)
@@ -208,4 +214,4 @@ export default {
padding-bottom: 5px; padding-bottom: 5px;
background-color: #333; background-color: #333;
} }
</style> </style>
+10 -4
View File
@@ -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: {
@@ -201,4 +207,4 @@ export default {
padding-bottom: 5px; padding-bottom: 5px;
background-color: #272727; background-color: #272727;
} }
</style> </style>
@@ -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: {
@@ -205,4 +209,4 @@ export default {
} }
} }
} }
</script> </script>
@@ -143,6 +143,12 @@ export default {
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: {
@@ -195,7 +201,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 +269,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 +287,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)
@@ -314,4 +322,4 @@ export default {
.episode-leave-active { .episode-leave-active {
position: absolute; position: absolute;
} }
</style> </style>
+1 -3
View File
@@ -68,8 +68,6 @@ export default {
} }
}, },
mounted() {}, mounted() {},
beforeDestroy() { beforeDestroy() {}
console.log('Before destroy')
}
} }
</script> </script>
@@ -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>
@@ -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) {
@@ -309,4 +331,4 @@ export default {
this.init() this.init()
} }
} }
</script> </script>
@@ -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,37 @@ 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`
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 || [])]
}, },
@@ -228,4 +238,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
@@ -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,92 @@
<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>
+35 -2
View File
@@ -1,17 +1,18 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.15", "version": "2.2.16",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.15", "version": "2.2.16",
"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
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.2.15", "version": "2.2.16",
"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",
+33 -15
View File
@@ -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 } : {}
@@ -113,4 +131,4 @@ export default {
this.initServerSettings() this.initServerSettings()
} }
} }
</script> </script>
+19 -2
View File
@@ -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: {
@@ -420,4 +437,4 @@ export default {
this.initServerSettings() this.initServerSettings()
} }
} }
</script> </script>
+27 -1
View File
@@ -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 }}.&nbsp;&nbsp;&nbsp;&nbsp;<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 : []
}, },
@@ -135,4 +161,4 @@ export default {
this.init() this.init()
} }
} }
</script> </script>
+9 -3
View File
@@ -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
} }
@@ -266,4 +272,4 @@ export default {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
} }
</style> </style>
+8 -2
View File
@@ -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: {
+9 -3
View File
@@ -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
} }
@@ -252,4 +258,4 @@ export default {
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
} }
</style> </style>
+10 -5
View File
@@ -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>
@@ -315,6 +318,9 @@ export default {
isInvalid() { isInvalid() {
return this.libraryItem.isInvalid return this.libraryItem.isInvalid
}, },
isExplicit() {
return this.mediaMetadata.explicit || false
},
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)
@@ -632,7 +638,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 +759,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
} }
@@ -206,4 +226,4 @@ export default {
this.loadRecentEpisodes() this.loadRecentEpisodes()
} }
} }
</script> </script>
@@ -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>
+17 -1
View File
@@ -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
@@ -167,4 +183,4 @@ export default ({ app, store }, inject) => {
inject('isDev', process.env.NODE_ENV !== 'production') inject('isDev', process.env.NODE_ENV !== 'production')
store.commit('setRouterBasePath', app.$config.routerBasePath) store.commit('setRouterBasePath', app.$config.routerBasePath)
} }
+7 -1
View File
@@ -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,
@@ -144,4 +150,4 @@ export function supplant(str, subs) {
return typeof r === 'string' || typeof r === 'number' ? r : a return typeof r === 'string' || typeof r === 'number' ? r : a
} }
) )
} }
+40 -1
View File
@@ -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']
}) })
@@ -169,4 +208,4 @@ export const mutations = {
state.selectedMediaItems.push(item) state.selectedMediaItems.push(item)
} }
} }
} }
+9 -1
View File
@@ -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
@@ -225,4 +233,4 @@ export const mutations = {
setInnerModalOpen(state, val) { setInnerModalOpen(state, val) {
state.innerModalOpen = val state.innerModalOpen = val
} }
} }
+23 -3
View File
@@ -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",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "Episode löschen", "HeaderRemoveEpisode": "Episode löschen",
"HeaderRemoveEpisodes": "Lösche {0} Episoden", "HeaderRemoveEpisodes": "Lösche {0} Episoden",
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet", "HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte", "HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
"HeaderSchedule": "Zeitplan", "HeaderSchedule": "Zeitplan",
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans", "HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
@@ -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",
@@ -162,6 +167,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 +198,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 +216,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 +278,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 +310,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",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "RSS Feed Offen", "LabelRSSFeedOpen": "RSS Feed Offen",
"LabelRSSFeedSlug": "RSS Feed Schlagwort", "LabelRSSFeedSlug": "RSS Feed Schlagwort",
"LabelRSSFeedURL": "RSS Feed URL", "LabelRSSFeedURL": "RSS Feed URL",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Begriff suchen", "LabelSearchTerm": "Begriff suchen",
"LabelSearchTitle": "Titel", "LabelSearchTitle": "Titel",
"LabelSearchTitleOrASIN": "Titel oder ASIN", "LabelSearchTitleOrASIN": "Titel oder ASIN",
@@ -357,6 +372,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 +397,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",
@@ -485,6 +502,8 @@
"MessageNoCollections": "Keine Sammlungen", "MessageNoCollections": "Keine Sammlungen",
"MessageNoCoversFound": "Keine Titelbilder gefunden", "MessageNoCoversFound": "Keine Titelbilder gefunden",
"MessageNoDescription": "Keine Beschreibung", "MessageNoDescription": "Keine Beschreibung",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"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 +520,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",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
"ToastUserDeleteSuccess": "Benutzer gelöscht" "ToastUserDeleteSuccess": "Benutzer gelöscht"
} }
+21 -1
View File
@@ -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",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "Remove Episode", "HeaderRemoveEpisode": "Remove Episode",
"HeaderRemoveEpisodes": "Remove {0} Episodes", "HeaderRemoveEpisodes": "Remove {0} Episodes",
"HeaderRSSFeedIsOpen": "RSS Feed is Open", "HeaderRSSFeedIsOpen": "RSS Feed is Open",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "Saved Media Progress", "HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule", "HeaderSchedule": "Schedule",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans", "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
@@ -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",
@@ -162,6 +167,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 +198,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 +216,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 +278,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 +310,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",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "RSS Feed Open", "LabelRSSFeedOpen": "RSS Feed Open",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "RSS Feed URL", "LabelRSSFeedURL": "RSS Feed URL",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Search Term", "LabelSearchTerm": "Search Term",
"LabelSearchTitle": "Search Title", "LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN", "LabelSearchTitleOrASIN": "Search Title or ASIN",
@@ -357,6 +372,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 +400,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",
@@ -485,6 +502,8 @@
"MessageNoCollections": "No Collections", "MessageNoCollections": "No Collections",
"MessageNoCoversFound": "No Covers Found", "MessageNoCoversFound": "No Covers Found",
"MessageNoDescription": "No description", "MessageNoDescription": "No description",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"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 +520,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",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "Socket failed to connect", "ToastSocketFailedToConnect": "Socket failed to connect",
"ToastUserDeleteFailed": "Failed to delete user", "ToastUserDeleteFailed": "Failed to delete user",
"ToastUserDeleteSuccess": "User deleted" "ToastUserDeleteSuccess": "User deleted"
} }
+21 -1
View File
@@ -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",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "Remove Episode", "HeaderRemoveEpisode": "Remove Episode",
"HeaderRemoveEpisodes": "Remove {0} Episodes", "HeaderRemoveEpisodes": "Remove {0} Episodes",
"HeaderRSSFeedIsOpen": "RSS Feed is Open", "HeaderRSSFeedIsOpen": "RSS Feed is Open",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "Saved Media Progress", "HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule", "HeaderSchedule": "Schedule",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans", "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
@@ -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",
@@ -162,6 +167,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 +198,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 +216,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 +278,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 +310,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",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "RSS Feed Open", "LabelRSSFeedOpen": "RSS Feed Open",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "RSS Feed URL", "LabelRSSFeedURL": "RSS Feed URL",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Search Term", "LabelSearchTerm": "Search Term",
"LabelSearchTitle": "Search Title", "LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN", "LabelSearchTitleOrASIN": "Search Title or ASIN",
@@ -357,6 +372,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 +400,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",
@@ -485,6 +502,8 @@
"MessageNoCollections": "No Collections", "MessageNoCollections": "No Collections",
"MessageNoCoversFound": "No Covers Found", "MessageNoCoversFound": "No Covers Found",
"MessageNoDescription": "No description", "MessageNoDescription": "No description",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"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 +520,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",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "Socket failed to connect", "ToastSocketFailedToConnect": "Socket failed to connect",
"ToastUserDeleteFailed": "Failed to delete user", "ToastUserDeleteFailed": "Failed to delete user",
"ToastUserDeleteSuccess": "User deleted" "ToastUserDeleteSuccess": "User deleted"
} }
+49 -29
View File
@@ -20,6 +20,7 @@
"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",
@@ -64,18 +65,18 @@
"ButtonScan": "Analyser", "ButtonScan": "Analyser",
"ButtonScanLibrary": "Analyser la bibliothèque", "ButtonScanLibrary": "Analyser la bibliothèque",
"ButtonSearch": "Rechercher", "ButtonSearch": "Rechercher",
"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 le temps 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",
@@ -92,7 +93,9 @@
"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",
@@ -107,14 +110,14 @@
"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": "Rechercher",
"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",
@@ -126,7 +129,8 @@
"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", "HeaderRSSFeedIsOpen": "Le Flux RSS est actif",
"HeaderRSSFeedGeneral": "RSS Details",
"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,6 +142,7 @@
"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",
@@ -161,7 +166,8 @@
"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)",
@@ -169,11 +175,11 @@
"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": "Changer le mot de passe",
@@ -192,9 +198,10 @@
"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",
@@ -209,12 +216,13 @@
"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)",
@@ -248,7 +256,7 @@
"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",
@@ -270,6 +278,8 @@
"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",
@@ -300,7 +310,9 @@
"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",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "Flux RSS ouvert", "LabelRSSFeedOpen": "Flux RSS ouvert",
"LabelRSSFeedSlug": "Identificateur d'adresse du Flux RSS ", "LabelRSSFeedSlug": "Identificateur d'adresse du Flux RSS ",
"LabelRSSFeedURL": "Adresse du flux RSS", "LabelRSSFeedURL": "Adresse du flux RSS",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Terme de recherche", "LabelSearchTerm": "Terme de recherche",
"LabelSearchTitle": "Titre de recherche", "LabelSearchTitle": "Titre de recherche",
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN", "LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
@@ -324,16 +339,16 @@
"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": "Design 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",
@@ -341,7 +356,7 @@
"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.",
@@ -357,6 +372,7 @@
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les 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 fichiersde l'article. Seul un fichier nommé \"cover\" sera gardé.",
"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",
@@ -372,7 +388,7 @@
"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",
@@ -380,10 +396,11 @@
"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",
"LabelTasks": "Tasks Running",
"LabelTimeListened": "Temps d'écoute", "LabelTimeListened": "Temps d'écoute",
"LabelTimeListenedToday": "Nombres d'écoutes Aujourd'hui", "LabelTimeListenedToday": "Nombres d'écoutes Aujourd'hui",
"LabelTimeRemaining": "{0} restantes", "LabelTimeRemaining": "{0} restantes",
@@ -428,7 +445,7 @@
"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}\"",
@@ -485,6 +502,8 @@
"MessageNoCollections": "Pas de collections", "MessageNoCollections": "Pas de collections",
"MessageNoCoversFound": "Aucune couverture trouvée", "MessageNoCoversFound": "Aucune couverture trouvée",
"MessageNoDescription": "Pas de description", "MessageNoDescription": "Pas de description",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"MessageNoEpisodeMatchesFound": "Pas de correspondance d'épisode trouvée", "MessageNoEpisodeMatchesFound": "Pas de correspondance d'épisode trouvée",
"MessageNoEpisodes": "Aucun épisode", "MessageNoEpisodes": "Aucun épisode",
"MessageNoFoldersAvailable": "Aucun dossier disponible", "MessageNoFoldersAvailable": "Aucun dossier disponible",
@@ -501,6 +520,7 @@
"MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"", "MessageNoSearchResultsFor": "Pas de résultats de recherche pour \"{0}\"",
"MessageNoSeries": "Pas de séries", "MessageNoSeries": "Pas de séries",
"MessageNoTags": "Pas d'étiquettes", "MessageNoTags": "Pas d'étiquettes",
"MessageNoTasksRunning": "No Tasks Running",
"MessageNotYetImplemented": "Non implémenté", "MessageNotYetImplemented": "Non implémenté",
"MessageNoUpdateNecessary": "Pas de mise à jour nécessaire", "MessageNoUpdateNecessary": "Pas de mise à jour nécessaire",
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire", "MessageNoUpdatesWereNecessary": "Aucune mise à jour n'était nécessaire",
+21 -1
View File
@@ -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",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "Ukloni epizodu", "HeaderRemoveEpisode": "Ukloni epizodu",
"HeaderRemoveEpisodes": "Ukloni {0} epizoda/-e", "HeaderRemoveEpisodes": "Ukloni {0} epizoda/-e",
"HeaderRSSFeedIsOpen": "RSS Feed je otvoren", "HeaderRSSFeedIsOpen": "RSS Feed je otvoren",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "Spremljen Media Progress", "HeaderSavedMediaProgress": "Spremljen Media Progress",
"HeaderSchedule": "Schedule", "HeaderSchedule": "Schedule",
"HeaderScheduleLibraryScans": "Zakaži automatsko skeniranje biblioteke", "HeaderScheduleLibraryScans": "Zakaži automatsko skeniranje biblioteke",
@@ -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",
@@ -162,6 +167,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 +198,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 +216,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 +278,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 +310,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",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "RSS Feed Open", "LabelRSSFeedOpen": "RSS Feed Open",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "RSS Feed URL", "LabelRSSFeedURL": "RSS Feed URL",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Traži pojam", "LabelSearchTerm": "Traži pojam",
"LabelSearchTitle": "Traži naslov", "LabelSearchTitle": "Traži naslov",
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN", "LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
@@ -357,6 +372,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 +400,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",
@@ -485,6 +502,8 @@
"MessageNoCollections": "Nema kolekcija", "MessageNoCollections": "Nema kolekcija",
"MessageNoCoversFound": "Covers nisu pronađeni", "MessageNoCoversFound": "Covers nisu pronađeni",
"MessageNoDescription": "Nema opisa", "MessageNoDescription": "Nema opisa",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"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 +520,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",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "Socket failed to connect", "ToastSocketFailedToConnect": "Socket failed to connect",
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika", "ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
"ToastUserDeleteSuccess": "Korisnik obrisan" "ToastUserDeleteSuccess": "Korisnik obrisan"
} }
+21 -1
View File
@@ -20,6 +20,7 @@
"ButtonCreate": "Crea", "ButtonCreate": "Crea",
"ButtonCreateBackup": "Crea un Backup", "ButtonCreateBackup": "Crea un Backup",
"ButtonDelete": "Elimina", "ButtonDelete": "Elimina",
"ButtonDownloadQueue": "Queue",
"ButtonEdit": "Edit", "ButtonEdit": "Edit",
"ButtonEditChapters": "Modifica Capitoli", "ButtonEditChapters": "Modifica Capitoli",
"ButtonEditPodcast": "Modifica Podcast", "ButtonEditPodcast": "Modifica Podcast",
@@ -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",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "Rimuovi Episodi", "HeaderRemoveEpisode": "Rimuovi Episodi",
"HeaderRemoveEpisodes": "Rimuovi {0} Episodi", "HeaderRemoveEpisodes": "Rimuovi {0} Episodi",
"HeaderRSSFeedIsOpen": "RSS Feed è aperto", "HeaderRSSFeedIsOpen": "RSS Feed è aperto",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "Progressi salvati", "HeaderSavedMediaProgress": "Progressi salvati",
"HeaderSchedule": "Schedula", "HeaderSchedule": "Schedula",
"HeaderScheduleLibraryScans": "Schedula la scansione della libreria", "HeaderScheduleLibraryScans": "Schedula la scansione della libreria",
@@ -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",
@@ -162,6 +167,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": "Already in your library",
"LabelAppend": "Appese", "LabelAppend": "Appese",
"LabelAuthor": "Autore", "LabelAuthor": "Autore",
"LabelAuthorFirstLast": "Autore (Per Nome)", "LabelAuthorFirstLast": "Autore (Per Nome)",
@@ -192,6 +198,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 +216,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",
@@ -270,6 +278,8 @@
"LabelNewestAuthors": "Autori Recenti", "LabelNewestAuthors": "Autori Recenti",
"LabelNewestEpisodes": "Episodi Recenti", "LabelNewestEpisodes": "Episodi Recenti",
"LabelNewPassword": "Nuova Password", "LabelNewPassword": "Nuova Password",
"LabelNextBackupDate": "Next backup date",
"LabelNextScheduledRun": "Next scheduled run",
"LabelNotes": "Note", "LabelNotes": "Note",
"LabelNotFinished": "Da Completare", "LabelNotFinished": "Da Completare",
"LabelNotificationAppriseURL": "Apprendi URL(s)", "LabelNotificationAppriseURL": "Apprendi URL(s)",
@@ -300,7 +310,9 @@
"LabelPlayMethod": "Metodo di riproduzione", "LabelPlayMethod": "Metodo di riproduzione",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type",
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)", "LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "Cominciati", "LabelProgress": "Cominciati",
"LabelProvider": "Provider", "LabelProvider": "Provider",
"LabelPubDate": "Data Pubblicazione", "LabelPubDate": "Data Pubblicazione",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "RSS Feed Aperto", "LabelRSSFeedOpen": "RSS Feed Aperto",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "RSS Feed URL", "LabelRSSFeedURL": "RSS Feed URL",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Ricerca", "LabelSearchTerm": "Ricerca",
"LabelSearchTitle": "Cerca Titolo", "LabelSearchTitle": "Cerca Titolo",
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN", "LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
@@ -357,6 +372,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": "Time Format",
"LabelShowAll": "Mostra Tutto", "LabelShowAll": "Mostra Tutto",
"LabelSize": "Dimensione", "LabelSize": "Dimensione",
"LabelSleepTimer": "Sleep timer", "LabelSleepTimer": "Sleep timer",
@@ -384,6 +400,7 @@
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti", "LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
"LabelTasks": "Tasks Running",
"LabelTimeListened": "Tempo di Ascolto", "LabelTimeListened": "Tempo di Ascolto",
"LabelTimeListenedToday": "Tempo di Ascolto Oggi", "LabelTimeListenedToday": "Tempo di Ascolto Oggi",
"LabelTimeRemaining": "{0} rimanente", "LabelTimeRemaining": "{0} rimanente",
@@ -485,6 +502,8 @@
"MessageNoCollections": "Nessuna Raccolta", "MessageNoCollections": "Nessuna Raccolta",
"MessageNoCoversFound": "Nessuna Cover Trovata", "MessageNoCoversFound": "Nessuna Cover Trovata",
"MessageNoDescription": "Nessuna descrizione", "MessageNoDescription": "Nessuna descrizione",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"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 +520,7 @@
"MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"", "MessageNoSearchResultsFor": "Nessun risultato per \"{0}\"",
"MessageNoSeries": "Nessuna Serie", "MessageNoSeries": "Nessuna Serie",
"MessageNoTags": "No Tags", "MessageNoTags": "No Tags",
"MessageNoTasksRunning": "No Tasks Running",
"MessageNotYetImplemented": "Non Ancora Implementato", "MessageNotYetImplemented": "Non Ancora Implementato",
"MessageNoUpdateNecessary": "Nessun aggiornamento necessario", "MessageNoUpdateNecessary": "Nessun aggiornamento necessario",
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario", "MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "Socket non riesce a connettersi", "ToastSocketFailedToConnect": "Socket non riesce a connettersi",
"ToastUserDeleteFailed": "Errore eliminazione utente", "ToastUserDeleteFailed": "Errore eliminazione utente",
"ToastUserDeleteSuccess": "Utente eliminato" "ToastUserDeleteSuccess": "Utente eliminato"
} }
+21 -1
View File
@@ -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",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "Usuń odcinek", "HeaderRemoveEpisode": "Usuń odcinek",
"HeaderRemoveEpisodes": "Usuń {0} odcinków", "HeaderRemoveEpisodes": "Usuń {0} odcinków",
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty", "HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "Zapisany postęp", "HeaderSavedMediaProgress": "Zapisany postęp",
"HeaderSchedule": "Harmonogram", "HeaderSchedule": "Harmonogram",
"HeaderScheduleLibraryScans": "Zaplanuj automatyczne skanowanie biblioteki", "HeaderScheduleLibraryScans": "Zaplanuj automatyczne skanowanie biblioteki",
@@ -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",
@@ -162,6 +167,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 +198,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 +216,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 +278,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 +310,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",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "RSS Feed otwarty", "LabelRSSFeedOpen": "RSS Feed otwarty",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "URL kanały RSS", "LabelRSSFeedURL": "URL kanały RSS",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Wyszukiwanie frazy", "LabelSearchTerm": "Wyszukiwanie frazy",
"LabelSearchTitle": "Wyszukaj tytuł", "LabelSearchTitle": "Wyszukaj tytuł",
"LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN", "LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN",
@@ -357,6 +372,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 +400,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}",
@@ -485,6 +502,8 @@
"MessageNoCollections": "Brak kolekcji", "MessageNoCollections": "Brak kolekcji",
"MessageNoCoversFound": "Okładki nieznalezione", "MessageNoCoversFound": "Okładki nieznalezione",
"MessageNoDescription": "Brak opisu", "MessageNoDescription": "Brak opisu",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"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 +520,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",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się", "ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika", "ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
"ToastUserDeleteSuccess": "Użytkownik usunięty" "ToastUserDeleteSuccess": "Użytkownik usunięty"
} }
+21 -1
View File
@@ -20,6 +20,7 @@
"ButtonCreate": "Создать", "ButtonCreate": "Создать",
"ButtonCreateBackup": "Создать бэкап", "ButtonCreateBackup": "Создать бэкап",
"ButtonDelete": "Удалить", "ButtonDelete": "Удалить",
"ButtonDownloadQueue": "Queue",
"ButtonEdit": "Редактировать", "ButtonEdit": "Редактировать",
"ButtonEditChapters": "Редактировать Главы", "ButtonEditChapters": "Редактировать Главы",
"ButtonEditPodcast": "Редактировать Подкаст", "ButtonEditPodcast": "Редактировать Подкаст",
@@ -92,7 +93,9 @@
"HeaderCollection": "Коллекция", "HeaderCollection": "Коллекция",
"HeaderCollectionItems": "Элементы Коллекции", "HeaderCollectionItems": "Элементы Коллекции",
"HeaderCover": "Обложка", "HeaderCover": "Обложка",
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Подробности", "HeaderDetails": "Подробности",
"HeaderDownloadQueue": "Download Queue",
"HeaderEpisodes": "Эпизоды", "HeaderEpisodes": "Эпизоды",
"HeaderFiles": "Файлы", "HeaderFiles": "Файлы",
"HeaderFindChapters": "Найти Главы", "HeaderFindChapters": "Найти Главы",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "Удалить Эпизод", "HeaderRemoveEpisode": "Удалить Эпизод",
"HeaderRemoveEpisodes": "Удалить {0} Эпизодов", "HeaderRemoveEpisodes": "Удалить {0} Эпизодов",
"HeaderRSSFeedIsOpen": "RSS-канал Открыт", "HeaderRSSFeedIsOpen": "RSS-канал Открыт",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "Прогресс Медиа Сохранен", "HeaderSavedMediaProgress": "Прогресс Медиа Сохранен",
"HeaderSchedule": "Планировщик", "HeaderSchedule": "Планировщик",
"HeaderScheduleLibraryScans": "Планировщик Автоматического Сканирования Библиотеки", "HeaderScheduleLibraryScans": "Планировщик Автоматического Сканирования Библиотеки",
@@ -138,6 +142,7 @@
"HeaderSettingsGeneral": "Основные", "HeaderSettingsGeneral": "Основные",
"HeaderSettingsScanner": "Сканер", "HeaderSettingsScanner": "Сканер",
"HeaderSleepTimer": "Таймер Сна", "HeaderSleepTimer": "Таймер Сна",
"HeaderStatsLargestItems": "Largest Items",
"HeaderStatsLongestItems": "Самые Длинные Книги (часов)", "HeaderStatsLongestItems": "Самые Длинные Книги (часов)",
"HeaderStatsMinutesListeningChart": "Минут прослушивания (последние 7 дней)", "HeaderStatsMinutesListeningChart": "Минут прослушивания (последние 7 дней)",
"HeaderStatsRecentSessions": "Последние Сеансы", "HeaderStatsRecentSessions": "Последние Сеансы",
@@ -162,6 +167,7 @@
"LabelAddToPlaylistBatch": "Добавить {0} Элементов в Плейлист", "LabelAddToPlaylistBatch": "Добавить {0} Элементов в Плейлист",
"LabelAll": "Все", "LabelAll": "Все",
"LabelAllUsers": "Все пользователи", "LabelAllUsers": "Все пользователи",
"LabelAlreadyInYourLibrary": "Already in your library",
"LabelAppend": "Добавить", "LabelAppend": "Добавить",
"LabelAuthor": "Автор", "LabelAuthor": "Автор",
"LabelAuthorFirstLast": "Автор (Имя Фамилия)", "LabelAuthorFirstLast": "Автор (Имя Фамилия)",
@@ -192,6 +198,7 @@
"LabelCronExpression": "Выражение Cron", "LabelCronExpression": "Выражение Cron",
"LabelCurrent": "Текущий", "LabelCurrent": "Текущий",
"LabelCurrently": "Текущее:", "LabelCurrently": "Текущее:",
"LabelCustomCronExpression": "Custom Cron Expression:",
"LabelDatetime": "Дата и время", "LabelDatetime": "Дата и время",
"LabelDescription": "Описание", "LabelDescription": "Описание",
"LabelDeselectAll": "Снять Выделение", "LabelDeselectAll": "Снять Выделение",
@@ -209,6 +216,7 @@
"LabelEpisode": "Эпизод", "LabelEpisode": "Эпизод",
"LabelEpisodeTitle": "Имя Эпизода", "LabelEpisodeTitle": "Имя Эпизода",
"LabelEpisodeType": "Тип Эпизода", "LabelEpisodeType": "Тип Эпизода",
"LabelExample": "Example",
"LabelExplicit": "Явный", "LabelExplicit": "Явный",
"LabelFeedURL": "URL Канала", "LabelFeedURL": "URL Канала",
"LabelFile": "Файл", "LabelFile": "Файл",
@@ -270,6 +278,8 @@
"LabelNewestAuthors": "Новые Авторы", "LabelNewestAuthors": "Новые Авторы",
"LabelNewestEpisodes": "Новые Эпизоды", "LabelNewestEpisodes": "Новые Эпизоды",
"LabelNewPassword": "Новый Пароль", "LabelNewPassword": "Новый Пароль",
"LabelNextBackupDate": "Next backup date",
"LabelNextScheduledRun": "Next scheduled run",
"LabelNotes": "Заметки", "LabelNotes": "Заметки",
"LabelNotFinished": "Не Завершено", "LabelNotFinished": "Не Завершено",
"LabelNotificationAppriseURL": "URL(ы) для извещений", "LabelNotificationAppriseURL": "URL(ы) для извещений",
@@ -300,7 +310,9 @@
"LabelPlayMethod": "Метод Воспроизведения", "LabelPlayMethod": "Метод Воспроизведения",
"LabelPodcast": "Подкаст", "LabelPodcast": "Подкаст",
"LabelPodcasts": "Подкасты", "LabelPodcasts": "Подкасты",
"LabelPodcastType": "Podcast Type",
"LabelPrefixesToIgnore": "Игнорируемые Префиксы (без учета регистра)", "LabelPrefixesToIgnore": "Игнорируемые Префиксы (без учета регистра)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "Прогресс", "LabelProgress": "Прогресс",
"LabelProvider": "Провайдер", "LabelProvider": "Провайдер",
"LabelPubDate": "Дата Публикации", "LabelPubDate": "Дата Публикации",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "Открыть RSS-канал", "LabelRSSFeedOpen": "Открыть RSS-канал",
"LabelRSSFeedSlug": "Встроить RSS-канал", "LabelRSSFeedSlug": "Встроить RSS-канал",
"LabelRSSFeedURL": "URL RSS-канала", "LabelRSSFeedURL": "URL RSS-канала",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "Поисковый Запрос", "LabelSearchTerm": "Поисковый Запрос",
"LabelSearchTitle": "Поиск по Названию", "LabelSearchTitle": "Поиск по Названию",
"LabelSearchTitleOrASIN": "Поиск по Названию или ASIN", "LabelSearchTitleOrASIN": "Поиск по Названию или ASIN",
@@ -357,6 +372,7 @@
"LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"", "LabelSettingsStoreCoversWithItemHelp": "По умолчанию обложки сохраняются в папке /metadata/items, при включении этой настройки обложка будет храниться в папке элемента. Будет сохраняться только один файл с именем \"cover\"",
"LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом", "LabelSettingsStoreMetadataWithItem": "Хранить метаинформацию с элементом",
"LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента. Используется расширение файла .abs", "LabelSettingsStoreMetadataWithItemHelp": "По умолчанию метаинформация сохраняется в папке /metadata/items, при включении этой настройки метаинформация будет храниться в папке элемента. Используется расширение файла .abs",
"LabelSettingsTimeFormat": "Time Format",
"LabelShowAll": "Показать Все", "LabelShowAll": "Показать Все",
"LabelSize": "Размер", "LabelSize": "Размер",
"LabelSleepTimer": "Таймер сна", "LabelSleepTimer": "Таймер сна",
@@ -384,6 +400,7 @@
"LabelTag": "Тег", "LabelTag": "Тег",
"LabelTags": "Теги", "LabelTags": "Теги",
"LabelTagsAccessibleToUser": "Теги Доступные для Пользователя", "LabelTagsAccessibleToUser": "Теги Доступные для Пользователя",
"LabelTasks": "Tasks Running",
"LabelTimeListened": "Время Прослушивания", "LabelTimeListened": "Время Прослушивания",
"LabelTimeListenedToday": "Время Прослушивания Сегодня", "LabelTimeListenedToday": "Время Прослушивания Сегодня",
"LabelTimeRemaining": "{0} осталось", "LabelTimeRemaining": "{0} осталось",
@@ -485,6 +502,8 @@
"MessageNoCollections": "Нет Коллекций", "MessageNoCollections": "Нет Коллекций",
"MessageNoCoversFound": "Обложек не найдено", "MessageNoCoversFound": "Обложек не найдено",
"MessageNoDescription": "Нет описания", "MessageNoDescription": "Нет описания",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"MessageNoEpisodeMatchesFound": "Совпадения эпизодов не найдены", "MessageNoEpisodeMatchesFound": "Совпадения эпизодов не найдены",
"MessageNoEpisodes": "Нет Эпизодов", "MessageNoEpisodes": "Нет Эпизодов",
"MessageNoFoldersAvailable": "Нет доступных папок", "MessageNoFoldersAvailable": "Нет доступных папок",
@@ -501,6 +520,7 @@
"MessageNoSearchResultsFor": "Нет результатов поиска для \"{0}\"", "MessageNoSearchResultsFor": "Нет результатов поиска для \"{0}\"",
"MessageNoSeries": "Нет Серий", "MessageNoSeries": "Нет Серий",
"MessageNoTags": "Нет Тегов", "MessageNoTags": "Нет Тегов",
"MessageNoTasksRunning": "No Tasks Running",
"MessageNotYetImplemented": "Пока не реализовано", "MessageNotYetImplemented": "Пока не реализовано",
"MessageNoUpdateNecessary": "Обновление не требуется", "MessageNoUpdateNecessary": "Обновление не требуется",
"MessageNoUpdatesWereNecessary": "Обновления не требовались", "MessageNoUpdatesWereNecessary": "Обновления не требовались",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "Не удалось подключить сокет", "ToastSocketFailedToConnect": "Не удалось подключить сокет",
"ToastUserDeleteFailed": "Не удалось удалить пользователя", "ToastUserDeleteFailed": "Не удалось удалить пользователя",
"ToastUserDeleteSuccess": "Пользователь удален" "ToastUserDeleteSuccess": "Пользователь удален"
} }
+21 -1
View File
@@ -20,6 +20,7 @@
"ButtonCreate": "创建", "ButtonCreate": "创建",
"ButtonCreateBackup": "创建备份", "ButtonCreateBackup": "创建备份",
"ButtonDelete": "删除", "ButtonDelete": "删除",
"ButtonDownloadQueue": "Queue",
"ButtonEdit": "编辑", "ButtonEdit": "编辑",
"ButtonEditChapters": "编辑章节", "ButtonEditChapters": "编辑章节",
"ButtonEditPodcast": "编辑播客", "ButtonEditPodcast": "编辑播客",
@@ -92,7 +93,9 @@
"HeaderCollection": "收藏", "HeaderCollection": "收藏",
"HeaderCollectionItems": "收藏项目", "HeaderCollectionItems": "收藏项目",
"HeaderCover": "封面", "HeaderCover": "封面",
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "详情", "HeaderDetails": "详情",
"HeaderDownloadQueue": "Download Queue",
"HeaderEpisodes": "剧集", "HeaderEpisodes": "剧集",
"HeaderFiles": "文件", "HeaderFiles": "文件",
"HeaderFindChapters": "查找章节", "HeaderFindChapters": "查找章节",
@@ -127,6 +130,7 @@
"HeaderRemoveEpisode": "移除剧集", "HeaderRemoveEpisode": "移除剧集",
"HeaderRemoveEpisodes": "移除 {0} 剧集", "HeaderRemoveEpisodes": "移除 {0} 剧集",
"HeaderRSSFeedIsOpen": "RSS 源已打开", "HeaderRSSFeedIsOpen": "RSS 源已打开",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderSavedMediaProgress": "保存媒体进度", "HeaderSavedMediaProgress": "保存媒体进度",
"HeaderSchedule": "计划任务", "HeaderSchedule": "计划任务",
"HeaderScheduleLibraryScans": "自动扫描媒体库", "HeaderScheduleLibraryScans": "自动扫描媒体库",
@@ -138,6 +142,7 @@
"HeaderSettingsGeneral": "通用", "HeaderSettingsGeneral": "通用",
"HeaderSettingsScanner": "扫描", "HeaderSettingsScanner": "扫描",
"HeaderSleepTimer": "睡眠计时", "HeaderSleepTimer": "睡眠计时",
"HeaderStatsLargestItems": "Largest Items",
"HeaderStatsLongestItems": "项目时长(小时)", "HeaderStatsLongestItems": "项目时长(小时)",
"HeaderStatsMinutesListeningChart": "收听分钟数(最近7天)", "HeaderStatsMinutesListeningChart": "收听分钟数(最近7天)",
"HeaderStatsRecentSessions": "历史会话", "HeaderStatsRecentSessions": "历史会话",
@@ -162,6 +167,7 @@
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表", "LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
"LabelAll": "全部", "LabelAll": "全部",
"LabelAllUsers": "所有用户", "LabelAllUsers": "所有用户",
"LabelAlreadyInYourLibrary": "Already in your library",
"LabelAppend": "附加", "LabelAppend": "附加",
"LabelAuthor": "作者", "LabelAuthor": "作者",
"LabelAuthorFirstLast": "作者 (姓 名)", "LabelAuthorFirstLast": "作者 (姓 名)",
@@ -192,6 +198,7 @@
"LabelCronExpression": "计划任务表达式", "LabelCronExpression": "计划任务表达式",
"LabelCurrent": "当前", "LabelCurrent": "当前",
"LabelCurrently": "当前:", "LabelCurrently": "当前:",
"LabelCustomCronExpression": "Custom Cron Expression:",
"LabelDatetime": "日期时间", "LabelDatetime": "日期时间",
"LabelDescription": "描述", "LabelDescription": "描述",
"LabelDeselectAll": "全部取消选择", "LabelDeselectAll": "全部取消选择",
@@ -209,6 +216,7 @@
"LabelEpisode": "剧集", "LabelEpisode": "剧集",
"LabelEpisodeTitle": "剧集标题", "LabelEpisodeTitle": "剧集标题",
"LabelEpisodeType": "剧集类型", "LabelEpisodeType": "剧集类型",
"LabelExample": "Example",
"LabelExplicit": "信息准确", "LabelExplicit": "信息准确",
"LabelFeedURL": "源 URL", "LabelFeedURL": "源 URL",
"LabelFile": "文件", "LabelFile": "文件",
@@ -270,6 +278,8 @@
"LabelNewestAuthors": "最新作者", "LabelNewestAuthors": "最新作者",
"LabelNewestEpisodes": "最新剧集", "LabelNewestEpisodes": "最新剧集",
"LabelNewPassword": "新密码", "LabelNewPassword": "新密码",
"LabelNextBackupDate": "Next backup date",
"LabelNextScheduledRun": "Next scheduled run",
"LabelNotes": "注释", "LabelNotes": "注释",
"LabelNotFinished": "未听完", "LabelNotFinished": "未听完",
"LabelNotificationAppriseURL": "通知 URL(s)", "LabelNotificationAppriseURL": "通知 URL(s)",
@@ -300,7 +310,9 @@
"LabelPlayMethod": "播放方法", "LabelPlayMethod": "播放方法",
"LabelPodcast": "播客", "LabelPodcast": "播客",
"LabelPodcasts": "播客", "LabelPodcasts": "播客",
"LabelPodcastType": "Podcast Type",
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)", "LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "进度", "LabelProgress": "进度",
"LabelProvider": "供应商", "LabelProvider": "供应商",
"LabelPubDate": "出版日期", "LabelPubDate": "出版日期",
@@ -315,6 +327,9 @@
"LabelRSSFeedOpen": "打开 RSS 源", "LabelRSSFeedOpen": "打开 RSS 源",
"LabelRSSFeedSlug": "RSS 源段", "LabelRSSFeedSlug": "RSS 源段",
"LabelRSSFeedURL": "RSS 源 URL", "LabelRSSFeedURL": "RSS 源 URL",
"LabelRssFeedCustomOwnerName": "Custom owner Name",
"LabelRssFeedCustomOwnerEmail": "Custom owner Email",
"LabelRssFeedPreventIndexing": "Prevent Indexing",
"LabelSearchTerm": "搜索项", "LabelSearchTerm": "搜索项",
"LabelSearchTitle": "搜索标题", "LabelSearchTitle": "搜索标题",
"LabelSearchTitleOrASIN": "搜索标题或 ASIN", "LabelSearchTitleOrASIN": "搜索标题或 ASIN",
@@ -357,6 +372,7 @@
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件", "LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你媒体项目文件夹中. 只保留一个名为 \"cover\" 的文件",
"LabelSettingsStoreMetadataWithItem": "存储项目元数据", "LabelSettingsStoreMetadataWithItem": "存储项目元数据",
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名", "LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你媒体项目文件夹中. 使 .abs 文件护展名",
"LabelSettingsTimeFormat": "Time Format",
"LabelShowAll": "全部显示", "LabelShowAll": "全部显示",
"LabelSize": "文件大小", "LabelSize": "文件大小",
"LabelSleepTimer": "睡眠定时", "LabelSleepTimer": "睡眠定时",
@@ -384,6 +400,7 @@
"LabelTag": "标签", "LabelTag": "标签",
"LabelTags": "标签", "LabelTags": "标签",
"LabelTagsAccessibleToUser": "用户可访问的标签", "LabelTagsAccessibleToUser": "用户可访问的标签",
"LabelTasks": "Tasks Running",
"LabelTimeListened": "收听时间", "LabelTimeListened": "收听时间",
"LabelTimeListenedToday": "今日收听的时间", "LabelTimeListenedToday": "今日收听的时间",
"LabelTimeRemaining": "剩余 {0}", "LabelTimeRemaining": "剩余 {0}",
@@ -485,6 +502,8 @@
"MessageNoCollections": "没有收藏", "MessageNoCollections": "没有收藏",
"MessageNoCoversFound": "没有找到封面", "MessageNoCoversFound": "没有找到封面",
"MessageNoDescription": "没有描述", "MessageNoDescription": "没有描述",
"MessageNoDownloadsQueued": "No downloads queued",
"MessageNoDownloadsInProgress": "No downloads currently in progress",
"MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项", "MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项",
"MessageNoEpisodes": "没有剧集", "MessageNoEpisodes": "没有剧集",
"MessageNoFoldersAvailable": "没有可用文件夹", "MessageNoFoldersAvailable": "没有可用文件夹",
@@ -501,6 +520,7 @@
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"", "MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
"MessageNoSeries": "无系列", "MessageNoSeries": "无系列",
"MessageNoTags": "无标签", "MessageNoTags": "无标签",
"MessageNoTasksRunning": "No Tasks Running",
"MessageNotYetImplemented": "尚未实施", "MessageNotYetImplemented": "尚未实施",
"MessageNoUpdateNecessary": "无需更新", "MessageNoUpdateNecessary": "无需更新",
"MessageNoUpdatesWereNecessary": "无需更新", "MessageNoUpdatesWereNecessary": "无需更新",
@@ -615,4 +635,4 @@
"ToastSocketFailedToConnect": "网络连接失败", "ToastSocketFailedToConnect": "网络连接失败",
"ToastUserDeleteFailed": "删除用户失败", "ToastUserDeleteFailed": "删除用户失败",
"ToastUserDeleteSuccess": "用户已删除" "ToastUserDeleteSuccess": "用户已删除"
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.2.15", "version": "2.2.16",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.2.15", "version": "2.2.16",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.2.15", "version": "2.2.16",
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -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)
+18 -1
View File
@@ -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
} }
@@ -755,4 +772,4 @@ class LibraryController {
next() next()
} }
} }
module.exports = new LibraryController() module.exports = new LibraryController()
+5 -2
View File
@@ -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)
+15 -1
View File
@@ -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
@@ -283,4 +297,4 @@ class PodcastController {
next() next()
} }
} }
module.exports = new PodcastController() module.exports = new PodcastController()
+1 -1
View File
@@ -134,4 +134,4 @@ class RSSFeedController {
next() next()
} }
} }
module.exports = new RSSFeedController() module.exports = new RSSFeedController()
+8 -2
View File
@@ -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)
} }
@@ -110,4 +116,4 @@ class NotificationManager {
}) })
} }
} }
module.exports = NotificationManager module.exports = NotificationManager
+32 -4
View File
@@ -14,12 +14,14 @@ 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 +58,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,7 +93,7 @@ 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) => { let success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath).then(() => true).catch((error) => {
Logger.error(`[PodcastManager] Podcast Episode download failed`, error) Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
return false return false
}) })
@@ -90,15 +102,21 @@ class PodcastManager {
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
@@ -329,5 +347,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
+13 -4
View File
@@ -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}"`)
@@ -246,4 +255,4 @@ class RssFeedManager {
return this.handleCloseFeed(feed) return this.handleCloseFeed(feed)
} }
} }
module.exports = RssFeedManager module.exports = RssFeedManager
+12 -3
View File
@@ -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
@@ -333,4 +342,4 @@ class Feed {
return author return author
} }
} }
module.exports = Feed module.exports = Feed
+24 -6
View File
@@ -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,9 +159,12 @@ 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 }
] ]
} }
} }
} }
module.exports = FeedEpisode module.exports = FeedEpisode
+36 -9
View File
@@ -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,15 +89,15 @@ 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" }
] ]
} }
} }
} }
module.exports = FeedMeta module.exports = FeedMeta
+19 -6
View File
@@ -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
} }
+15 -8
View File
@@ -8,9 +8,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,15 +22,21 @@ 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
} }
} }
@@ -47,13 +53,14 @@ 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 this.url = encodeURI(podcastEpisode.enclosure.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) {
@@ -62,4 +69,4 @@ class PodcastEpisodeDownload {
this.failed = !success this.failed = !success
} }
} }
module.exports = PodcastEpisodeDownload module.exports = PodcastEpisodeDownload
+2 -2
View File
@@ -117,7 +117,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()
@@ -165,4 +165,4 @@ class PodcastEpisode {
return cleanStringForSearch(this.title).includes(query) return cleanStringForSearch(this.title).includes(query)
} }
} }
module.exports = PodcastEpisode module.exports = PodcastEpisode
+1 -1
View File
@@ -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') {
+8 -3
View File
@@ -15,6 +15,7 @@ class PodcastMetadata {
this.itunesArtistId = null this.itunesArtistId = null
this.explicit = false this.explicit = false
this.language = null this.language = null
this.type = null
if (metadata) { if (metadata) {
this.construct(metadata) this.construct(metadata)
@@ -34,6 +35,7 @@ class PodcastMetadata {
this.itunesArtistId = metadata.itunesArtistId this.itunesArtistId = metadata.itunesArtistId
this.explicit = metadata.explicit this.explicit = metadata.explicit
this.language = metadata.language || null this.language = metadata.language || null
this.type = metadata.type || 'episodic'
} }
toJSON() { toJSON() {
@@ -49,7 +51,8 @@ class PodcastMetadata {
itunesId: this.itunesId, itunesId: this.itunesId,
itunesArtistId: this.itunesArtistId, itunesArtistId: this.itunesArtistId,
explicit: this.explicit, explicit: this.explicit,
language: this.language language: this.language,
type: this.type
} }
} }
@@ -67,7 +70,8 @@ class PodcastMetadata {
itunesId: this.itunesId, itunesId: this.itunesId,
itunesArtistId: this.itunesArtistId, itunesArtistId: this.itunesArtistId,
explicit: this.explicit, explicit: this.explicit,
language: this.language language: this.language,
type: this.type
} }
} }
@@ -112,6 +116,7 @@ class PodcastMetadata {
this.itunesArtistId = mediaMetadata.itunesArtistId || null this.itunesArtistId = mediaMetadata.itunesArtistId || null
this.explicit = !!mediaMetadata.explicit this.explicit = !!mediaMetadata.explicit
this.language = mediaMetadata.language || null this.language = mediaMetadata.language || null
this.type = mediaMetadata.type || null
if (mediaMetadata.genres && mediaMetadata.genres.length) { if (mediaMetadata.genres && mediaMetadata.genres.length) {
this.genres = [...mediaMetadata.genres] this.genres = [...mediaMetadata.genres]
} }
@@ -132,4 +137,4 @@ class PodcastMetadata {
return hasUpdates return hasUpdates
} }
} }
module.exports = PodcastMetadata module.exports = PodcastMetadata
+4 -1
View File
@@ -51,6 +51,7 @@ class ServerSettings {
this.chromecastEnabled = false this.chromecastEnabled = false
this.enableEReader = false this.enableEReader = false
this.dateFormat = 'MM/dd/yyyy' this.dateFormat = 'MM/dd/yyyy'
this.timeFormat = 'HH:mm'
this.language = 'en-us' this.language = 'en-us'
this.logLevel = Logger.logLevel this.logLevel = Logger.logLevel
@@ -96,6 +97,7 @@ class ServerSettings {
this.chromecastEnabled = !!settings.chromecastEnabled this.chromecastEnabled = !!settings.chromecastEnabled
this.enableEReader = !!settings.enableEReader this.enableEReader = !!settings.enableEReader
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy' this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
this.timeFormat = settings.timeFormat || 'HH:mm'
this.language = settings.language || 'en-us' this.language = settings.language || 'en-us'
this.logLevel = settings.logLevel || Logger.logLevel this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null this.version = settings.version || null
@@ -146,6 +148,7 @@ class ServerSettings {
chromecastEnabled: this.chromecastEnabled, chromecastEnabled: this.chromecastEnabled,
enableEReader: this.enableEReader, enableEReader: this.enableEReader,
dateFormat: this.dateFormat, dateFormat: this.dateFormat,
timeFormat: this.timeFormat,
language: this.language, language: this.language,
logLevel: this.logLevel, logLevel: this.logLevel,
version: this.version version: this.version
@@ -178,4 +181,4 @@ class ServerSettings {
return hasUpdates return hasUpdates
} }
} }
module.exports = ServerSettings module.exports = ServerSettings
+3 -2
View File
@@ -95,7 +95,8 @@ class iTunes {
cover: this.getCoverArtwork(data), cover: this.getCoverArtwork(data),
trackCount: data.trackCount, trackCount: data.trackCount,
feedUrl: data.feedUrl, feedUrl: data.feedUrl,
pageUrl: data.collectionViewUrl pageUrl: data.collectionViewUrl,
explicit: data.trackExplicitness === 'explicit'
} }
} }
@@ -105,4 +106,4 @@ class iTunes {
}) })
} }
} }
module.exports = iTunes module.exports = iTunes
+3 -1
View File
@@ -76,6 +76,7 @@ class ApiRouter {
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this)) this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this))
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this))
@@ -235,6 +236,7 @@ class ApiRouter {
this.router.get('/podcasts/:id/search-episode', PodcastController.middleware.bind(this), PodcastController.findEpisode.bind(this)) this.router.get('/podcasts/:id/search-episode', PodcastController.middleware.bind(this), PodcastController.findEpisode.bind(this))
this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this)) this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this))
this.router.post('/podcasts/:id/match-episodes', PodcastController.middleware.bind(this), PodcastController.quickMatchEpisodes.bind(this)) this.router.post('/podcasts/:id/match-episodes', PodcastController.middleware.bind(this), PodcastController.quickMatchEpisodes.bind(this))
this.router.get('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.getEpisode.bind(this))
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this)) this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this))
this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this)) this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this))
@@ -553,4 +555,4 @@ class ApiRouter {
} }
} }
} }
module.exports = ApiRouter module.exports = ApiRouter
+3 -2
View File
@@ -201,6 +201,7 @@ class Scanner {
const dataFound = libraryItemDataFound.find(lid => lid.ino === libraryItem.ino || comparePaths(lid.relPath, libraryItem.relPath)) const dataFound = libraryItemDataFound.find(lid => lid.ino === libraryItem.ino || comparePaths(lid.relPath, libraryItem.relPath))
if (!dataFound) { if (!dataFound) {
libraryScan.addLog(LogLevel.WARN, `Library Item "${libraryItem.media.metadata.title}" is missing`) libraryScan.addLog(LogLevel.WARN, `Library Item "${libraryItem.media.metadata.title}" is missing`)
Logger.warn(`[Scanner] Library item "${libraryItem.media.metadata.title}" is missing (inode "${libraryItem.ino}")`)
libraryScan.resultsMissing++ libraryScan.resultsMissing++
libraryItem.setMissing() libraryItem.setMissing()
itemsToUpdate.push(libraryItem) itemsToUpdate.push(libraryItem)
@@ -899,7 +900,7 @@ class Scanner {
description: episodeToMatch.description || '', description: episodeToMatch.description || '',
enclosure: episodeToMatch.enclosure || null, enclosure: episodeToMatch.enclosure || null,
episode: episodeToMatch.episode || '', episode: episodeToMatch.episode || '',
episodeType: episodeToMatch.episodeType || '', episodeType: episodeToMatch.episodeType || 'full',
season: episodeToMatch.season || '', season: episodeToMatch.season || '',
pubDate: episodeToMatch.pubDate || '', pubDate: episodeToMatch.pubDate || '',
publishedAt: episodeToMatch.publishedAt publishedAt: episodeToMatch.publishedAt
@@ -993,4 +994,4 @@ class Scanner {
return MediaFileScanner.probeAudioFileWithTone(audioFile) return MediaFileScanner.probeAudioFileWithTone(audioFile)
} }
} }
module.exports = Scanner module.exports = Scanner
+2 -2
View File
@@ -1,10 +1,10 @@
const sanitizeHtml = require('../libs/sanitizeHtml') const sanitizeHtml = require('../libs/sanitizeHtml')
const {entities} = require("./htmlEntities"); const { entities } = require("./htmlEntities");
function sanitize(html) { function sanitize(html) {
const sanitizerOptions = { const sanitizerOptions = {
allowedTags: [ allowedTags: [
'p', 'ol', 'ul', 'li', 'a', 'strong', 'em', 'del' 'p', 'ol', 'ul', 'li', 'a', 'strong', 'em', 'del', 'br'
], ],
disallowedTagsMode: 'discard', disallowedTagsMode: 'discard',
allowedAttributes: { allowedAttributes: {
+24 -8
View File
@@ -95,17 +95,20 @@ module.exports = {
checkSeriesProgressFilter(series, filterBy, user) { checkSeriesProgressFilter(series, filterBy, user) {
const filter = this.decode(filterBy.split('.')[1]) const filter = this.decode(filterBy.split('.')[1])
var numBooksStartedOrFinished = 0 let someBookHasProgress = false
let someBookIsUnfinished = false
for (const libraryItem of series.books) { for (const libraryItem of series.books) {
const itemProgress = user.getMediaProgress(libraryItem.id) const itemProgress = user.getMediaProgress(libraryItem.id)
if (filter === 'Finished' && (!itemProgress || !itemProgress.isFinished)) return false if (!itemProgress || !itemProgress.isFinished) someBookIsUnfinished = true
if (filter === 'Not Started' && itemProgress) return false if (itemProgress && itemProgress.progress > 0) someBookHasProgress = true
if (itemProgress) numBooksStartedOrFinished++
if (filter === 'finished' && (!itemProgress || !itemProgress.isFinished)) return false
if (filter === 'not-started' && itemProgress) return false
} }
if (numBooksStartedOrFinished === series.books.length) { // Completely finished series if (!someBookIsUnfinished && filter === 'not-finished') { // Completely finished series
if (filter === 'Not Finished') return false return false
} else if (numBooksStartedOrFinished === 0 && filter === 'In Progress') { // Series not started } else if (!someBookHasProgress && filter === 'in-progress') { // Series not started
return false return false
} }
return true return true
@@ -280,6 +283,19 @@ module.exports = {
} }
}, },
getItemSizeStats(libraryItems) {
var sorted = sort(libraryItems).desc(li => li.media.size)
var top10 = sorted.slice(0, 10).map(li => ({ id: li.id, title: li.media.metadata.title, size: li.media.size })).filter(i => i.size > 0)
var totalSize = 0
libraryItems.forEach((li) => {
totalSize += li.media.size
})
return {
totalSize,
largestItems: top10
}
},
getLibraryItemsTotalSize(libraryItems) { getLibraryItemsTotalSize(libraryItems) {
var totalSize = 0 var totalSize = 0
libraryItems.forEach((li) => { libraryItems.forEach((li) => {
@@ -843,4 +859,4 @@ module.exports = {
return Object.values(albums) return Object.values(albums)
} }
} }
+9 -3
View File
@@ -7,7 +7,7 @@ module.exports.notificationData = {
requiresLibrary: true, requiresLibrary: true,
libraryMediaType: 'podcast', libraryMediaType: 'podcast',
description: 'Triggered when a podcast episode is auto-downloaded', description: 'Triggered when a podcast episode is auto-downloaded',
variables: ['libraryItemId', 'libraryId', 'podcastTitle', 'episodeTitle', 'libraryName', 'episodeId'], variables: ['libraryItemId', 'libraryId', 'podcastTitle', 'podcastAuthor', 'podcastDescription', 'podcastGenres', 'episodeTitle', 'episodeSubtitle', 'episodeDescription', 'libraryName', 'episodeId', 'mediaTags'],
defaults: { defaults: {
title: 'New {{podcastTitle}} Episode!', title: 'New {{podcastTitle}} Episode!',
body: '{{episodeTitle}} has been added to {{libraryName}} library.' body: '{{episodeTitle}} has been added to {{libraryName}} library.'
@@ -16,9 +16,15 @@ module.exports.notificationData = {
libraryItemId: 'li_notification_test', libraryItemId: 'li_notification_test',
libraryId: 'lib_test', libraryId: 'lib_test',
libraryName: 'Podcasts', libraryName: 'Podcasts',
mediaTags: 'TestTag1, TestTag2',
podcastTitle: 'Abs Test Podcast', podcastTitle: 'Abs Test Podcast',
podcastAuthor: 'Audiobookshelf',
podcastDescription: 'Description of the Abs Test Podcast belongs here.',
podcastGenres: 'TestGenre1, TestGenre2',
episodeId: 'ep_notification_test', episodeId: 'ep_notification_test',
episodeTitle: 'Successful Test' episodeTitle: 'Successful Test Episode',
episodeSubtitle: 'Episode Subtitle',
episodeDescription: 'Some description of the podcast episode.'
} }
}, },
{ {
@@ -35,4 +41,4 @@ module.exports.notificationData = {
} }
} }
] ]
} }
+2 -2
View File
@@ -111,7 +111,7 @@ function fetchVolumeNumber(metadataMeta) {
function fetchNarrators(creators, metadata) { function fetchNarrators(creators, metadata) {
const narrators = fetchCreators(creators, 'nrt') const narrators = fetchCreators(creators, 'nrt')
if (typeof metadata.meta == "undefined" || narrators.length) return narrators if (narrators?.length) return narrators
try { try {
const narratorsJSON = JSON.parse(fetchTagString(metadata.meta, "calibre:user_metadata:#narrators").replace(/&quot;/g, '"')) const narratorsJSON = JSON.parse(fetchTagString(metadata.meta, "calibre:user_metadata:#narrators").replace(/&quot;/g, '"'))
return narratorsJSON["#value#"] return narratorsJSON["#value#"]
@@ -150,7 +150,7 @@ module.exports.parseOpfMetadataXML = async (xml) => {
const metadataMeta = prefix ? metadata[`${prefix}:meta`] || metadata.meta : metadata.meta const metadataMeta = prefix ? metadata[`${prefix}:meta`] || metadata.meta : metadata.meta
metadata.meta = {} metadata.meta = {}
if (metadataMeta && metadataMeta.length) { if (metadataMeta?.length) {
metadataMeta.forEach((meta) => { metadataMeta.forEach((meta) => {
if (meta && meta['$'] && meta['$'].name) { if (meta && meta['$'] && meta['$'].name) {
metadata.meta[meta['$'].name] = [meta['$'].content || ''] metadata.meta[meta['$'].name] = [meta['$'].content || '']
+4 -3
View File
@@ -46,7 +46,8 @@ function extractPodcastMetadata(channel) {
categories: extractCategories(channel), categories: extractCategories(channel),
feedUrl: null, feedUrl: null,
description: null, description: null,
descriptionPlain: null descriptionPlain: null,
type: null
} }
if (channel['itunes:new-feed-url']) { if (channel['itunes:new-feed-url']) {
@@ -61,7 +62,7 @@ function extractPodcastMetadata(channel) {
metadata.descriptionPlain = htmlSanitizer.stripAllTags(rawDescription) metadata.descriptionPlain = htmlSanitizer.stripAllTags(rawDescription)
} }
var arrayFields = ['title', 'language', 'itunes:explicit', 'itunes:author', 'pubDate', 'link'] var arrayFields = ['title', 'language', 'itunes:explicit', 'itunes:author', 'pubDate', 'link', 'itunes:type']
arrayFields.forEach((key) => { arrayFields.forEach((key) => {
var cleanKey = key.split(':').pop() var cleanKey = key.split(':').pop()
metadata[cleanKey] = extractFirstArrayItem(channel, key) metadata[cleanKey] = extractFirstArrayItem(channel, key)
@@ -258,4 +259,4 @@ module.exports.findMatchingEpisodesInFeed = (feed, searchTitle) => {
} }
}) })
return matches.sort((a, b) => a.levenshtein - b.levenshtein) return matches.sort((a, b) => a.levenshtein - b.levenshtein)
} }