-
-
Backups
-
-
-
Backups include users, user progress, book details, server settings and covers stored in /metadata/books . Backups do not include any files stored in your library folders.
-
-
-
-
- Run daily backups info_outlined
-
-
-
-
-
-
-
Number of backups to keep
-
-
-
+
+
+
+ Scanner parse subtitles info_outlined
+
-
-
-
-
Reset All Audiobooks
-
-
View Logger
+
+
+
+ Scanner find covers info_outlined
+
-
-
-
-
v{{ $config.version }}
-
-
Report bugs, request features, provide feedback, and contribute on github .
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Experimental Features info_outlined
-
-
-
-
-
-
+
+
+
+ Store covers with audiobook info_outlined
+
-
+
+
+
+
Reset All Audiobooks
+
+
Report bugs, request features, provide feedback, and contribute on github .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Experimental Features info_outlined
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/pages/config/libraries.vue b/client/pages/config/libraries.vue
new file mode 100644
index 00000000..c7db6ed5
--- /dev/null
+++ b/client/pages/config/libraries.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/client/pages/config/users/_id.vue b/client/pages/config/users/_id.vue
new file mode 100644
index 00000000..3cc39012
--- /dev/null
+++ b/client/pages/config/users/_id.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+ arrow_back
+
+
All Users
+
+
+
+
+
{{ username }}
+
+
+
+
Reading Progress
+
+
+ Book
+
+ Progress
+ Started At
+ Last Update
+
+
+
+
+
+
+ {{ ab.book ? ab.book.title : ab.audiobookTitle || 'Unknown' }}
+ by {{ ab.book.author }}
+
+ {{ Math.floor(ab.progress * 100) }}%
+
+
+ {{ $dateDistanceFromNow(ab.startedAt) }}
+
+
+
+
+ {{ $dateDistanceFromNow(ab.lastUpdate) }}
+
+
+
+
+
Nothing read yet...
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/pages/config/users/index.vue b/client/pages/config/users/index.vue
new file mode 100644
index 00000000..a47eedc4
--- /dev/null
+++ b/client/pages/config/users/index.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/client/pages/login.vue b/client/pages/login.vue
index f2758c0c..62231a8b 100644
--- a/client/pages/login.vue
+++ b/client/pages/login.vue
@@ -48,6 +48,24 @@ export default {
}
},
methods: {
+ setUser(user) {
+ // If user is not able to access main library, then set current library
+ // var userLibrariesAccessible = this.$store.getters['user/getLibrariesAccessible']
+ var userCanAccessAll = user.permissions ? !!user.permissions.accessAllLibraries : false
+ if (!userCanAccessAll) {
+ var accessibleLibraries = user.librariesAccessible || []
+ console.log('Setting user without all library access', accessibleLibraries)
+ if (accessibleLibraries.length && !accessibleLibraries.includes('main')) {
+ console.log('Setting current library', accessibleLibraries[0])
+ this.$store.commit('libraries/setCurrentLibrary', accessibleLibraries[0])
+ }
+ }
+ // if (userLibrariesAccessible.length && !userLibrariesAccessible.includes('main')) {
+ // this.$store.commit('libraries/setCurrentLibrary', userLibrariesAccessible[0])
+ // }
+
+ this.$store.commit('user/setUser', user)
+ },
async submitForm() {
this.error = null
this.processing = true
@@ -65,7 +83,7 @@ export default {
if (authRes && authRes.error) {
this.error = authRes.error
} else if (authRes) {
- this.$store.commit('user/setUser', authRes.user)
+ this.setUser(authRes.user)
}
this.processing = false
},
@@ -83,7 +101,7 @@ export default {
}
})
.then((res) => {
- this.$store.commit('user/setUser', res.user)
+ this.setUser(res.user)
this.processing = false
})
.catch((error) => {
diff --git a/client/plugins/constants.js b/client/plugins/constants.js
index dbd71632..fd631ff5 100644
--- a/client/plugins/constants.js
+++ b/client/plugins/constants.js
@@ -15,6 +15,17 @@ const Constants = {
CoverDestination
}
+const Hotkeys = {
+ PLAY_PAUSE: 32, // Space
+ JUMP_FORWARD: 39, // ArrowRight
+ JUMP_BACKWARD: 37, // ArrowLeft
+ CLOSE: 27, // ESCAPE
+ VOLUME_UP: 38, // ArrowUp
+ VOLUME_DOWN: 40, // ArrowDown
+ MUTE: 77, // M
+}
+
export default ({ app }, inject) => {
inject('constants', Constants)
+ inject('hotkeys', Hotkeys)
}
\ No newline at end of file
diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js
index 924b13b0..ad8611ab 100644
--- a/client/plugins/init.client.js
+++ b/client/plugins/init.client.js
@@ -1,6 +1,8 @@
import Vue from 'vue'
import { formatDistance, format } from 'date-fns'
+Vue.prototype.$eventBus = new Vue()
+
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
Vue.prototype.$dateDistanceFromNow = (unixms) => {
diff --git a/client/store/index.js b/client/store/index.js
index 1cf36fd8..5d7334bb 100644
--- a/client/store/index.js
+++ b/client/store/index.js
@@ -18,14 +18,18 @@ export const state = () => ({
routeHistory: [],
showExperimentalFeatures: false,
backups: [],
- bookshelfBookIds: []
+ bookshelfBookIds: [],
+ openModal: null
})
export const getters = {
getIsAudiobookSelected: state => audiobookId => {
return !!state.selectedAudiobooks.includes(audiobookId)
},
- getNumAudiobooksSelected: state => state.selectedAudiobooks.length
+ getNumAudiobooksSelected: state => state.selectedAudiobooks.length,
+ getAudiobookIdStreaming: state => {
+ return state.streamAudiobook ? state.streamAudiobook.id : null
+ }
}
export const actions = {
@@ -155,5 +159,8 @@ export const mutations = {
},
setBackups(state, val) {
state.backups = val.sort((a, b) => b.createdAt - a.createdAt)
+ },
+ setOpenModal(state, val) {
+ state.openModal = val
}
}
\ No newline at end of file
diff --git a/client/store/libraries.js b/client/store/libraries.js
index 84d5c56f..5aa0ac27 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -3,7 +3,6 @@ export const state = () => ({
lastLoad: 0,
listeners: [],
currentLibraryId: 'main',
- showModal: false,
folders: [],
folderLastUpdate: 0
})
@@ -42,12 +41,18 @@ export const actions = {
return []
})
},
- fetch({ state, commit, rootState }, libraryId) {
+ fetch({ state, commit, rootState, rootGetters }, libraryId) {
if (!rootState.user || !rootState.user.user) {
console.error('libraries/fetch - User not set')
return false
}
+ var canUserAccessLibrary = rootGetters['user/getCanAccessLibrary'](libraryId)
+ if (!canUserAccessLibrary) {
+ console.warn('Access not allowed to library')
+ return false
+ }
+
var library = state.libraries.find(lib => lib.id === libraryId)
if (library) {
commit('setCurrentLibrary', libraryId)
@@ -102,9 +107,6 @@ export const mutations = {
setFoldersLastUpdate(state) {
state.folderLastUpdate = Date.now()
},
- setShowModal(state, val) {
- state.showModal = val
- },
setLastLoad(state) {
state.lastLoad = Date.now()
},
diff --git a/client/store/user.js b/client/store/user.js
index 25420d2e..ecefb2ac 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -33,6 +33,19 @@ export const getters = {
},
getUserCanUpload: (state) => {
return state.user && state.user.permissions ? !!state.user.permissions.upload : false
+ },
+ getUserCanAccessAllLibraries: (state) => {
+ return state.user && state.user.permissions ? !!state.user.permissions.accessAllLibraries : false
+ },
+ getLibrariesAccessible: (state, getters) => {
+ if (!state.user) return []
+ if (getters.getUserCanAccessAllLibraries) return []
+ return state.user.librariesAccessible || []
+ },
+ getCanAccessLibrary: (state, getters) => (libraryId) => {
+ if (!state.user) return false
+ if (getters.getUserCanAccessAllLibraries) return true
+ return getters.getLibrariesAccessible.includes(libraryId)
}
}
@@ -60,6 +73,7 @@ export const actions = {
export const mutations = {
setUser(state, user) {
state.user = user
+
if (user) {
if (user.token) localStorage.setItem('token', user.token)
} else {
diff --git a/client/store/users.js b/client/store/users.js
index fc08486c..131b7d8c 100644
--- a/client/store/users.js
+++ b/client/store/users.js
@@ -4,7 +4,9 @@ export const state = () => ({
})
export const getters = {
-
+ getIsUserOnline: state => id => {
+ return state.users.find(u => u.id === id)
+ }
}
export const actions = {
@@ -12,6 +14,9 @@ export const actions = {
}
export const mutations = {
+ resetUsers(state) {
+ state.users = []
+ },
updateUser(state, user) {
var index = state.users.findIndex(u => u.id === user.id)
if (index >= 0) {
diff --git a/package-lock.json b/package-lock.json
index 5e5e530a..dbcf0137 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "1.4.3",
+ "version": "1.4.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1222,9 +1222,9 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"njodb": {
- "version": "0.4.20",
- "resolved": "https://registry.npmjs.org/njodb/-/njodb-0.4.20.tgz",
- "integrity": "sha512-y/V9yTSa6fXlfkD453o8engmbFvMabpogSYt53sNft48oqzO5tk4OTl564Zf2IN8JtJDp4ShnZE4hIXePqfvhg==",
+ "version": "0.4.21",
+ "resolved": "https://registry.npmjs.org/njodb/-/njodb-0.4.21.tgz",
+ "integrity": "sha512-3qLMzwIZUgT1yq2PCzJlT6FFK/zfLHz71QnFeE9ec4KKJH9abY4SXnmHVaWP7wVq+lY77wW1F+EeKG9gm8j6WA==",
"requires": {
"proper-lockfile": "^4.1.2"
}
diff --git a/package.json b/package.json
index 6946d653..7d7dfd3b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "1.4.13",
+ "version": "1.4.14",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
@@ -37,7 +37,7 @@
"ip": "^1.1.5",
"jsonwebtoken": "^8.5.1",
"libgen": "^2.1.0",
- "njodb": "^0.4.20",
+ "njodb": "^0.4.21",
"node-cron": "^3.0.0",
"node-dir": "^0.1.17",
"node-stream-zip": "^1.15.0",
diff --git a/server/ApiController.js b/server/ApiController.js
index bd3c24e1..a16f21aa 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -63,6 +63,7 @@ class ApiController {
this.router.patch('/user/settings', this.userUpdateSettings.bind(this))
this.router.get('/users', this.getUsers.bind(this))
this.router.post('/user', this.createUser.bind(this))
+ this.router.get('/user/:id', this.getUser.bind(this))
this.router.patch('/user/:id', this.updateUser.bind(this))
this.router.delete('/user/:id', this.deleteUser.bind(this))
@@ -314,8 +315,17 @@ class ApiController {
}
getAudiobook(req, res) {
+ if (!req.user) {
+ return res.sendStatus(403)
+ }
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
+
+ // Check user can access this audiobooks library
+ if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
+ return res.sendStatus(403)
+ }
+
res.json(audiobook.toJSONExpanded())
}
@@ -522,20 +532,23 @@ class ApiController {
res.sendStatus(200)
}
- getUsers(req, res) {
- if (req.user.type !== 'root') return res.sendStatus(403)
- return res.json(this.db.users.map(u => u.toJSONForBrowser()))
- }
-
async resetUserAudiobookProgress(req, res) {
- req.user.resetAudiobookProgress(req.params.id)
+ var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
+ if (!audiobook) {
+ return res.status(404).send('Audiobook not found')
+ }
+ req.user.resetAudiobookProgress(audiobook)
await this.db.updateEntity('user', req.user)
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.sendStatus(200)
}
async updateUserAudiobookProgress(req, res) {
- var wasUpdated = req.user.updateAudiobookProgress(req.params.id, req.body)
+ var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id)
+ if (!audiobook) {
+ return res.status(404).send('Audiobook not found')
+ }
+ var wasUpdated = req.user.updateAudiobookProgress(audiobook, req.body)
if (wasUpdated) {
await this.db.updateEntity('user', req.user)
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
@@ -551,8 +564,11 @@ class ApiController {
var shouldUpdate = false
abProgresses.forEach((progress) => {
- var wasUpdated = req.user.updateAudiobookProgress(progress.audiobookId, progress)
- if (wasUpdated) shouldUpdate = true
+ var audiobook = this.db.audiobooks.find(ab => ab.id === progress.audiobookId)
+ if (audiobook) {
+ var wasUpdated = req.user.updateAudiobookProgress(audiobook, progress)
+ if (wasUpdated) shouldUpdate = true
+ }
})
if (shouldUpdate) {
@@ -591,6 +607,30 @@ class ApiController {
})
}
+ userJsonWithBookProgressDetails(user) {
+ var json = user.toJSONForBrowser()
+
+ // User audiobook progress attach book details
+ if (json.audiobooks && Object.keys(json.audiobooks).length) {
+ for (const audiobookId in json.audiobooks) {
+ var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId)
+ if (!audiobook) {
+ Logger.error('[ApiController] Audiobook not found for users progress ' + audiobookId)
+ } else {
+ json.audiobooks[audiobookId].book = audiobook.book.toJSON()
+ }
+ }
+ }
+
+ return json
+ }
+
+ getUsers(req, res) {
+ if (req.user.type !== 'root') return res.sendStatus(403)
+ var users = this.db.users.map(u => this.userJsonWithBookProgressDetails(u))
+ res.json(users)
+ }
+
async createUser(req, res) {
if (!req.user.isRoot) {
Logger.warn('Non-root user attempted to create user', req.user)
@@ -621,6 +661,20 @@ class ApiController {
}
}
+ async getUser(req, res) {
+ if (!req.user.isRoot) {
+ Logger.error('User other than root attempting to get user', req.user)
+ return res.sendStatus(403)
+ }
+
+ var user = this.db.users.find(u => u.id === req.params.id)
+ if (!user) {
+ return res.sendStatus(404)
+ }
+
+ res.json(this.userJsonWithBookProgressDetails(user))
+ }
+
async updateUser(req, res) {
if (!req.user.isRoot) {
Logger.error('User other than root attempting to update user', req.user)
diff --git a/server/Server.js b/server/Server.js
index 9772241f..2c7d9403 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -46,7 +46,7 @@ class Server {
this.watcher = new Watcher(this.AudiobookPath)
this.coverController = new CoverController(this.db, this.MetadataPath, this.AudiobookPath)
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this))
- this.streamManager = new StreamManager(this.db, this.MetadataPath)
+ this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this))
this.rssFeeds = new RssFeeds(this.Port, this.db)
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this))
this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.emitter.bind(this), this.clientEmitter.bind(this))
@@ -57,8 +57,6 @@ class Server {
this.io = null
this.clients = {}
-
- this.isScanningCovers = false
}
get audiobooks() {
@@ -70,6 +68,11 @@ class Server {
get serverSettings() {
return this.db.serverSettings
}
+ get usersOnline() {
+ return Object.values(this.clients).filter(c => c.user).map(client => {
+ return client.user.toJSONForPublic(this.streamManager.streams)
+ })
+ }
getClientsForUser(userId) {
return Object.values(this.clients).filter(c => c.user && c.user.id === userId)
@@ -83,6 +86,7 @@ class Server {
clientEmitter(userId, ev, data) {
var clients = this.getClientsForUser(userId)
if (!clients.length) {
+ console.log('clients', clients)
return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`)
}
clients.forEach((client) => {
@@ -193,7 +197,7 @@ class Server {
var loginRateLimiter = this.getLoginRateLimiter()
app.post('/login', loginRateLimiter, (req, res) => this.auth.login(req, res))
- app.post('/logout', this.logout.bind(this))
+ app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
app.get('/ping', (req, res) => {
Logger.info('Recieved ping')
@@ -203,10 +207,6 @@ class Server {
// Used in development to set-up streams without authentication
if (process.env.NODE_ENV !== 'production') {
app.use('/test-hls', this.hlsController.router)
- app.get('/test-stream/:id', async (req, res) => {
- var uri = await this.streamManager.openTestStream(this.MetadataPath, req.params.id)
- res.send(uri)
- })
app.get('/catalog.json', (req, res) => {
Logger.error('Catalog request made', req.headers)
res.json()
@@ -269,15 +269,16 @@ class Server {
var _client = this.clients[socket.id]
if (!_client) {
- Logger.warn('[SOCKET] Socket disconnect, no client ' + socket.id)
+ Logger.warn('[Server] Socket disconnect, no client ' + socket.id)
} else if (!_client.user) {
- Logger.info('[SOCKET] Unauth socket disconnected ' + socket.id)
+ Logger.info('[Server] Unauth socket disconnected ' + socket.id)
delete this.clients[socket.id]
} else {
- socket.broadcast.emit('user_offline', _client.user.toJSONForPublic(this.streamManager.streams))
+ Logger.debug('[Server] User Offline ' + _client.user.username)
+ this.io.emit('user_offline', _client.user.toJSONForPublic(this.streamManager.streams))
const disconnectTime = Date.now() - _client.connected_at
- Logger.info(`[SOCKET] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
+ Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
delete this.clients[socket.id]
}
})
@@ -426,6 +427,27 @@ class Server {
}
logout(req, res) {
+ var { socketId } = req.body
+ Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${socketId}`)
+
+ // Strip user and client from client and client socket
+ if (socketId && this.clients[socketId]) {
+ var client = this.clients[socketId]
+ var clientSocket = client.socket
+ Logger.debug(`[Server] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`)
+
+ if (client.user) {
+ Logger.debug('[Server] User Offline ' + client.user.username)
+ this.io.emit('user_offline', client.user.toJSONForPublic(null))
+ }
+
+ delete this.clients[socketId].user
+ delete this.clients[socketId].stream
+ if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
+ } else if (socketId) {
+ Logger.warn(`[Server] No client for socket ${socketId}`)
+ }
+
res.sendStatus(200)
}
@@ -444,6 +466,11 @@ class Server {
return socket.emit('invalid_token')
}
var client = this.clients[socket.id]
+
+ if (client.user !== undefined) {
+ Logger.debug(`[Server] Authenticating socket client already has user`, client.user)
+ }
+
client.user = user
if (!client.user.toJSONForBrowser) {
@@ -462,7 +489,8 @@ class Server {
}
}
- socket.broadcast.emit('user_online', client.user.toJSONForPublic(this.streamManager.streams))
+ Logger.debug(`[Server] User Online ${client.user.username}`)
+ this.io.emit('user_online', client.user.toJSONForPublic(this.streamManager.streams))
user.lastSeen = Date.now()
await this.db.updateEntity('user', user)
@@ -477,6 +505,9 @@ class Server {
librariesScanning: this.scanner.librariesScanning,
backups: (this.backupManager.backups || []).map(b => b.toJSON())
}
+ if (user.type === 'root') {
+ initialPayload.usersOnline = this.usersOnline
+ }
client.socket.emit('init', initialPayload)
// Setup log listener for root user
diff --git a/server/StreamManager.js b/server/StreamManager.js
index fef8bac5..ba950581 100644
--- a/server/StreamManager.js
+++ b/server/StreamManager.js
@@ -5,9 +5,11 @@ const fs = require('fs-extra')
const Path = require('path')
class StreamManager {
- constructor(db, MetadataPath) {
+ constructor(db, MetadataPath, emitter) {
this.db = db
+ this.emitter = emitter
+
this.MetadataPath = MetadataPath
this.streams = []
this.StreamsPath = Path.join(this.MetadataPath, 'streams')
@@ -112,7 +114,7 @@ class StreamManager {
var stream = await this.openStream(client, audiobook)
this.db.updateUserStream(client.user.id, stream.id)
- socket.broadcast.emit('user_stream_update', client.user.toJSONForPublic(this.streams))
+ this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
}
async closeStreamRequest(socket) {
@@ -129,24 +131,7 @@ class StreamManager {
client.stream = null
this.db.updateUserStream(client.user.id, null)
- socket.broadcast.emit('user_stream_update', client.user.toJSONForPublic(this.streams))
- }
-
- async openTestStream(StreamsPath, audiobookId) {
- Logger.info('Open Stream Test Request', audiobookId)
- // var audiobook = this.audiobooks.find(ab => ab.id === audiobookId)
- // var stream = new StreamTest(StreamsPath, audiobook)
-
- // stream.on('closed', () => {
- // console.log('Stream closed')
- // })
-
- // var playlistUri = await stream.generatePlaylist()
- // stream.start()
-
- // Logger.info('Stream Playlist', playlistUri)
- // Logger.info('Test Stream Opened for audiobook', audiobook.title, 'with streamId', stream.id)
- // return playlistUri
+ this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
}
streamUpdate(socket, { currentTime, streamId }) {
diff --git a/server/objects/AudiobookProgress.js b/server/objects/AudiobookProgress.js
index c7923a26..8202a884 100644
--- a/server/objects/AudiobookProgress.js
+++ b/server/objects/AudiobookProgress.js
@@ -1,7 +1,8 @@
class AudiobookProgress {
constructor(progress) {
this.audiobookId = null
- this.audiobookTitle = null
+
+ this.id = null
this.totalDuration = null // seconds
this.progress = null // 0 to 1
this.currentTime = null // seconds
@@ -18,7 +19,6 @@ class AudiobookProgress {
toJSON() {
return {
audiobookId: this.audiobookId,
- audiobookTitle: this.audiobookTitle,
totalDuration: this.totalDuration,
progress: this.progress,
currentTime: this.currentTime,
@@ -31,7 +31,6 @@ class AudiobookProgress {
construct(progress) {
this.audiobookId = progress.audiobookId
- this.audiobookTitle = progress.audiobookTitle || null
this.totalDuration = progress.totalDuration
this.progress = progress.progress
this.currentTime = progress.currentTime
@@ -43,7 +42,6 @@ class AudiobookProgress {
updateFromStream(stream) {
this.audiobookId = stream.audiobookId
- this.audiobookTitle = stream.audiobookTitle
this.totalDuration = stream.totalDuration
this.progress = stream.clientProgress
this.currentTime = stream.clientCurrentTime
@@ -89,6 +87,9 @@ class AudiobookProgress {
if (!this.startedAt) {
this.startedAt = Date.now()
}
+ if (hasUpdates) {
+ this.lastUpdate = Date.now()
+ }
return hasUpdates
}
}
diff --git a/server/objects/Book.js b/server/objects/Book.js
index b144f9e7..a1acddf8 100644
--- a/server/objects/Book.js
+++ b/server/objects/Book.js
@@ -235,6 +235,17 @@ class Book {
}
}
+ parseGenresTag(genreTag) {
+ if (!genreTag || !genreTag.length) return []
+ var separators = ['/', '//', ';']
+ for (let i = 0; i < separators.length; i++) {
+ if (genreTag.includes(separators[i])) {
+ return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g)
+ }
+ }
+ return [genreTag]
+ }
+
setDetailsFromFileMetadata(audioFileMetadata) {
const MetadataMapArray = [
{
@@ -260,14 +271,24 @@ class Book {
{
tag: 'tagArtist',
key: 'author'
+ },
+ {
+ tag: 'tagGenre',
+ key: 'genres'
}
]
var updatePayload = {}
MetadataMapArray.forEach((mapping) => {
if (!this[mapping.key] && audioFileMetadata[mapping.tag]) {
- updatePayload[mapping.key] = audioFileMetadata[mapping.tag]
- Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key]}`)
+ // Genres can contain multiple
+ if (mapping.key === 'genres') {
+ updatePayload[mapping.key] = this.parseGenresTag(audioFileMetadata[mapping.tag])
+ Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key].join(',')}`)
+ } else {
+ updatePayload[mapping.key] = audioFileMetadata[mapping.tag]
+ Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key]}`)
+ }
}
})
diff --git a/server/objects/User.js b/server/objects/User.js
index 7cb81442..986b6621 100644
--- a/server/objects/User.js
+++ b/server/objects/User.js
@@ -16,6 +16,7 @@ class User {
this.settings = {}
this.permissions = {}
+ this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
if (user) {
this.construct(user)
@@ -37,6 +38,9 @@ class User {
get canUpload() {
return !!this.permissions.upload && this.isActive
}
+ get canAccessAllLibraries() {
+ return !!this.permissions.accessAllLibraries && this.isActive
+ }
get hasPw() {
return !!this.pash && !!this.pash.length
}
@@ -59,7 +63,8 @@ class User {
download: true,
update: true,
delete: this.type === 'root',
- upload: this.type === 'root' || this.type === 'admin'
+ upload: this.type === 'root' || this.type === 'admin',
+ accessAllLibraries: true
}
}
@@ -88,7 +93,8 @@ class User {
lastSeen: this.lastSeen,
createdAt: this.createdAt,
settings: this.settings,
- permissions: this.permissions
+ permissions: this.permissions,
+ librariesAccessible: [...this.librariesAccessible]
}
}
@@ -105,10 +111,12 @@ class User {
lastSeen: this.lastSeen,
createdAt: this.createdAt,
settings: this.settings,
- permissions: this.permissions
+ permissions: this.permissions,
+ librariesAccessible: [...this.librariesAccessible]
}
}
+ // Data broadcasted
toJSONForPublic(streams) {
var stream = this.stream && streams ? streams.find(s => s.id === this.stream) : null
return {
@@ -144,6 +152,11 @@ class User {
this.permissions = user.permissions || this.getDefaultUserPermissions()
// Upload permission added v1.1.13, make sure root user has upload permissions
if (this.type === 'root' && !this.permissions.upload) this.permissions.upload = true
+
+ // Library restriction permissions added v1.4.14, defaults to all libraries
+ if (this.permissions.accessAllLibraries === undefined) this.permissions.accessAllLibraries = true
+
+ this.librariesAccessible = (user.librariesAccessible || []).map(l => l)
}
update(payload) {
@@ -169,6 +182,18 @@ class User {
}
}
}
+ // Update accessible libraries
+ if (payload.librariesAccessible !== undefined) {
+ if (payload.librariesAccessible.length) {
+ if (payload.librariesAccessible.join(',') !== this.librariesAccessible.join(',')) {
+ hasUpdates = true
+ this.librariesAccessible = [...payload.librariesAccessible]
+ }
+ } else if (this.librariesAccessible.length > 0) {
+ hasUpdates = true
+ this.librariesAccessible = []
+ }
+ }
return hasUpdates
}
@@ -180,13 +205,13 @@ class User {
this.audiobooks[stream.audiobookId].updateFromStream(stream)
}
- updateAudiobookProgress(audiobookId, updatePayload) {
+ updateAudiobookProgress(audiobook, updatePayload) {
if (!this.audiobooks) this.audiobooks = {}
- if (!this.audiobooks[audiobookId]) {
- this.audiobooks[audiobookId] = new AudiobookProgress()
- this.audiobooks[audiobookId].audiobookId = audiobookId
+ if (!this.audiobooks[audiobook.id]) {
+ this.audiobooks[audiobook.id] = new AudiobookProgress()
+ this.audiobooks[audiobook.id].audiobookId = audiobook.id
}
- return this.audiobooks[audiobookId].update(updatePayload)
+ return this.audiobooks[audiobook.id].update(updatePayload)
}
// Returns Boolean If update was made
@@ -215,11 +240,11 @@ class User {
return madeUpdates
}
- resetAudiobookProgress(audiobookId) {
- if (!this.audiobooks || !this.audiobooks[audiobookId]) {
+ resetAudiobookProgress(audiobook) {
+ if (!this.audiobooks || !this.audiobooks[audiobook.id]) {
return false
}
- return this.updateAudiobookProgress(audiobookId, {
+ return this.updateAudiobookProgress(audiobook, {
progress: 0,
currentTime: 0,
isRead: false,
@@ -236,5 +261,11 @@ class User {
delete this.audiobooks[audiobookId]
return true
}
+
+ checkCanAccessLibrary(libraryId) {
+ if (this.permissions.accessAllLibraries) return true
+ if (!this.librariesAccessible) return false
+ return this.librariesAccessible.includes(libraryId)
+ }
}
module.exports = User
\ No newline at end of file
diff --git a/server/utils/prober.js b/server/utils/prober.js
index 03bbdb55..7db7eedb 100644
--- a/server/utils/prober.js
+++ b/server/utils/prober.js
@@ -169,7 +169,6 @@ function parseTags(format, verbose) {
file_tag_seriespart: tryGrabTag(format, 'series-part'),
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
- file_tag_genre: tryGrabTags(format, 'genre', 'genre')
}
for (const key in tags) {
if (!tags[key]) {