mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-04-22 06:59:45 +00:00
Merge remote-tracking branch 'origin/master' into auth_passportjs
This commit is contained in:
commit
f0f03efe17
138 changed files with 11777 additions and 7343 deletions
|
|
@ -55,6 +55,7 @@ export default {
|
|||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
|
||||
else if (pageName === 'email') return this.$strings.HeaderEmail
|
||||
}
|
||||
return this.$strings.HeaderSettings
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
</ui-tooltip>
|
||||
</div>
|
||||
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
|
||||
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
||||
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="sortingPrefixesUpdated" :disabled="savingPrefixes" />
|
||||
<div class="flex justify-end py-1">
|
||||
<ui-btn v-if="hasPrefixesChanged" color="success" :loading="savingPrefixes" small @click="updateSortingPrefixes">Save</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2 mb-2">
|
||||
|
|
@ -157,10 +160,10 @@
|
|||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
||||
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
|
||||
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
|
||||
<p class="pl-4">
|
||||
<span id="settings-disable-watcher">{{ $strings.LabelSettingsDisableWatcher }}</span>
|
||||
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
|
|
@ -259,9 +262,12 @@ export default {
|
|||
updatingServerSettings: false,
|
||||
homepageUseBookshelfView: false,
|
||||
useBookshelfView: false,
|
||||
scannerEnableWatcher: false,
|
||||
isPurgingCache: false,
|
||||
hasPrefixesChanged: false,
|
||||
newServerSettings: {},
|
||||
showConfirmPurgeCache: false,
|
||||
savingPrefixes: false,
|
||||
metadataFileFormats: [
|
||||
{
|
||||
text: '.json',
|
||||
|
|
@ -304,15 +310,36 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
updateSortingPrefixes(val) {
|
||||
if (!val || !val.length) {
|
||||
sortingPrefixesUpdated(val) {
|
||||
const prefixes = [...new Set(val?.map((prefix) => prefix.trim().toLowerCase()) || [])]
|
||||
this.newServerSettings.sortingPrefixes = prefixes
|
||||
const serverPrefixes = this.serverSettings.sortingPrefixes || []
|
||||
this.hasPrefixesChanged = prefixes.some((p) => !serverPrefixes.includes(p)) || serverPrefixes.some((p) => !prefixes.includes(p))
|
||||
},
|
||||
updateSortingPrefixes() {
|
||||
const prefixes = [...new Set(this.newServerSettings.sortingPrefixes.map((prefix) => prefix.trim().toLowerCase()) || [])]
|
||||
if (!prefixes.length) {
|
||||
this.$toast.error('Must have at least 1 prefix')
|
||||
return
|
||||
}
|
||||
var prefixes = val.map((prefix) => prefix.trim().toLowerCase())
|
||||
this.updateServerSettings({
|
||||
sortingPrefixes: prefixes
|
||||
})
|
||||
|
||||
this.savingPrefixes = true
|
||||
this.$axios
|
||||
.$patch(`/api/sorting-prefixes`, { sortingPrefixes: prefixes })
|
||||
.then((data) => {
|
||||
this.$toast.success(`Sorting prefixes updated. ${data.rowsUpdated} rows`)
|
||||
if (data.serverSettings) {
|
||||
this.$store.commit('setServerSettings', data.serverSettings)
|
||||
}
|
||||
this.hasPrefixesChanged = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update prefixes', error)
|
||||
this.$toast.error('Failed to update sorting prefixes')
|
||||
})
|
||||
.finally(() => {
|
||||
this.savingPrefixes = false
|
||||
})
|
||||
},
|
||||
updateScannerCoverProvider(val) {
|
||||
this.updateServerSettings({
|
||||
|
|
@ -337,6 +364,9 @@ export default {
|
|||
this.updateSettingsKey('metadataFileFormat', val)
|
||||
},
|
||||
updateSettingsKey(key, val) {
|
||||
if (key === 'scannerDisableWatcher') {
|
||||
this.newServerSettings.scannerDisableWatcher = val
|
||||
}
|
||||
this.updateServerSettings({
|
||||
[key]: val
|
||||
})
|
||||
|
|
@ -363,6 +393,7 @@ export default {
|
|||
initServerSettings() {
|
||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
|
||||
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
|
||||
|
||||
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
this.useBookshelfView = this.newServerSettings.bookshelfView != this.$constants.BookshelfView.DETAIL
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-80 my-6 mx-auto">
|
||||
<div v-if="isBookLibrary" class="w-80 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||
<template v-for="(author, index) in top10Authors">
|
||||
|
|
@ -114,43 +114,49 @@ export default {
|
|||
return this.$store.state.user.user
|
||||
},
|
||||
totalItems() {
|
||||
return this.libraryStats ? this.libraryStats.totalItems : 0
|
||||
return this.libraryStats?.totalItems || 0
|
||||
},
|
||||
genresWithCount() {
|
||||
return this.libraryStats ? this.libraryStats.genresWithCount : []
|
||||
return this.libraryStats?.genresWithCount || []
|
||||
},
|
||||
top5Genres() {
|
||||
return this.genresWithCount.slice(0, 5)
|
||||
return this.genresWithCount?.slice(0, 5) || []
|
||||
},
|
||||
top10LongestItems() {
|
||||
return this.libraryStats ? this.libraryStats.longestItems || [] : []
|
||||
return this.libraryStats?.longestItems || []
|
||||
},
|
||||
longestItemDuration() {
|
||||
if (!this.top10LongestItems.length) return 0
|
||||
return this.top10LongestItems[0].duration
|
||||
},
|
||||
top10LargestItems() {
|
||||
return this.libraryStats ? this.libraryStats.largestItems || [] : []
|
||||
return this.libraryStats?.largestItems || []
|
||||
},
|
||||
largestItemSize() {
|
||||
if (!this.top10LargestItems.length) return 0
|
||||
return this.top10LargestItems[0].size
|
||||
},
|
||||
authorsWithCount() {
|
||||
return this.libraryStats ? this.libraryStats.authorsWithCount : []
|
||||
return this.libraryStats?.authorsWithCount || []
|
||||
},
|
||||
mostUsedAuthorCount() {
|
||||
if (!this.authorsWithCount.length) return 0
|
||||
return this.authorsWithCount[0].count
|
||||
},
|
||||
top10Authors() {
|
||||
return this.authorsWithCount.slice(0, 10)
|
||||
return this.authorsWithCount?.slice(0, 10) || []
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
currentLibraryName() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||
},
|
||||
currentLibraryMediaType() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||
},
|
||||
isBookLibrary() {
|
||||
return this.currentLibraryMediaType === 'book'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
176
client/pages/config/rss-feeds.vue
Normal file
176
client/pages/config/rss-feeds.vue
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderRSSFeeds">
|
||||
<div v-if="feeds.length" class="block max-w-full">
|
||||
<table class="rssFeedsTable text-xs">
|
||||
<tr class="bg-primary bg-opacity-40 h-12">
|
||||
<th class="w-16 min-w-16"></th>
|
||||
<th class="w-48 max-w-64 min-w-24 text-left truncate">{{ $strings.LabelTitle }}</th>
|
||||
<th class="w-48 min-w-24 text-left hidden xl:table-cell">{{ $strings.LabelSlug }}</th>
|
||||
<th class="w-24 min-w-16 text-left hidden md:table-cell">{{ $strings.LabelType }}</th>
|
||||
<th class="w-16 min-w-16 text-center">{{ $strings.HeaderEpisodes }}</th>
|
||||
<th class="w-16 min-w-16 text-center hidden lg:table-cell">{{ $strings.LabelRSSFeedPreventIndexing }}</th>
|
||||
<th class="w-48 min-w-24 flex-grow hidden md:table-cell">{{ $strings.LabelLastUpdate }}</th>
|
||||
<th class="w-16 text-left"></th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="feed in feeds" :key="feed.id" class="cursor-pointer h-12" @click="showFeed(feed)">
|
||||
<!-- -->
|
||||
<td>
|
||||
<img :src="coverUrl(feed)" class="h-full w-full" />
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="w-48 max-w-64 min-w-24 text-left truncate">
|
||||
<p class="truncate">{{ feed.meta.title }}</p>
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="hidden xl:table-cell">
|
||||
<p class="truncate">{{ feed.slug }}</p>
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="">{{ getEntityType(feed.entityType) }}</p>
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="text-center">
|
||||
<p class="">{{ feed.episodes.length }}</p>
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="text-center leading-none hidden lg:table-cell">
|
||||
<p v-if="feed.meta.preventIndexing" class="">
|
||||
<span class="material-icons text-2xl">check</span>
|
||||
</p>
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="text-center hidden md:table-cell">
|
||||
<ui-tooltip v-if="feed.updatedAt" direction="top" :text="$formatDatetime(feed.updatedAt, dateFormat, timeFormat)">
|
||||
<p class="text-gray-200">{{ $dateDistanceFromNow(feed.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
<!-- -->
|
||||
<td class="text-center">
|
||||
<ui-icon-btn icon="delete" class="mx-0.5" :size="7" bg-color="error" outlined @click.stop="deleteFeedClick(feed)" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
<modals-rssfeed-view-feed-modal v-model="showFeedModal" :feed="selectedFeed" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showFeedModal: false,
|
||||
selectedFeed: null,
|
||||
feeds: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
},
|
||||
timeFormat() {
|
||||
return this.$store.state.serverSettings.timeFormat
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showFeed(feed) {
|
||||
this.selectedFeed = feed
|
||||
this.showFeedModal = true
|
||||
},
|
||||
deleteFeedClick(feed) {
|
||||
const payload = {
|
||||
message: this.$strings.MessageConfirmCloseFeed,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.deleteFeed(feed)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
deleteFeed(feed) {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$post(`/api/feeds/${feed.id}/close`)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
|
||||
this.show = false
|
||||
this.loadFeeds()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to close RSS feed', error)
|
||||
this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
getEntityType(entityType) {
|
||||
if (entityType === 'libraryItem') return this.$strings.LabelItem
|
||||
else if (entityType === 'series') return this.$strings.LabelSeries
|
||||
else if (entityType === 'collection') return this.$strings.LabelCollection
|
||||
return this.$strings.LabelUnknown
|
||||
},
|
||||
coverUrl(feed) {
|
||||
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
|
||||
return `${feed.feedUrl}/cover`
|
||||
},
|
||||
async loadFeeds() {
|
||||
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
|
||||
console.error('Failed to load RSS feeds', err)
|
||||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load RSS feeds')
|
||||
return
|
||||
}
|
||||
this.feeds = data.feeds
|
||||
},
|
||||
init() {
|
||||
this.loadFeeds()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rssFeedsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
}
|
||||
|
||||
.rssFeedsTable tr:first-child {
|
||||
background-color: #272727;
|
||||
}
|
||||
|
||||
.rssFeedsTable tr:not(:first-child) {
|
||||
background-color: #373838;
|
||||
}
|
||||
|
||||
.rssFeedsTable tr:not(:first-child):nth-child(odd) {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.rssFeedsTable tr:hover:not(:first-child) {
|
||||
background-color: #474747;
|
||||
}
|
||||
|
||||
.rssFeedsTable td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.rssFeedsTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">{{ $strings.HeaderSavedMediaProgress }}</h1>
|
||||
|
||||
<table v-if="mediaProgressWithMedia.length" class="userAudiobooksTable">
|
||||
<table v-if="mediaProgress.length" class="userAudiobooksTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="w-16 text-left">{{ $strings.LabelItem }}</th>
|
||||
<th class="text-left"></th>
|
||||
|
|
@ -55,19 +55,14 @@
|
|||
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelStartedAt }}</th>
|
||||
<th class="w-40 hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
|
||||
</tr>
|
||||
<tr v-for="item in mediaProgressWithMedia" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
||||
<tr v-for="item in mediaProgress" :key="item.id" :class="!item.isFinished ? '' : 'isFinished'">
|
||||
<td>
|
||||
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-preview-cover v-if="item.coverPath" :width="50" :src="$store.getters['globals/getLibraryItemCoverSrcById'](item.libraryItemId, item.mediaUpdatedAt)" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
|
||||
<div v-else class="bg-primary flex items-center justify-center text-center text-xs text-gray-400 p-1" :style="{ width: '50px', height: 50 * bookCoverAspectRatio + 'px' }">No Cover</div>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="item.media && item.media.metadata && item.episode">
|
||||
<p>{{ item.episode.title || 'Unknown' }}</p>
|
||||
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
|
||||
</template>
|
||||
<template v-else-if="item.media && item.media.metadata">
|
||||
<p>{{ item.media.metadata.title || 'Unknown' }}</p>
|
||||
<p v-if="item.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ item.media.metadata.authorName }}</p>
|
||||
</template>
|
||||
<p>{{ item.displayTitle || 'Unknown' }}</p>
|
||||
<p v-if="item.displaySubtitle" class="text-white text-opacity-50 text-sm font-sans">{{ item.displaySubtitle }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-sm">{{ Math.floor(item.progress * 100) }}%</p>
|
||||
|
|
@ -124,9 +119,6 @@ export default {
|
|||
mediaProgress() {
|
||||
return this.user.mediaProgress.sort((a, b) => b.lastUpdate - a.lastUpdate)
|
||||
},
|
||||
mediaProgressWithMedia() {
|
||||
return this.mediaProgress.filter((mp) => mp.media)
|
||||
},
|
||||
totalListeningTime() {
|
||||
return this.listeningStats.totalTime || 0
|
||||
},
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export default {
|
|||
}
|
||||
|
||||
// Include episode downloads for podcasts
|
||||
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=authors,downloads,rssfeed`).catch((error) => {
|
||||
var item = await app.$axios.$get(`/api/items/${params.id}?expanded=1&include=downloads,rssfeed`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
|
|
@ -761,6 +761,7 @@ export default {
|
|||
if (this.libraryId) {
|
||||
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||
}
|
||||
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||
|
|
@ -769,6 +770,7 @@ export default {
|
|||
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
|
||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue