2021-08-18 00:01:11 +02:00
|
|
|
const Path = require('path')
|
|
|
|
const express = require('express')
|
|
|
|
const http = require('http')
|
|
|
|
const SocketIO = require('socket.io')
|
|
|
|
const fs = require('fs-extra')
|
2021-09-14 03:18:58 +02:00
|
|
|
const fileUpload = require('express-fileupload')
|
2021-09-29 17:16:38 +02:00
|
|
|
const rateLimit = require('express-rate-limit')
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
const { version } = require('../package.json')
|
|
|
|
|
|
|
|
// Utils
|
2021-10-01 01:52:32 +02:00
|
|
|
const { ScanResult } = require('./utils/constants')
|
2021-10-07 04:08:52 +02:00
|
|
|
const filePerms = require('./utils/filePerms')
|
2021-11-25 03:15:50 +01:00
|
|
|
const { secondsToTimestamp } = require('./utils/index')
|
2021-10-05 05:11:42 +02:00
|
|
|
const Logger = require('./Logger')
|
2021-10-01 01:52:32 +02:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
// Classes
|
2021-08-18 00:01:11 +02:00
|
|
|
const Auth = require('./Auth')
|
|
|
|
const Watcher = require('./Watcher')
|
2021-12-25 01:06:17 +01:00
|
|
|
const Scanner = require('./scanner/Scanner')
|
2021-08-18 00:01:11 +02:00
|
|
|
const Db = require('./Db')
|
2021-10-09 00:30:20 +02:00
|
|
|
const BackupManager = require('./BackupManager')
|
2021-10-31 23:55:28 +01:00
|
|
|
const LogManager = require('./LogManager')
|
2021-08-18 00:01:11 +02:00
|
|
|
const ApiController = require('./ApiController')
|
|
|
|
const HlsController = require('./HlsController')
|
|
|
|
const StreamManager = require('./StreamManager')
|
2021-08-23 21:08:54 +02:00
|
|
|
const RssFeeds = require('./RssFeeds')
|
2021-09-04 21:17:26 +02:00
|
|
|
const DownloadManager = require('./DownloadManager')
|
2021-10-02 01:42:48 +02:00
|
|
|
const CoverController = require('./CoverController')
|
2021-12-13 00:15:37 +01:00
|
|
|
const CacheManager = require('./CacheManager')
|
2021-10-05 05:11:42 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
class Server {
|
2021-10-07 04:08:52 +02:00
|
|
|
constructor(PORT, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
|
2021-08-18 00:01:11 +02:00
|
|
|
this.Port = PORT
|
2021-10-07 04:08:52 +02:00
|
|
|
this.Uid = isNaN(UID) ? 0 : Number(UID)
|
|
|
|
this.Gid = isNaN(GID) ? 0 : Number(GID)
|
2021-08-18 00:01:11 +02:00
|
|
|
this.Host = '0.0.0.0'
|
2021-08-26 00:36:54 +02:00
|
|
|
this.ConfigPath = Path.normalize(CONFIG_PATH)
|
|
|
|
this.AudiobookPath = Path.normalize(AUDIOBOOK_PATH)
|
|
|
|
this.MetadataPath = Path.normalize(METADATA_PATH)
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-10-07 04:08:52 +02:00
|
|
|
fs.ensureDirSync(CONFIG_PATH, 0o774)
|
|
|
|
fs.ensureDirSync(METADATA_PATH, 0o774)
|
|
|
|
fs.ensureDirSync(AUDIOBOOK_PATH, 0o774)
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
this.db = new Db(this.ConfigPath, this.AudiobookPath)
|
2021-08-18 00:01:11 +02:00
|
|
|
this.auth = new Auth(this.db)
|
2021-10-09 00:30:20 +02:00
|
|
|
this.backupManager = new BackupManager(this.MetadataPath, this.Uid, this.Gid, this.db)
|
2021-10-31 23:55:28 +01:00
|
|
|
this.logManager = new LogManager(this.MetadataPath, this.db)
|
2021-12-13 00:15:37 +01:00
|
|
|
this.cacheManager = new CacheManager(this.MetadataPath)
|
2021-08-18 00:01:11 +02:00
|
|
|
this.watcher = new Watcher(this.AudiobookPath)
|
2021-12-13 00:15:37 +01:00
|
|
|
this.coverController = new CoverController(this.db, this.cacheManager, this.MetadataPath, this.AudiobookPath)
|
2021-10-02 03:29:00 +02:00
|
|
|
this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this))
|
2021-11-25 03:15:50 +01:00
|
|
|
|
2021-10-26 04:14:54 +02:00
|
|
|
this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this), this.clientEmitter.bind(this))
|
2021-08-23 21:08:54 +02:00
|
|
|
this.rssFeeds = new RssFeeds(this.Port, this.db)
|
2021-09-15 03:45:00 +02:00
|
|
|
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this))
|
2021-12-25 01:06:17 +01:00
|
|
|
this.apiController = new ApiController(this.MetadataPath, this.db, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
|
|
|
this.hlsController = new HlsController(this.db, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
|
2021-10-02 01:42:48 +02:00
|
|
|
|
2021-10-31 23:55:28 +01:00
|
|
|
Logger.logManager = this.logManager
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
this.expressApp = null
|
2021-08-18 00:01:11 +02:00
|
|
|
this.server = null
|
|
|
|
this.io = null
|
|
|
|
|
|
|
|
this.clients = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
get audiobooks() {
|
|
|
|
return this.db.audiobooks
|
|
|
|
}
|
2021-10-05 05:11:42 +02:00
|
|
|
get libraries() {
|
|
|
|
return this.db.libraries
|
|
|
|
}
|
2021-09-05 02:58:39 +02:00
|
|
|
get serverSettings() {
|
|
|
|
return this.db.serverSettings
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2021-10-23 03:08:02 +02:00
|
|
|
get usersOnline() {
|
|
|
|
return Object.values(this.clients).filter(c => c.user).map(client => {
|
|
|
|
return client.user.toJSONForPublic(this.streamManager.streams)
|
|
|
|
})
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-09-06 01:20:29 +02:00
|
|
|
getClientsForUser(userId) {
|
|
|
|
return Object.values(this.clients).filter(c => c.user && c.user.id === userId)
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
emitter(ev, data) {
|
2021-08-24 14:15:56 +02:00
|
|
|
// Logger.debug('EMITTER', ev)
|
2021-08-18 00:01:11 +02:00
|
|
|
this.io.emit(ev, data)
|
|
|
|
}
|
|
|
|
|
2021-09-06 01:20:29 +02:00
|
|
|
clientEmitter(userId, ev, data) {
|
|
|
|
var clients = this.getClientsForUser(userId)
|
|
|
|
if (!clients.length) {
|
|
|
|
return Logger.error(`[Server] clientEmitter - no clients found for user ${userId}`)
|
|
|
|
}
|
|
|
|
clients.forEach((client) => {
|
|
|
|
if (client.socket) {
|
|
|
|
client.socket.emit(ev, data)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 03:07:42 +02:00
|
|
|
authMiddleware(req, res, next) {
|
|
|
|
this.auth.authMiddleware(req, res, next)
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
async init() {
|
2021-10-05 05:11:42 +02:00
|
|
|
Logger.info('[Server] Init v' + version)
|
2021-09-22 03:57:33 +02:00
|
|
|
await this.streamManager.ensureStreamsDir()
|
2021-08-18 00:01:11 +02:00
|
|
|
await this.streamManager.removeOrphanStreams()
|
2021-09-04 21:17:26 +02:00
|
|
|
await this.downloadManager.removeOrphanDownloads()
|
2021-09-23 03:40:35 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
await this.db.init()
|
|
|
|
this.auth.init()
|
|
|
|
|
2021-11-04 13:59:28 +01:00
|
|
|
await this.checkUserAudiobookData()
|
2021-10-02 01:42:48 +02:00
|
|
|
await this.purgeMetadata()
|
2021-10-09 00:30:20 +02:00
|
|
|
await this.backupManager.init()
|
2021-10-31 23:55:28 +01:00
|
|
|
await this.logManager.init()
|
2021-10-02 01:42:48 +02:00
|
|
|
|
2021-11-16 03:09:42 +01:00
|
|
|
// Only fix duplicate ids once on upgrade
|
|
|
|
if (this.db.previousVersion === '1.0.0') {
|
|
|
|
Logger.info(`[Server] Running scan for duplicate book IDs`)
|
|
|
|
await this.scanner.fixDuplicateIds()
|
|
|
|
}
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
this.watcher.initWatcher(this.libraries)
|
2021-09-11 02:55:02 +02:00
|
|
|
this.watcher.on('files', this.filesChanged.bind(this))
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async start() {
|
|
|
|
Logger.info('=== Starting Server ===')
|
|
|
|
await this.init()
|
|
|
|
|
|
|
|
const app = express()
|
2021-10-05 05:11:42 +02:00
|
|
|
this.expressApp = app
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
this.server = http.createServer(app)
|
|
|
|
|
|
|
|
app.use(this.auth.cors)
|
2021-09-14 03:18:58 +02:00
|
|
|
app.use(fileUpload())
|
2021-10-05 05:11:42 +02:00
|
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
app.use(express.json())
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
// Static path to generated nuxt
|
2021-08-24 02:37:40 +02:00
|
|
|
const distPath = Path.join(global.appRoot, '/client/dist')
|
2021-10-05 05:11:42 +02:00
|
|
|
app.use(express.static(distPath))
|
|
|
|
|
|
|
|
// Old static path for covers
|
|
|
|
app.use('/local', this.authMiddleware.bind(this), express.static(this.AudiobookPath))
|
2021-08-21 23:23:35 +02:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
// Metadata folder static path
|
2021-09-22 03:57:33 +02:00
|
|
|
app.use('/metadata', this.authMiddleware.bind(this), express.static(this.MetadataPath))
|
|
|
|
|
2021-10-06 14:23:32 +02:00
|
|
|
// Downloads folder static path
|
|
|
|
app.use('/downloads', this.authMiddleware.bind(this), express.static(this.downloadManager.downloadDirPath))
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
// Static folder
|
2021-08-23 21:08:54 +02:00
|
|
|
app.use(express.static(Path.join(global.appRoot, 'static')))
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
// Static file routes
|
|
|
|
app.get('/lib/:library/:folder/*', this.authMiddleware.bind(this), (req, res) => {
|
|
|
|
var library = this.libraries.find(lib => lib.id === req.params.library)
|
|
|
|
if (!library) return res.sendStatus(404)
|
|
|
|
var folder = library.folders.find(fol => fol.id === req.params.folder)
|
|
|
|
if (!folder) return res.status(404).send('Folder not found')
|
|
|
|
|
2021-10-06 20:00:12 +02:00
|
|
|
var remainingPath = req.params['0']
|
2021-10-05 05:11:42 +02:00
|
|
|
var fullPath = Path.join(folder.fullPath, remainingPath)
|
|
|
|
res.sendFile(fullPath)
|
|
|
|
})
|
|
|
|
|
|
|
|
// Book static file routes
|
|
|
|
app.get('/s/book/:id/*', this.authMiddleware.bind(this), (req, res) => {
|
|
|
|
var audiobook = this.audiobooks.find(ab => ab.id === req.params.id)
|
|
|
|
if (!audiobook) return res.status(404).send('Book not found with id ' + req.params.id)
|
|
|
|
|
2021-10-06 20:00:12 +02:00
|
|
|
var remainingPath = req.params['0']
|
2021-10-05 05:11:42 +02:00
|
|
|
var fullPath = Path.join(audiobook.fullPath, remainingPath)
|
|
|
|
res.sendFile(fullPath)
|
|
|
|
})
|
|
|
|
|
2021-10-09 00:30:20 +02:00
|
|
|
// EBook static file routes
|
|
|
|
app.get('/ebook/:library/:folder/*', (req, res) => {
|
|
|
|
var library = this.libraries.find(lib => lib.id === req.params.library)
|
|
|
|
if (!library) return res.sendStatus(404)
|
|
|
|
var folder = library.folders.find(fol => fol.id === req.params.folder)
|
|
|
|
if (!folder) return res.status(404).send('Folder not found')
|
|
|
|
|
|
|
|
var remainingPath = req.params['0']
|
|
|
|
var fullPath = Path.join(folder.fullPath, remainingPath)
|
|
|
|
res.sendFile(fullPath)
|
|
|
|
})
|
|
|
|
|
2021-11-13 02:43:16 +01:00
|
|
|
// Client dynamic routes
|
2021-08-24 02:37:40 +02:00
|
|
|
app.get('/audiobook/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
2021-10-05 05:11:42 +02:00
|
|
|
app.get('/audiobook/:id/edit', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
|
|
|
app.get('/library/:library', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
2021-12-03 02:40:42 +01:00
|
|
|
app.get('/library/:library/search', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
2021-10-05 05:11:42 +02:00
|
|
|
app.get('/library/:library/bookshelf/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
2021-12-03 02:40:42 +01:00
|
|
|
app.get('/library/:library/authors', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
|
|
|
app.get('/library/:library/series/:id?', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
2021-11-13 02:43:16 +01:00
|
|
|
app.get('/config/users/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
|
|
|
app.get('/collection/:id', (req, res) => res.sendFile(Path.join(distPath, 'index.html')))
|
2021-08-24 02:37:40 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
app.use('/api', this.authMiddleware.bind(this), this.apiController.router)
|
|
|
|
app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router)
|
2021-09-29 17:16:38 +02:00
|
|
|
|
|
|
|
// Incomplete work in progress
|
|
|
|
// app.use('/feeds', this.rssFeeds.router)
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-09-18 19:45:34 +02:00
|
|
|
app.post('/upload', this.authMiddleware.bind(this), this.handleUpload.bind(this))
|
2021-09-14 03:18:58 +02:00
|
|
|
|
2021-09-29 17:16:38 +02:00
|
|
|
var loginRateLimiter = this.getLoginRateLimiter()
|
|
|
|
app.post('/login', loginRateLimiter, (req, res) => this.auth.login(req, res))
|
|
|
|
|
2021-10-23 03:08:02 +02:00
|
|
|
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
2021-09-29 17:16:38 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
app.get('/ping', (req, res) => {
|
|
|
|
Logger.info('Recieved ping')
|
|
|
|
res.json({ success: true })
|
|
|
|
})
|
|
|
|
|
2021-09-01 20:47:18 +02:00
|
|
|
// Used in development to set-up streams without authentication
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
|
|
app.use('/test-hls', this.hlsController.router)
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.server.listen(this.Port, this.Host, () => {
|
|
|
|
Logger.info(`Running on http://${this.Host}:${this.Port}`)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.io = new SocketIO.Server(this.server, {
|
|
|
|
cors: {
|
|
|
|
origin: '*',
|
|
|
|
methods: ["GET", "POST"]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
this.io.on('connection', (socket) => {
|
|
|
|
this.clients[socket.id] = {
|
|
|
|
id: socket.id,
|
|
|
|
socket,
|
|
|
|
connected_at: Date.now()
|
|
|
|
}
|
|
|
|
socket.sheepClient = this.clients[socket.id]
|
|
|
|
|
|
|
|
Logger.info('[SOCKET] Socket Connected', socket.id)
|
|
|
|
|
|
|
|
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
2021-09-12 23:10:12 +02:00
|
|
|
|
|
|
|
// Scanning
|
2021-08-18 00:01:11 +02:00
|
|
|
socket.on('scan', this.scan.bind(this))
|
2021-08-25 03:24:40 +02:00
|
|
|
socket.on('cancel_scan', this.cancelScan.bind(this))
|
2021-10-01 01:52:32 +02:00
|
|
|
socket.on('scan_audiobook', (audiobookId) => this.scanAudiobook(socket, audiobookId))
|
2021-09-29 17:16:38 +02:00
|
|
|
socket.on('save_metadata', (audiobookId) => this.saveMetadata(socket, audiobookId))
|
2021-09-12 23:10:12 +02:00
|
|
|
|
|
|
|
// Streaming
|
2021-08-18 00:01:11 +02:00
|
|
|
socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId))
|
|
|
|
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
|
|
|
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
2021-11-13 02:43:16 +01:00
|
|
|
socket.on('stream_sync', (syncData) => this.streamManager.streamSync(socket, syncData))
|
2021-09-12 23:10:12 +02:00
|
|
|
|
2021-10-24 22:53:51 +02:00
|
|
|
socket.on('progress_update', (payload) => this.audiobookProgressUpdate(socket, payload))
|
2021-09-12 23:10:12 +02:00
|
|
|
|
|
|
|
// Downloading
|
2021-09-04 21:17:26 +02:00
|
|
|
socket.on('download', (payload) => this.downloadManager.downloadSocketRequest(socket, payload))
|
2021-09-12 23:10:12 +02:00
|
|
|
socket.on('remove_download', (downloadId) => this.downloadManager.removeSocketRequest(socket, downloadId))
|
|
|
|
|
2021-10-09 00:30:20 +02:00
|
|
|
// Logs
|
2021-10-01 01:52:32 +02:00
|
|
|
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
2021-11-01 01:10:45 +01:00
|
|
|
socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket))
|
2021-10-01 01:52:32 +02:00
|
|
|
|
2021-10-09 00:30:20 +02:00
|
|
|
// Backups
|
|
|
|
socket.on('create_backup', () => this.backupManager.requestCreateBackup(socket))
|
|
|
|
socket.on('apply_backup', (id) => this.backupManager.requestApplyBackup(socket, id))
|
|
|
|
|
2021-10-25 01:25:44 +02:00
|
|
|
// Bookmarks
|
|
|
|
socket.on('create_bookmark', (payload) => this.createBookmark(socket, payload))
|
2021-10-27 03:09:04 +02:00
|
|
|
socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload))
|
|
|
|
socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload))
|
2021-10-25 01:25:44 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
socket.on('test', () => {
|
|
|
|
socket.emit('test_received', socket.id)
|
|
|
|
})
|
|
|
|
|
|
|
|
socket.on('disconnect', () => {
|
2021-10-01 01:52:32 +02:00
|
|
|
Logger.removeSocketListener(socket.id)
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
var _client = this.clients[socket.id]
|
|
|
|
if (!_client) {
|
2021-10-23 03:08:02 +02:00
|
|
|
Logger.warn('[Server] Socket disconnect, no client ' + socket.id)
|
2021-08-18 00:01:11 +02:00
|
|
|
} else if (!_client.user) {
|
2021-10-23 03:08:02 +02:00
|
|
|
Logger.info('[Server] Unauth socket disconnected ' + socket.id)
|
2021-08-18 00:01:11 +02:00
|
|
|
delete this.clients[socket.id]
|
|
|
|
} else {
|
2021-10-23 03:08:02 +02:00
|
|
|
Logger.debug('[Server] User Offline ' + _client.user.username)
|
|
|
|
this.io.emit('user_offline', _client.user.toJSONForPublic(this.streamManager.streams))
|
2021-10-13 03:07:42 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
const disconnectTime = Date.now() - _client.connected_at
|
2021-10-23 03:08:02 +02:00
|
|
|
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
|
2021-08-18 00:01:11 +02:00
|
|
|
delete this.clients[socket.id]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
async filesChanged(fileUpdates) {
|
|
|
|
Logger.info('[Server]', fileUpdates.length, 'Files Changed')
|
2021-12-25 01:06:17 +01:00
|
|
|
await this.scanner.scanFilesChanged(fileUpdates)
|
2021-10-05 05:11:42 +02:00
|
|
|
}
|
|
|
|
|
2021-12-05 00:57:47 +01:00
|
|
|
async scan(libraryId, options = {}) {
|
2021-10-05 05:11:42 +02:00
|
|
|
Logger.info('[Server] Starting Scan')
|
2021-12-25 01:06:17 +01:00
|
|
|
await this.scanner.scan(libraryId, options)
|
2021-11-26 01:39:02 +01:00
|
|
|
// await this.scanner.scan(libraryId)
|
2021-10-05 05:11:42 +02:00
|
|
|
Logger.info('[Server] Scan complete')
|
|
|
|
}
|
|
|
|
|
|
|
|
async scanAudiobook(socket, audiobookId) {
|
2021-12-25 01:06:17 +01:00
|
|
|
var result = await this.scanner.scanAudiobookById(audiobookId)
|
2021-10-05 05:11:42 +02:00
|
|
|
var scanResultName = ''
|
|
|
|
for (const key in ScanResult) {
|
|
|
|
if (ScanResult[key] === result) {
|
|
|
|
scanResultName = key
|
|
|
|
}
|
|
|
|
}
|
|
|
|
socket.emit('audiobook_scan_complete', scanResultName)
|
|
|
|
}
|
|
|
|
|
|
|
|
cancelScan(id) {
|
2021-10-06 04:10:49 +02:00
|
|
|
Logger.debug('[Server] Cancel scan', id)
|
2021-12-25 01:06:17 +01:00
|
|
|
this.scanner.setCancelLibraryScan(id)
|
2021-10-05 05:11:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generates an NFO metadata file, if no audiobookId is passed then all audiobooks are done
|
|
|
|
async saveMetadata(socket, audiobookId = null) {
|
|
|
|
Logger.info('[Server] Starting save metadata files')
|
|
|
|
var response = await this.scanner.saveMetadata(audiobookId)
|
|
|
|
Logger.info(`[Server] Finished saving metadata files Successful: ${response.success}, Failed: ${response.failed}`)
|
|
|
|
socket.emit('save_metadata_complete', response)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove unused /metadata/books/{id} folders
|
|
|
|
async purgeMetadata() {
|
|
|
|
var booksMetadata = Path.join(this.MetadataPath, 'books')
|
|
|
|
var booksMetadataExists = await fs.pathExists(booksMetadata)
|
|
|
|
if (!booksMetadataExists) return
|
|
|
|
var foldersInBooksMetadata = await fs.readdir(booksMetadata)
|
|
|
|
|
|
|
|
var purged = 0
|
|
|
|
await Promise.all(foldersInBooksMetadata.map(async foldername => {
|
|
|
|
var hasMatchingAudiobook = this.audiobooks.find(ab => ab.id === foldername)
|
|
|
|
if (!hasMatchingAudiobook) {
|
|
|
|
var folderPath = Path.join(booksMetadata, foldername)
|
|
|
|
Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
|
|
|
|
|
|
|
|
await fs.remove(folderPath).then(() => {
|
|
|
|
purged++
|
|
|
|
}).catch((err) => {
|
|
|
|
Logger.error(`[Server] Failed to delete folder path ${folderPath}`, err)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
if (purged > 0) {
|
|
|
|
Logger.info(`[Server] Purged ${purged} unused audiobook metadata`)
|
|
|
|
}
|
|
|
|
return purged
|
|
|
|
}
|
|
|
|
|
2021-11-04 13:59:28 +01:00
|
|
|
// Check user audiobook data has matching audiobook
|
|
|
|
async checkUserAudiobookData() {
|
|
|
|
for (let i = 0; i < this.db.users.length; i++) {
|
|
|
|
var _user = this.db.users[i]
|
|
|
|
if (_user.audiobooks) {
|
|
|
|
// Find user audiobook data that has no matching audiobook
|
|
|
|
var audiobookIdsToRemove = Object.keys(_user.audiobooks).filter(aid => {
|
|
|
|
return !this.db.audiobooks.find(ab => ab.id === aid)
|
|
|
|
})
|
|
|
|
if (audiobookIdsToRemove.length) {
|
|
|
|
Logger.debug(`[Server] Found ${audiobookIdsToRemove.length} audiobook data to remove from user ${_user.username}`)
|
|
|
|
for (let y = 0; y < audiobookIdsToRemove.length; y++) {
|
|
|
|
_user.deleteAudiobookData(audiobookIdsToRemove[y])
|
|
|
|
}
|
|
|
|
await this.db.updateEntity('user', _user)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
async handleUpload(req, res) {
|
|
|
|
if (!req.user.canUpload) {
|
|
|
|
Logger.warn('User attempted to upload without permission', req.user)
|
|
|
|
return res.sendStatus(403)
|
|
|
|
}
|
|
|
|
var files = Object.values(req.files)
|
|
|
|
var title = req.body.title
|
|
|
|
var author = req.body.author
|
|
|
|
var series = req.body.series
|
2021-10-06 04:10:49 +02:00
|
|
|
var libraryId = req.body.library
|
|
|
|
var folderId = req.body.folder
|
|
|
|
|
|
|
|
var library = this.db.libraries.find(lib => lib.id === libraryId)
|
|
|
|
if (!library) {
|
|
|
|
return res.status(500).error(`Library not found with id ${libraryId}`)
|
|
|
|
}
|
|
|
|
var folder = library.folders.find(fold => fold.id === folderId)
|
|
|
|
if (!folder) {
|
|
|
|
return res.status(500).error(`Folder not found with id ${folderId} in library ${library.name}`)
|
|
|
|
}
|
2021-10-05 05:11:42 +02:00
|
|
|
|
|
|
|
if (!files.length || !title || !author) {
|
2021-10-06 04:10:49 +02:00
|
|
|
return res.status(500).error(`Invalid post data`)
|
2021-10-05 05:11:42 +02:00
|
|
|
}
|
|
|
|
|
2021-10-07 04:08:52 +02:00
|
|
|
// For setting permissions recursively
|
|
|
|
var firstDirPath = Path.join(folder.fullPath, author)
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
var outputDirectory = ''
|
|
|
|
if (series && series.length && series !== 'null') {
|
2021-10-06 04:10:49 +02:00
|
|
|
outputDirectory = Path.join(folder.fullPath, author, series, title)
|
2021-10-05 05:11:42 +02:00
|
|
|
} else {
|
2021-10-06 04:10:49 +02:00
|
|
|
outputDirectory = Path.join(folder.fullPath, author, title)
|
2021-10-05 05:11:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var exists = await fs.pathExists(outputDirectory)
|
|
|
|
if (exists) {
|
|
|
|
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
2021-10-06 04:10:49 +02:00
|
|
|
return res.status(500).error(`Directory "${outputDirectory}" already exists`)
|
2021-10-05 05:11:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
await fs.ensureDir(outputDirectory)
|
2021-10-07 04:08:52 +02:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
|
|
|
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
|
|
var file = files[i]
|
|
|
|
|
|
|
|
var path = Path.join(outputDirectory, file.name)
|
2021-10-07 04:08:52 +02:00
|
|
|
await file.mv(path).then(() => {
|
|
|
|
return true
|
|
|
|
}).catch((error) => {
|
2021-10-05 05:11:42 +02:00
|
|
|
Logger.error('Failed to move file', path, error)
|
2021-10-07 04:08:52 +02:00
|
|
|
return false
|
2021-10-05 05:11:42 +02:00
|
|
|
})
|
|
|
|
}
|
2021-10-07 04:08:52 +02:00
|
|
|
|
|
|
|
Logger.info(`[Server] Setting owner/perms for first dir "${firstDirPath}"`)
|
|
|
|
await filePerms(firstDirPath, 0o774, this.Uid, this.Gid)
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
res.sendStatus(200)
|
|
|
|
}
|
|
|
|
|
|
|
|
// First time login rate limit is hit
|
|
|
|
loginLimitReached(req, res, options) {
|
|
|
|
Logger.error(`[Server] Login rate limit (${options.max}) was hit for ip ${req.ip}`)
|
|
|
|
options.message = 'Too many attempts. Login temporarily locked.'
|
|
|
|
}
|
|
|
|
|
|
|
|
getLoginRateLimiter() {
|
|
|
|
return rateLimit({
|
|
|
|
windowMs: this.db.serverSettings.rateLimitLoginWindow, // 5 minutes
|
|
|
|
max: this.db.serverSettings.rateLimitLoginRequests,
|
|
|
|
skipSuccessfulRequests: true,
|
|
|
|
onLimitReached: this.loginLimitReached
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
logout(req, res) {
|
2021-10-23 03:08:02 +02:00
|
|
|
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}`)
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
res.sendStatus(200)
|
|
|
|
}
|
|
|
|
|
2021-10-26 04:14:54 +02:00
|
|
|
async audiobookProgressUpdate(socket, progressPayload) {
|
2021-10-24 22:53:51 +02:00
|
|
|
var client = socket.sheepClient
|
2021-09-12 02:59:48 +02:00
|
|
|
if (!client || !client.user) {
|
|
|
|
Logger.error('[Server] audiobookProgressUpdate invalid socket client')
|
|
|
|
return
|
|
|
|
}
|
2021-10-29 00:19:09 +02:00
|
|
|
var audiobookProgress = client.user.updateAudiobookData(progressPayload.audiobookId, progressPayload)
|
2021-10-26 04:14:54 +02:00
|
|
|
if (audiobookProgress) {
|
|
|
|
await this.db.updateEntity('user', client.user)
|
|
|
|
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
2021-10-24 22:53:51 +02:00
|
|
|
id: progressPayload.audiobookId,
|
2021-10-26 04:14:54 +02:00
|
|
|
data: audiobookProgress || null
|
2021-10-24 22:53:51 +02:00
|
|
|
})
|
|
|
|
}
|
2021-09-12 02:59:48 +02:00
|
|
|
}
|
|
|
|
|
2021-10-25 01:25:44 +02:00
|
|
|
async createBookmark(socket, payload) {
|
|
|
|
var client = socket.sheepClient
|
|
|
|
if (!client || !client.user) {
|
|
|
|
Logger.error('[Server] createBookmark invalid socket client')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var userAudiobook = client.user.createBookmark(payload)
|
2021-10-27 03:09:04 +02:00
|
|
|
if (!userAudiobook || userAudiobook.error) {
|
|
|
|
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
|
|
|
socket.emit('show_error_toast', `Failed to create Bookmark: ${failMessage}`)
|
|
|
|
return
|
|
|
|
}
|
2021-10-26 04:14:54 +02:00
|
|
|
|
2021-10-27 03:09:04 +02:00
|
|
|
await this.db.updateEntity('user', client.user)
|
|
|
|
|
|
|
|
socket.emit('show_success_toast', `${secondsToTimestamp(payload.time)} Bookmarked`)
|
|
|
|
|
|
|
|
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
|
|
|
id: userAudiobook.audiobookId,
|
|
|
|
data: userAudiobook || null
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateBookmark(socket, payload) {
|
|
|
|
var client = socket.sheepClient
|
|
|
|
if (!client || !client.user) {
|
|
|
|
Logger.error('[Server] updateBookmark invalid socket client')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var userAudiobook = client.user.updateBookmark(payload)
|
|
|
|
if (!userAudiobook || userAudiobook.error) {
|
|
|
|
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
|
|
|
socket.emit('show_error_toast', `Failed to update Bookmark: ${failMessage}`)
|
|
|
|
return
|
2021-10-25 01:25:44 +02:00
|
|
|
}
|
2021-10-27 03:09:04 +02:00
|
|
|
|
|
|
|
await this.db.updateEntity('user', client.user)
|
|
|
|
|
|
|
|
socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Updated`)
|
|
|
|
|
|
|
|
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
|
|
|
id: userAudiobook.audiobookId,
|
|
|
|
data: userAudiobook || null
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async deleteBookmark(socket, payload) {
|
|
|
|
var client = socket.sheepClient
|
|
|
|
if (!client || !client.user) {
|
|
|
|
Logger.error('[Server] deleteBookmark invalid socket client')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var userAudiobook = client.user.deleteBookmark(payload)
|
|
|
|
if (!userAudiobook || userAudiobook.error) {
|
|
|
|
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
|
|
|
socket.emit('show_error_toast', `Failed to delete Bookmark: ${failMessage}`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.db.updateEntity('user', client.user)
|
|
|
|
|
|
|
|
socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Removed`)
|
|
|
|
|
|
|
|
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
|
|
|
id: userAudiobook.audiobookId,
|
|
|
|
data: userAudiobook || null
|
|
|
|
})
|
2021-10-25 01:25:44 +02:00
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
async authenticateSocket(socket, token) {
|
2021-11-13 02:43:16 +01:00
|
|
|
var user = await this.auth.authenticateUser(token)
|
2021-08-18 00:01:11 +02:00
|
|
|
if (!user) {
|
|
|
|
Logger.error('Cannot validate socket - invalid token')
|
|
|
|
return socket.emit('invalid_token')
|
|
|
|
}
|
|
|
|
var client = this.clients[socket.id]
|
2021-10-23 03:08:02 +02:00
|
|
|
|
|
|
|
if (client.user !== undefined) {
|
|
|
|
Logger.debug(`[Server] Authenticating socket client already has user`, client.user)
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
client.user = user
|
|
|
|
|
2021-08-18 00:43:29 +02:00
|
|
|
if (!client.user.toJSONForBrowser) {
|
|
|
|
Logger.error('Invalid user...', client.user)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
// Check if user has stream open
|
|
|
|
if (client.user.stream) {
|
|
|
|
Logger.info('User has stream open already', client.user.stream)
|
|
|
|
client.stream = this.streamManager.getStream(client.user.stream)
|
|
|
|
if (!client.stream) {
|
|
|
|
Logger.error('Invalid user stream id', client.user.stream)
|
|
|
|
this.streamManager.removeOrphanStreamFiles(client.user.stream)
|
|
|
|
await this.db.updateUserStream(client.user.id, null)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-23 03:08:02 +02:00
|
|
|
Logger.debug(`[Server] User Online ${client.user.username}`)
|
|
|
|
this.io.emit('user_online', client.user.toJSONForPublic(this.streamManager.streams))
|
2021-10-13 03:07:42 +02:00
|
|
|
|
|
|
|
user.lastSeen = Date.now()
|
|
|
|
await this.db.updateEntity('user', user)
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
const initialPayload = {
|
2021-09-05 02:58:39 +02:00
|
|
|
serverSettings: this.serverSettings.toJSON(),
|
2021-08-18 00:01:11 +02:00
|
|
|
audiobookPath: this.AudiobookPath,
|
|
|
|
metadataPath: this.MetadataPath,
|
|
|
|
configPath: this.ConfigPath,
|
|
|
|
user: client.user.toJSONForBrowser(),
|
2021-10-06 04:10:49 +02:00
|
|
|
stream: client.stream || null,
|
2021-12-25 01:06:17 +01:00
|
|
|
librariesScanning: this.scanner.librariesScanning,
|
2021-10-09 00:30:20 +02:00
|
|
|
backups: (this.backupManager.backups || []).map(b => b.toJSON())
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2021-10-23 03:08:02 +02:00
|
|
|
if (user.type === 'root') {
|
|
|
|
initialPayload.usersOnline = this.usersOnline
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
client.socket.emit('init', initialPayload)
|
2021-10-01 01:52:32 +02:00
|
|
|
|
|
|
|
// Setup log listener for root user
|
|
|
|
if (user.type === 'root') {
|
|
|
|
Logger.addSocketListener(socket, this.db.serverSettings.logLevel || 0)
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async stop() {
|
|
|
|
await this.watcher.close()
|
|
|
|
Logger.info('Watcher Closed')
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
this.server.close((err) => {
|
|
|
|
if (err) {
|
|
|
|
Logger.error('Failed to close server', err)
|
|
|
|
} else {
|
|
|
|
Logger.info('Server successfully closed')
|
|
|
|
}
|
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = Server
|