mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-28 16:59:21 +01:00
Add RSS feeds config page
This commit is contained in:
parent
7222171c5b
commit
13427b9f70
@ -99,6 +99,11 @@ export default {
|
||||
id: 'config-item-metadata-utils',
|
||||
title: this.$strings.HeaderItemMetadataUtils,
|
||||
path: '/config/item-metadata-utils'
|
||||
},
|
||||
{
|
||||
id: 'config-rss-feeds',
|
||||
title: this.$strings.HeaderRSSFeeds,
|
||||
path: '/config/rss-feeds'
|
||||
}
|
||||
]
|
||||
|
||||
|
128
client/components/modals/rssfeed/ViewFeedModal.vue
Normal file
128
client/components/modals/rssfeed/ViewFeedModal.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="rss-feed-view-modal" :processing="processing" :width="700" :height="'unset'">
|
||||
<div ref="wrapper"
|
||||
class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||
<div v-if="feed" class="w-full">
|
||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
|
||||
|
||||
<div class="w-full relative">
|
||||
<ui-text-input v-model="feed.feedUrl" readonly />
|
||||
<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(feed.feedUrl)">content_copy</span>
|
||||
</div>
|
||||
|
||||
<div v-if="feed.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>{{ feed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
|
||||
</div>
|
||||
<div v-if="feed.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>{{ feed.meta.ownerName }}</div>
|
||||
</div>
|
||||
<div v-if="feed.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>{{ feed.meta.ownerEmail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- -->
|
||||
<div class="episodesTable mt-2">
|
||||
<div class="bg-primary bg-opacity-40 h-12 header">
|
||||
{{ $strings.LabelEpisodeTitle }}
|
||||
</div>
|
||||
<div class="scroller">
|
||||
<div v-for="episode in feed.episodes" :key="episode.id" class="h-8 text-xs truncate">
|
||||
{{ episode.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
feed: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
_feed() {
|
||||
return this.feed || {}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(str) {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
},
|
||||
mounted() { }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.episodesTable {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.episodesTable div.header {
|
||||
background-color: #272727;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.episodesTable .scroller {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 250px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.episodesTable .scroller div {
|
||||
background-color: #373838;
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: 32px;
|
||||
flex: 0 0 32px;
|
||||
}
|
||||
|
||||
.episodesTable .scroller div:nth-child(even) {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
</style>
|
||||
|
@ -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
|
||||
|
178
client/pages/config/rss-feeds.vue
Normal file
178
client/pages/config/rss-feeds.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<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>
|
@ -138,6 +138,7 @@
|
||||
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
||||
"HeaderRSSFeedGeneral": "RSS Details",
|
||||
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
||||
"HeaderRSSFeeds": "RSS Feeds",
|
||||
"HeaderSavedMediaProgress": "Saved Media Progress",
|
||||
"HeaderSchedule": "Schedule",
|
||||
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
|
||||
@ -201,6 +202,7 @@
|
||||
"LabelClosePlayer": "Close player",
|
||||
"LabelCodec": "Codec",
|
||||
"LabelCollapseSeries": "Collapse Series",
|
||||
"LabelCollection": "Collection",
|
||||
"LabelCollections": "Collections",
|
||||
"LabelComplete": "Complete",
|
||||
"LabelConfirmPassword": "Confirm Password",
|
||||
@ -428,6 +430,7 @@
|
||||
"LabelShowAll": "Show All",
|
||||
"LabelSize": "Size",
|
||||
"LabelSleepTimer": "Sleep timer",
|
||||
"LabelSlug": "Slug",
|
||||
"LabelStart": "Start",
|
||||
"LabelStarted": "Started",
|
||||
"LabelStartedAt": "Started At",
|
||||
@ -516,6 +519,7 @@
|
||||
"MessageChapterErrorStartLtPrev": "Invalid start time must be greater than or equal to previous chapter start time",
|
||||
"MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook",
|
||||
"MessageCheckingCron": "Checking cron...",
|
||||
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
||||
"MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?",
|
||||
"MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?",
|
||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||
|
@ -5,6 +5,14 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
|
||||
class RSSFeedController {
|
||||
constructor() { }
|
||||
|
||||
async getAll(req, res) {
|
||||
const feeds = await this.rssFeedManager.getFeeds()
|
||||
res.json({
|
||||
feeds: feeds.map(f => f.toJSON()),
|
||||
minified: feeds.map(f => f.toJSONMinified())
|
||||
})
|
||||
}
|
||||
|
||||
// POST: api/feeds/item/:itemId/open
|
||||
async openRSSFeedForItem(req, res) {
|
||||
const options = req.body || {}
|
||||
|
@ -52,7 +52,7 @@ class RssFeedManager {
|
||||
|
||||
/**
|
||||
* Find open feed for an entity (e.g. collection id, playlist id, library item id)
|
||||
* @param {string} entityId
|
||||
* @param {string} entityId
|
||||
* @returns {Promise<objects.Feed>} oldFeed
|
||||
*/
|
||||
findFeedForEntityId(entityId) {
|
||||
@ -61,7 +61,7 @@ class RssFeedManager {
|
||||
|
||||
/**
|
||||
* Find open feed for a slug
|
||||
* @param {string} slug
|
||||
* @param {string} slug
|
||||
* @returns {Promise<objects.Feed>} oldFeed
|
||||
*/
|
||||
findFeedBySlug(slug) {
|
||||
@ -70,7 +70,7 @@ class RssFeedManager {
|
||||
|
||||
/**
|
||||
* Find open feed for a slug
|
||||
* @param {string} slug
|
||||
* @param {string} slug
|
||||
* @returns {Promise<objects.Feed>} oldFeed
|
||||
*/
|
||||
findFeed(id) {
|
||||
@ -262,5 +262,11 @@ class RssFeedManager {
|
||||
if (!feed) return
|
||||
return this.handleCloseFeed(feed)
|
||||
}
|
||||
|
||||
async getFeeds() {
|
||||
const feeds = await Database.models.feed.getOldFeeds()
|
||||
Logger.info(`[RssFeedManager] Fetched all feeds`)
|
||||
return feeds
|
||||
}
|
||||
}
|
||||
module.exports = RssFeedManager
|
||||
|
@ -296,9 +296,10 @@ class ApiRouter {
|
||||
this.router.post('/tools/item/:id/embed-metadata', ToolsController.middleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this))
|
||||
this.router.post('/tools/batch/embed-metadata', ToolsController.middleware.bind(this), ToolsController.batchEmbedMetadata.bind(this))
|
||||
|
||||
//
|
||||
//
|
||||
// RSS Feed Routes (Admin and up)
|
||||
//
|
||||
this.router.get('/feeds', RSSFeedController.getAll.bind(this))
|
||||
this.router.post('/feeds/item/:itemId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForItem.bind(this))
|
||||
this.router.post('/feeds/collection/:collectionId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForCollection.bind(this))
|
||||
this.router.post('/feeds/series/:seriesId/open', RSSFeedController.middleware.bind(this), RSSFeedController.openRSSFeedForSeries.bind(this))
|
||||
@ -354,8 +355,8 @@ class ApiRouter {
|
||||
//
|
||||
/**
|
||||
* Remove library item and associated entities
|
||||
* @param {string} mediaType
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} mediaType
|
||||
* @param {string} libraryItemId
|
||||
* @param {string[]} mediaItemIds array of bookId or podcastEpisodeId
|
||||
*/
|
||||
async handleDeleteLibraryItem(mediaType, libraryItemId, mediaItemIds) {
|
||||
@ -446,7 +447,7 @@ class ApiRouter {
|
||||
* Series is removed if it only has 1 book
|
||||
* TODO: Update filter data
|
||||
* @param {UUIDV4} bookId
|
||||
* @param {UUIDV4[]} seriesIds
|
||||
* @param {UUIDV4[]} seriesIds
|
||||
*/
|
||||
async checkRemoveEmptySeries(bookId, seriesIds) {
|
||||
if (!seriesIds?.length) return
|
||||
|
Loading…
Reference in New Issue
Block a user