diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 5ef36f3f..b57566a2 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -156,6 +156,11 @@ + + + + +
@@ -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) diff --git a/package-lock.json b/package-lock.json index ef0af7dd..8d4a2c19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index a556c00f..c6a0f5f5 100644 --- a/package.json +++ b/package.json @@ -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": {} -} \ No newline at end of file +} diff --git a/server/RssFeeds.js b/server/RssFeeds.js deleted file mode 100644 index 456fb012..00000000 --- a/server/RssFeeds.js +++ /dev/null @@ -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 \ No newline at end of file diff --git a/server/Server.js b/server/Server.js index 06b131ec..b3c9a159 100644 --- a/server/Server.js +++ b/server/Server.js @@ -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'))) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index c332050f..88cc0888 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -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() \ No newline at end of file diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js new file mode 100644 index 00000000..867ce187 --- /dev/null +++ b/server/managers/RssFeedManager.js @@ -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 \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index bfd25b0e..2e280efd 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -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