Add:experimental generate podcast feed for testing

This commit is contained in:
advplyr 2022-05-02 14:41:59 -05:00
parent 7373c7159b
commit 8b38dda229
8 changed files with 261 additions and 98 deletions

View File

@ -156,6 +156,11 @@
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" text="Find Episodes" direction="top">
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
</ui-tooltip>
<!-- Experimental RSS feed open -->
<ui-tooltip v-if="isPodcast && showExperimentalFeatures" text="Open RSS Feed" direction="top">
<ui-icon-btn icon="rss_feed" class="mx-0.5" outlined @click="openRSSFeed" />
</ui-tooltip>
</div>
<div class="my-4 max-w-2xl">
@ -478,6 +483,22 @@ export default {
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setShowUserCollectionsModal', true)
},
openRSSFeed() {
const payload = {
serverAddress: window.origin
}
if (this.$isDev) payload.serverAddress = 'http://localhost:3333'
console.log('Payload', payload)
this.$axios
.$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload)
.then((data) => {
console.log('Opened RSS Feed', data)
})
.catch((error) => {
console.error('Failed to open RSS Feed', error)
})
},
episodeDownloadQueued(episodeDownload) {
if (episodeDownload.libraryItemId === this.libraryItemId) {
this.episodeDownloadsQueued.push(episodeDownload)

84
package-lock.json generated
View File

@ -1,12 +1,11 @@
{
"name": "audiobookshelf",
"version": "1.7.3",
"version": "2.0.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
"version": "1.7.3",
"version": "2.0.8",
"license": "GPL-3.0",
"dependencies": {
"archiver": "^5.3.0",
@ -26,6 +25,7 @@
"node-cron": "^3.0.0",
"node-ffprobe": "^3.0.0",
"node-stream-zip": "^1.15.0",
"podcast": "^2.0.0",
"proper-lockfile": "^4.1.2",
"read-chunk": "^3.1.0",
"recursive-readdir-async": "^1.1.8",
@ -1477,6 +1477,14 @@
"node": ">=6"
}
},
"node_modules/podcast": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.0.tgz",
"integrity": "sha512-1NZe7cVabfkcMe39yOOhBMJrXC6OROLOIBkfEPayiJL59ncyJmOeO5bxolSCSGroho8jQ0zURMczyICI1U/xbw==",
"dependencies": {
"rss": "^1.2.2"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -1675,6 +1683,34 @@
"atomically": "^1.7.0"
}
},
"node_modules/rss": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
"integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=",
"dependencies": {
"mime-types": "2.1.13",
"xml": "1.0.1"
}
},
"node_modules/rss/node_modules/mime-db": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz",
"integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/rss/node_modules/mime-types": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz",
"integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=",
"dependencies": {
"mime-db": "~1.25.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -2071,6 +2107,11 @@
}
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU="
},
"node_modules/xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
@ -3222,6 +3263,14 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
},
"podcast": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.0.tgz",
"integrity": "sha512-1NZe7cVabfkcMe39yOOhBMJrXC6OROLOIBkfEPayiJL59ncyJmOeO5bxolSCSGroho8jQ0zURMczyICI1U/xbw==",
"requires": {
"rss": "^1.2.2"
}
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -3387,6 +3436,30 @@
"atomically": "^1.7.0"
}
},
"rss": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
"integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=",
"requires": {
"mime-types": "2.1.13",
"xml": "1.0.1"
},
"dependencies": {
"mime-db": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz",
"integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I="
},
"mime-types": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz",
"integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=",
"requires": {
"mime-db": "~1.25.0"
}
}
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -3690,6 +3763,11 @@
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
},
"xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU="
},
"xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",

View File

@ -44,6 +44,7 @@
"node-cron": "^3.0.0",
"node-ffprobe": "^3.0.0",
"node-stream-zip": "^1.15.0",
"podcast": "^2.0.0",
"proper-lockfile": "^4.1.2",
"read-chunk": "^3.1.0",
"recursive-readdir-async": "^1.1.8",
@ -53,4 +54,4 @@
"xml2js": "^0.4.23"
},
"devDependencies": {}
}
}

View File

@ -1,59 +0,0 @@
// const Podcast = require('podcast')
const express = require('express')
// const ip = require('ip')
const Logger = require('./Logger')
// Not functional at the moment - just an idea
class RssFeeds {
constructor(Port, db) {
this.Port = Port
this.db = db
this.feeds = {}
this.router = express()
this.init()
}
init() {
this.router.get('/:id', this.getFeed.bind(this))
}
getFeed(req, res) {
Logger.info('Get Feed', req.params.id, this.feeds[req.params.id])
var feed = this.feeds[req.params.id]
if (!feed) return null
var xml = feed.buildXml()
res.set('Content-Type', 'text/xml')
res.send(xml)
}
openFeed(audiobook) {
// Removed Podcast npm package and ip package
return null
// var ipAddress = ip.address('public', 'ipv4')
// var serverAddress = 'http://' + ipAddress + ':' + this.Port
// Logger.info('Open RSS Feed', 'Server address', serverAddress)
// var feedId = (Date.now() + Math.floor(Math.random() * 1000)).toString(36)
// const feed = new Podcast({
// title: audiobook.title,
// description: 'AudioBookshelf RSS Feed',
// feed_url: `${serverAddress}/feeds/${feedId}`,
// image_url: `${serverAddress}/Logo.png`,
// author: 'advplyr',
// language: 'en'
// })
// audiobook.tracks.forEach((track) => {
// feed.addItem({
// title: `Track ${track.index}`,
// description: `AudioBookshelf Audiobook Track #${track.index}`,
// url: `${serverAddress}/feeds/${feedId}?track=${track.index}`,
// author: 'advplyr'
// })
// })
// this.feeds[feedId] = feed
// return feed
}
}
module.exports = RssFeeds

View File

@ -31,6 +31,7 @@ const BackupManager = require('./managers/BackupManager')
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
const PodcastManager = require('./managers/PodcastManager')
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
const RssFeedManager = require('./managers/RssFeedManager')
class Server {
constructor(PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
@ -74,11 +75,12 @@ class Server {
this.coverManager = new CoverManager(this.db, this.cacheManager)
this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this))
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
this.rssFeedManager = new RssFeedManager(this.db)
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
// Routers
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.emitter.bind(this), this.clientEmitter.bind(this))
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.emitter.bind(this), this.clientEmitter.bind(this))
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
this.staticRouter = new StaticRouter(this.db)
@ -198,6 +200,16 @@ class Server {
res.sendFile(fullPath)
})
// RSS Feed temp route
app.get('/feed/:id', (req, res) => {
Logger.info(`[Server] requesting rss feed ${req.params.id}`)
this.rssFeedManager.getFeed(req, res)
})
app.get('/feed/:id/item/*', (req, res) => {
Logger.info(`[Server] requesting rss feed ${req.params.id}`)
this.rssFeedManager.getFeedItem(req, res)
})
// Client dynamic routes
app.get('/item/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
app.get('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))

View File

@ -120,14 +120,7 @@ class PodcastController {
return res.sendStatus(500)
}
var libraryItem = this.db.getLibraryItem(req.params.id)
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
return res.sendStatus(404)
}
if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
Logger.error(`[PodcastController] User attempted to check/download episodes for a library without permission`, req.user)
return res.sendStatus(500)
}
var libraryItem = req.libraryItem
if (!libraryItem.media.metadata.feedUrl) {
Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${libraryItem.id}`)
return res.status(500).send('Podcast has no rss feed url')
@ -149,10 +142,8 @@ class PodcastController {
}
getEpisodeDownloads(req, res) {
var libraryItem = this.db.getLibraryItem(req.params.id)
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
return res.sendStatus(404)
}
var libraryItem = req.libraryItem
var downloadsInQueue = this.podcastManager.getEpisodeDownloadsInQueue(libraryItem.id)
res.json({
downloads: downloadsInQueue.map(d => d.toJSONForClient())
@ -164,15 +155,7 @@ class PodcastController {
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
return res.sendStatus(500)
}
var libraryItem = this.db.getLibraryItem(req.params.id)
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
return res.sendStatus(404)
}
if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
Logger.error(`[PodcastController] User attempted to download episodes for library without permission`, req.user)
return res.sendStatus(404)
}
var libraryItem = req.libraryItem
var episodes = req.body
if (!episodes || !episodes.length) {
@ -183,14 +166,22 @@ class PodcastController {
res.sendStatus(200)
}
async openPodcastFeed(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempted to open podcast feed`, req.user)
return res.sendStatus(500)
}
const feedData = this.rssFeedManager.openPodcastFeed(req.user, req.libraryItem, req.body)
res.json({
success: true,
feedUrl: feedData.feedUrl
})
}
async updateEpisode(req, res) {
var libraryItem = this.db.getLibraryItem(req.params.id)
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
return res.sendStatus(404)
}
if (!req.user.canUpload || !req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
return res.sendStatus(404)
}
var libraryItem = req.libraryItem
var episodeId = req.params.episodeId
if (!libraryItem.media.checkHasEpisode(episodeId)) {
@ -205,5 +196,35 @@ class PodcastController {
res.json(libraryItem.toJSONExpanded())
}
middleware(req, res, next) {
var item = this.db.libraryItems.find(li => li.id === req.params.id)
if (!item || !item.media) return res.sendStatus(404)
if (!item.isPodcast) {
return res.sendStatus(500)
}
// Check user can access this library
if (!req.user.checkCanAccessLibrary(item.libraryId)) {
return res.sendStatus(403)
}
// Check user can access this library item
if (!req.user.checkCanAccessLibraryItemWithTags(item.media.tags)) {
return res.sendStatus(403)
}
if (req.method == 'DELETE' && !req.user.canDelete) {
Logger.warn(`[PodcastController] User attempted to delete without permission`, req.user.username)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {
Logger.warn('[PodcastController] User attempted to update without permission', req.user.username)
return res.sendStatus(403)
}
req.libraryItem = item
next()
}
}
module.exports = new PodcastController()

View File

@ -0,0 +1,87 @@
const Path = require('path')
const { Podcast } = require('podcast')
const { getId } = require('../utils/index')
const Logger = require('../Logger')
// Not functional at the moment
class RssFeedManager {
constructor(db) {
this.db = db
this.feeds = {}
}
getFeed(req, res) {
var feedData = this.feeds[req.params.id]
if (!feedData) {
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
res.sendStatus(404)
return
}
var xml = feedData.feed.buildXml()
res.set('Content-Type', 'text/xml')
res.send(xml)
}
getFeedItem(req, res) {
var feedData = this.feeds[req.params.id]
if (!feedData) {
Logger.error(`[RssFeedManager] Feed not found ${req.params.id}`)
res.sendStatus(404)
return
}
var remainingPath = req.params['0']
var fullPath = Path.join(feedData.libraryItemPath, remainingPath)
res.sendFile(fullPath)
}
openFeed(feedId, libraryItem, serverAddress) {
const podcast = libraryItem.media
const feedUrl = `${serverAddress}/feed/${feedId}`
// Removed Podcast npm package and ip package
const feed = new Podcast({
title: podcast.metadata.title,
description: podcast.metadata.description,
feedUrl,
imageUrl: `${serverAddress}/Logo.png`,
author: podcast.metadata.author || 'advplyr',
language: 'en'
})
podcast.episodes.forEach((episode) => {
var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/')
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${feedId}/item`)
feed.addItem({
title: episode.title,
description: episode.description || '',
enclosure: {
url: `${serverAddress}${contentUrl}`,
type: episode.audioTrack.mimeType,
size: episode.size
},
url: `${serverAddress}${contentUrl}`,
author: podcast.metadata.author || 'advplyr'
})
})
const feedData = {
id: feedId,
libraryItemId: libraryItem.id,
libraryItemPath: libraryItem.path,
serverAddress: serverAddress,
feedUrl,
feed
}
this.feeds[feedId] = feedData
return feedData
}
openPodcastFeed(user, libraryItem, options) {
const serverAddress = options.serverAddress
const feedId = getId('feed')
const feedData = this.openFeed(feedId, libraryItem, serverAddress)
Logger.debug(`[RssFeedManager] Opened podcast feed ${feedData.feedUrl}`)
return feedData
}
}
module.exports = RssFeedManager

View File

@ -25,7 +25,7 @@ const Series = require('../objects/entities/Series')
const FileSystemController = require('../controllers/FileSystemController')
class ApiRouter {
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, emitter, clientEmitter) {
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, emitter, clientEmitter) {
this.db = db
this.auth = auth
this.scanner = scanner
@ -37,6 +37,7 @@ class ApiRouter {
this.cacheManager = cacheManager
this.podcastManager = podcastManager
this.audioMetadataManager = audioMetadataManager
this.rssFeedManager = rssFeedManager
this.emitter = emitter
this.clientEmitter = clientEmitter
@ -180,11 +181,12 @@ class ApiRouter {
//
this.router.post('/podcasts', PodcastController.create.bind(this))
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
this.router.get('/podcasts/:id/checknew', PodcastController.checkNewEpisodes.bind(this))
this.router.get('/podcasts/:id/downloads', PodcastController.getEpisodeDownloads.bind(this))
this.router.get('/podcasts/:id/clear-queue', PodcastController.clearEpisodeDownloadQueue.bind(this))
this.router.post('/podcasts/:id/download-episodes', PodcastController.downloadEpisodes.bind(this))
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.updateEpisode.bind(this))
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this))
this.router.post('/podcasts/:id/open-feed', PodcastController.middleware.bind(this), PodcastController.openPodcastFeed.bind(this))
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this))
//
// Misc Routes