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