diff --git a/client/pages/audiobook/_id/index.vue b/client/pages/audiobook/_id/index.vue index d0d16a5c..11cbf9f6 100644 --- a/client/pages/audiobook/_id/index.vue +++ b/client/pages/audiobook/_id/index.vue @@ -23,6 +23,8 @@ editEdit + Open RSS Feed +

Your Progress: {{ Math.round(progressPercent * 100) }}%

{{ $elapsedPretty(userTimeRemaining) }} remaining

@@ -82,6 +84,9 @@ export default { } }, computed: { + isDeveloperMode() { + return this.$store.state.developerMode + }, missingPartChunks() { if (this.missingParts === 1) return this.missingParts[0] var chunks = [] @@ -180,6 +185,18 @@ export default { } }, methods: { + openRssFeed() { + this.$axios + .$post('/api/feed', { audiobookId: this.audiobook.id }) + .then((res) => { + console.log('Feed open', res) + this.$toast.success('RSS Feed Open') + }) + .catch((error) => { + console.error('Failed', error) + this.$toast.error('Failed to open feed') + }) + }, startStream() { this.$store.commit('setStreamAudiobook', this.audiobook) this.$root.socket.emit('open_stream', this.audiobook.id) diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 7e4d0b0e..001f31b2 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -53,6 +53,7 @@
+
@@ -70,6 +71,11 @@ export default { } }, methods: { + setDeveloperMode() { + var value = !this.$store.state.developerMode + this.$store.commit('setDeveloperMode', value) + this.$toast.info(`Developer Mode ${value ? 'Enabled' : 'Disabled'}`) + }, scan() { this.$root.socket.emit('scan') }, diff --git a/client/store/index.js b/client/store/index.js index 650301a6..2e1d56ab 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -6,7 +6,8 @@ export const state = () => ({ selectedAudiobook: null, playOnLoad: false, isScanning: false, - scanProgress: null + scanProgress: null, + developerMode: false }) export const getters = { @@ -59,5 +60,8 @@ export const mutations = { setScanProgress(state, progress) { if (progress > 0) state.isScanning = true state.scanProgress = progress + }, + setDeveloperMode(state, val) { + state.developerMode = val } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0b84c526..d27750ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "0.9.61-beta.0", + "version": "0.9.64-beta", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -561,6 +561,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -833,6 +838,14 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" }, + "podcast": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/podcast/-/podcast-1.3.0.tgz", + "integrity": "sha512-L0UNP8SMdoihxgpdXCaXZEKZBBCGzld5PSy8QbQYsk83bdzq14cdW8flJduZjQNbB2If5frwVIC5VpMq9CHchA==", + "requires": { + "rss": "^1.2.2" + } + }, "proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -913,6 +926,30 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, + "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.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1101,6 +1138,11 @@ "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" } } } diff --git a/package.json b/package.json index c0e21a58..42741009 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,13 @@ "express": "^4.17.1", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^10.0.0", + "ip": "^1.1.5", "jsonwebtoken": "^8.5.1", "libgen": "^2.1.0", "njodb": "^0.4.20", "node-dir": "^0.1.17", + "podcast": "^1.3.0", "socket.io": "^4.1.3" }, "devDependencies": {} -} \ No newline at end of file +} diff --git a/server/ApiController.js b/server/ApiController.js index ab0dd248..b852e4c8 100644 --- a/server/ApiController.js +++ b/server/ApiController.js @@ -2,11 +2,12 @@ const express = require('express') const Logger = require('./Logger') class ApiController { - constructor(db, scanner, auth, streamManager, emitter) { + constructor(db, scanner, auth, streamManager, rssFeeds, emitter) { this.db = db this.scanner = scanner this.auth = auth this.streamManager = streamManager + this.rssFeeds = rssFeeds this.emitter = emitter this.router = express() @@ -35,6 +36,8 @@ class ApiController { this.router.post('/authorize', this.authorize.bind(this)) this.router.get('/genres', this.getGenres.bind(this)) + + this.router.post('/feed', this.openRssFeed.bind(this)) } find(req, res) { @@ -42,7 +45,6 @@ class ApiController { } findCovers(req, res) { - console.log('Find covers', req.query) this.scanner.findCovers(req, res) } @@ -174,6 +176,15 @@ class ApiController { this.auth.userChangePassword(req, res) } + async openRssFeed(req, res) { + var audiobookId = req.body.audiobookId + var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId) + if (!audiobook) return res.sendStatus(404) + var feed = await this.rssFeeds.openFeed(audiobook) + console.log('Feed open', feed) + res.json(feed) + } + getGenres(req, res) { res.json({ genres: this.db.getGenres() diff --git a/server/Auth.js b/server/Auth.js index f4cd57a7..ca626eb8 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -75,6 +75,10 @@ class Auth { verifyToken(token) { return new Promise((resolve) => { jwt.verify(token, process.env.TOKEN_SECRET, (err, payload) => { + if (!payload || err) { + Logger.error('JWT Verify Token Failed', err) + return resolve(null) + } var user = this.users.find(u => u.id === payload.userId) resolve(user || null) }) diff --git a/server/RssFeeds.js b/server/RssFeeds.js new file mode 100644 index 00000000..2f98d26a --- /dev/null +++ b/server/RssFeeds.js @@ -0,0 +1,53 @@ +const Podcast = require('podcast') +const express = require('express') +const ip = require('ip') +const Logger = require('./Logger') + +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) { + 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) { + var serverAddress = 'http://' + ip.address('public', 'ipv4') + ':' + 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', + feedUrl: `${serverAddress}/feeds/${feedId}`, + imageUrl: `${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 e7eb1935..07423a50 100644 --- a/server/Server.js +++ b/server/Server.js @@ -11,6 +11,7 @@ const Db = require('./Db') const ApiController = require('./ApiController') const HlsController = require('./HlsController') const StreamManager = require('./StreamManager') +const RssFeeds = require('./RssFeeds') const Logger = require('./Logger') class Server { @@ -30,9 +31,11 @@ class Server { this.watcher = new Watcher(this.AudiobookPath) this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.emitter.bind(this)) this.streamManager = new StreamManager(this.db, this.MetadataPath) - this.apiController = new ApiController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this)) + this.rssFeeds = new RssFeeds(this.Port, this.db) + this.apiController = new ApiController(this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.emitter.bind(this)) this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.MetadataPath) + this.server = null this.io = null @@ -112,11 +115,13 @@ class Server { } app.use(express.static(this.MetadataPath)) + app.use(express.static(Path.join(global.appRoot, 'static'))) app.use(express.urlencoded({ extended: true })); app.use(express.json()) app.use('/api', this.authMiddleware.bind(this), this.apiController.router) app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router) + app.use('/feeds', this.rssFeeds.router) app.get('/', (req, res) => { res.sendFile('/index.html') diff --git a/server/utils/scandir.js b/server/utils/scandir.js index bbd00099..63592652 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -39,13 +39,15 @@ async function getAllAudiobookFiles(abRootPath) { var pathformat = Path.parse(relpath) var path = pathformat.dir - // If relative file directory has 3 folders, then the middle folder will be series - var splitDir = pathformat.dir.split(Path.sep) - if (splitDir.length === 1) { - Logger.error('Invalid file in root dir', filepath) + if (!path) { + Logger.error('Ignoring file in root dir', filepath) return } - var author = splitDir.shift() + + // If relative file directory has 3 folders, then the middle folder will be series + var splitDir = pathformat.dir.split(Path.sep) + var author = null + if (splitDir.length > 1) author = splitDir.shift() var series = null if (splitDir.length > 1) series = splitDir.shift() var title = splitDir.shift() diff --git a/static/Logo.png b/static/Logo.png new file mode 100644 index 00000000..5a5c4be4 Binary files /dev/null and b/static/Logo.png differ