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')
|
2022-07-06 02:53:01 +02:00
|
|
|
const fs = require('./libs/fsExtra')
|
2022-07-07 02:10:25 +02:00
|
|
|
const fileUpload = require('./libs/expressFileupload')
|
2022-07-07 02:14:47 +02:00
|
|
|
const rateLimit = require('./libs/expressRateLimit')
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
const { version } = require('../package.json')
|
|
|
|
|
|
|
|
// Utils
|
2022-03-10 02:23:17 +01:00
|
|
|
const dbMigration = require('./utils/dbMigration')
|
2022-04-25 02:12:00 +02:00
|
|
|
const filePerms = require('./utils/filePerms')
|
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')
|
2022-03-20 22:41:06 +01:00
|
|
|
|
2022-03-18 01:10:47 +01:00
|
|
|
const ApiRouter = require('./routers/ApiRouter')
|
|
|
|
const HlsRouter = require('./routers/HlsRouter')
|
|
|
|
const StaticRouter = require('./routers/StaticRouter')
|
2022-03-20 22:41:06 +01:00
|
|
|
|
2022-09-21 01:08:41 +02:00
|
|
|
const NotificationManager = require('./managers/NotificationManager')
|
2022-03-20 22:41:06 +01:00
|
|
|
const CoverManager = require('./managers/CoverManager')
|
2022-04-22 01:52:28 +02:00
|
|
|
const AbMergeManager = require('./managers/AbMergeManager')
|
2022-03-20 22:41:06 +01:00
|
|
|
const CacheManager = require('./managers/CacheManager')
|
|
|
|
const LogManager = require('./managers/LogManager')
|
|
|
|
const BackupManager = require('./managers/BackupManager')
|
|
|
|
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
|
|
|
const PodcastManager = require('./managers/PodcastManager')
|
2022-05-02 01:33:46 +02:00
|
|
|
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
2022-05-02 21:41:59 +02:00
|
|
|
const RssFeedManager = require('./managers/RssFeedManager')
|
2022-08-18 01:44:21 +02:00
|
|
|
const CronManager = require('./managers/CronManager')
|
2022-10-02 21:16:17 +02:00
|
|
|
const TaskManager = require('./managers/TaskManager')
|
2021-10-05 05:11:42 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
class Server {
|
2022-10-01 23:07:30 +02:00
|
|
|
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
|
2021-08-18 00:01:11 +02:00
|
|
|
this.Port = PORT
|
2022-03-17 12:06:52 +01:00
|
|
|
this.Host = HOST
|
2022-05-21 18:21:03 +02:00
|
|
|
global.Source = SOURCE
|
2022-03-13 00:45:32 +01:00
|
|
|
global.Uid = isNaN(UID) ? 0 : Number(UID)
|
|
|
|
global.Gid = isNaN(GID) ? 0 : Number(GID)
|
2022-02-27 20:47:52 +01:00
|
|
|
global.ConfigPath = Path.normalize(CONFIG_PATH)
|
|
|
|
global.MetadataPath = Path.normalize(METADATA_PATH)
|
2022-10-01 23:07:30 +02:00
|
|
|
global.RouterBasePath = ROUTER_BASE_PATH
|
2022-05-15 00:23:22 +02:00
|
|
|
|
2022-02-27 20:47:52 +01:00
|
|
|
// Fix backslash if not on Windows
|
|
|
|
if (process.platform !== 'win32') {
|
|
|
|
global.ConfigPath = global.ConfigPath.replace(/\\/g, '/')
|
|
|
|
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2022-04-25 02:12:00 +02:00
|
|
|
if (!fs.pathExistsSync(global.ConfigPath)) {
|
|
|
|
fs.mkdirSync(global.ConfigPath)
|
|
|
|
filePerms.setDefaultDirSync(global.ConfigPath, false)
|
|
|
|
}
|
|
|
|
if (!fs.pathExistsSync(global.MetadataPath)) {
|
|
|
|
fs.mkdirSync(global.MetadataPath)
|
|
|
|
filePerms.setDefaultDirSync(global.MetadataPath, false)
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2022-02-27 20:47:52 +01:00
|
|
|
this.db = new Db()
|
2022-03-20 22:41:06 +01:00
|
|
|
this.watcher = new Watcher()
|
2021-08-18 00:01:11 +02:00
|
|
|
this.auth = new Auth(this.db)
|
2022-03-20 22:41:06 +01:00
|
|
|
|
|
|
|
// Managers
|
2022-10-02 21:16:17 +02:00
|
|
|
this.taskManager = new TaskManager(this.emitter.bind(this))
|
2022-09-24 23:15:16 +02:00
|
|
|
this.notificationManager = new NotificationManager(this.db, this.emitter.bind(this))
|
2022-03-18 19:44:29 +01:00
|
|
|
this.backupManager = new BackupManager(this.db, this.emitter.bind(this))
|
2022-02-27 20:47:52 +01:00
|
|
|
this.logManager = new LogManager(this.db)
|
|
|
|
this.cacheManager = new CacheManager()
|
2022-10-02 21:16:17 +02:00
|
|
|
this.abMergeManager = new AbMergeManager(this.db, this.taskManager, this.clientEmitter.bind(this))
|
2022-03-20 22:41:06 +01:00
|
|
|
this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
|
|
|
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
2022-09-21 01:08:41 +02:00
|
|
|
this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this), this.notificationManager)
|
2022-10-02 21:16:17 +02:00
|
|
|
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
2022-05-02 23:42:30 +02:00
|
|
|
this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
|
2022-03-20 22:41:06 +01:00
|
|
|
|
|
|
|
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
2022-08-20 01:41:58 +02:00
|
|
|
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
2022-03-18 01:10:47 +01:00
|
|
|
|
|
|
|
// Routers
|
2022-11-11 00:42:20 +01:00
|
|
|
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.cronManager, this.notificationManager, this.taskManager, this.getUsersOnline.bind(this), this.emitter.bind(this), this.clientEmitter.bind(this))
|
2022-03-18 01:10:47 +01:00
|
|
|
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
|
|
|
|
this.staticRouter = new StaticRouter(this.db)
|
2021-10-02 01:42:48 +02:00
|
|
|
|
2021-10-31 23:55:28 +01:00
|
|
|
Logger.logManager = this.logManager
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.server = null
|
|
|
|
this.io = null
|
|
|
|
|
|
|
|
this.clients = {}
|
|
|
|
}
|
|
|
|
|
2022-11-11 00:42:20 +01:00
|
|
|
getUsersOnline() {
|
2021-10-23 03:08:02 +02:00
|
|
|
return Object.values(this.clients).filter(c => c.user).map(client => {
|
2022-03-18 01:10:47 +01:00
|
|
|
return client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems)
|
2021-10-23 03:08:02 +02:00
|
|
|
})
|
|
|
|
}
|
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) {
|
2022-02-26 23:35:40 +01:00
|
|
|
return Logger.debug(`[Server] clientEmitter - no clients found for user ${userId}`)
|
2021-09-06 01:20:29 +02:00
|
|
|
}
|
|
|
|
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)
|
2022-04-16 19:37:10 +02:00
|
|
|
await this.playbackSessionManager.removeOrphanStreams()
|
2021-09-23 03:40:35 +02:00
|
|
|
|
2022-04-20 15:31:57 +02:00
|
|
|
var previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
|
|
|
|
if (previousVersion) {
|
|
|
|
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
|
|
|
|
}
|
|
|
|
if (previousVersion && previousVersion.localeCompare('2.0.0') < 0) { // Old version data model migration
|
|
|
|
Logger.debug(`[Server] Previous version was < 2.0.0 - migration required`)
|
2022-03-19 16:13:10 +01:00
|
|
|
await dbMigration.migrate(this.db)
|
2022-03-16 00:57:15 +01:00
|
|
|
} else {
|
|
|
|
await this.db.init()
|
2022-03-10 02:23:17 +01:00
|
|
|
}
|
|
|
|
|
2022-07-19 00:19:16 +02:00
|
|
|
// Create token secret if does not exist (Added v2.1.0)
|
|
|
|
if (!this.db.serverSettings.tokenSecret) {
|
|
|
|
await this.auth.initTokenSecret()
|
|
|
|
}
|
|
|
|
|
2022-09-29 00:12:27 +02:00
|
|
|
await this.cleanUserData() // Remove invalid user item progress
|
2022-03-19 16:13:10 +01:00
|
|
|
await this.purgeMetadata() // Remove metadata folders without library item
|
2022-07-30 00:13:46 +02:00
|
|
|
await this.playbackSessionManager.removeInvalidSessions()
|
2022-05-15 18:19:04 +02:00
|
|
|
await this.cacheManager.ensureCachePaths()
|
2022-06-07 03:51:08 +02:00
|
|
|
await this.abMergeManager.ensureDownloadDirPath()
|
2022-06-18 20:11:15 +02:00
|
|
|
|
2021-10-09 00:30:20 +02:00
|
|
|
await this.backupManager.init()
|
2021-10-31 23:55:28 +01:00
|
|
|
await this.logManager.init()
|
2022-06-08 01:29:43 +02:00
|
|
|
await this.rssFeedManager.init()
|
2022-08-18 01:44:21 +02:00
|
|
|
this.cronManager.init()
|
2021-10-02 01:42:48 +02:00
|
|
|
|
2022-02-24 00:52:21 +01:00
|
|
|
if (this.db.serverSettings.scannerDisableWatcher) {
|
|
|
|
Logger.info(`[Server] Watcher is disabled`)
|
|
|
|
this.watcher.disabled = true
|
|
|
|
} else {
|
2022-03-10 02:23:17 +01:00
|
|
|
this.watcher.initWatcher(this.db.libraries)
|
2022-02-24 00:52:21 +01: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()
|
2022-10-01 23:07:30 +02:00
|
|
|
const router = express.Router()
|
|
|
|
app.use(global.RouterBasePath, router)
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
this.server = http.createServer(app)
|
|
|
|
|
2022-10-01 23:07:30 +02:00
|
|
|
router.use(this.auth.cors)
|
|
|
|
router.use(fileUpload())
|
|
|
|
router.use(express.urlencoded({ extended: true, limit: "5mb" }));
|
|
|
|
router.use(express.json({ limit: "5mb" }))
|
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')
|
2022-10-01 23:07:30 +02:00
|
|
|
router.use(express.static(distPath))
|
2021-10-05 05:11:42 +02:00
|
|
|
|
|
|
|
// Metadata folder static path
|
2022-10-01 23:07:30 +02:00
|
|
|
router.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
2022-03-18 19:44:29 +01:00
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
// Static folder
|
2022-10-01 23:07:30 +02:00
|
|
|
router.use(express.static(Path.join(global.appRoot, 'static')))
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2022-10-01 23:07:30 +02:00
|
|
|
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
|
|
|
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
|
|
|
router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
|
2022-03-11 01:45:02 +01:00
|
|
|
|
2021-10-09 00:30:20 +02:00
|
|
|
// EBook static file routes
|
2022-10-01 23:07:30 +02:00
|
|
|
router.get('/ebook/:library/:folder/*', (req, res) => {
|
2022-03-10 02:23:17 +01:00
|
|
|
var library = this.db.libraries.find(lib => lib.id === req.params.library)
|
2021-10-09 00:30:20 +02:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
|
2022-05-02 21:41:59 +02:00
|
|
|
// RSS Feed temp route
|
2022-10-01 23:07:30 +02:00
|
|
|
router.get('/feed/:id', (req, res) => {
|
2022-06-08 01:29:43 +02:00
|
|
|
Logger.info(`[Server] Requesting rss feed ${req.params.id}`)
|
2022-05-02 21:41:59 +02:00
|
|
|
this.rssFeedManager.getFeed(req, res)
|
|
|
|
})
|
2022-10-01 23:07:30 +02:00
|
|
|
router.get('/feed/:id/cover', (req, res) => {
|
2022-05-02 23:42:30 +02:00
|
|
|
this.rssFeedManager.getFeedCover(req, res)
|
|
|
|
})
|
2022-10-01 23:07:30 +02:00
|
|
|
router.get('/feed/:id/item/:episodeId/*', (req, res) => {
|
2022-06-08 01:29:43 +02:00
|
|
|
Logger.debug(`[Server] Requesting rss feed episode ${req.params.id}/${req.params.episodeId}`)
|
2022-05-02 21:41:59 +02:00
|
|
|
this.rssFeedManager.getFeedItem(req, res)
|
|
|
|
})
|
|
|
|
|
2021-11-13 02:43:16 +01:00
|
|
|
// Client dynamic routes
|
2022-05-14 20:08:56 +02:00
|
|
|
const dyanimicRoutes = [
|
|
|
|
'/item/:id',
|
2022-06-01 23:29:29 +02:00
|
|
|
'/author/:id',
|
2022-05-29 19:55:14 +02:00
|
|
|
'/audiobook/:id/chapters',
|
2022-05-14 20:08:56 +02:00
|
|
|
'/audiobook/:id/edit',
|
2022-10-02 18:53:53 +02:00
|
|
|
'/audiobook/:id/manage',
|
2022-05-14 20:08:56 +02:00
|
|
|
'/library/:library',
|
|
|
|
'/library/:library/search',
|
|
|
|
'/library/:library/bookshelf/:id?',
|
|
|
|
'/library/:library/authors',
|
|
|
|
'/library/:library/series/:id?',
|
2022-09-17 22:23:33 +02:00
|
|
|
'/library/:library/podcast/search',
|
|
|
|
'/library/:library/podcast/latest',
|
2022-05-14 20:08:56 +02:00
|
|
|
'/config/users/:id',
|
2022-05-29 19:55:14 +02:00
|
|
|
'/config/users/:id/sessions',
|
2022-05-14 20:08:56 +02:00
|
|
|
'/collection/:id'
|
|
|
|
]
|
2022-10-01 23:07:30 +02:00
|
|
|
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
2021-08-24 02:37:40 +02:00
|
|
|
|
2022-10-01 23:07:30 +02:00
|
|
|
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res, this.rssFeedManager.feedsArray))
|
|
|
|
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
|
|
|
router.post('/init', (req, res) => {
|
2022-05-15 00:23:22 +02:00
|
|
|
if (this.db.hasRootUser) {
|
|
|
|
Logger.error(`[Server] attempt to init server when server already has a root user`)
|
|
|
|
return res.sendStatus(500)
|
|
|
|
}
|
|
|
|
this.initializeServer(req, res)
|
|
|
|
})
|
2022-10-01 23:07:30 +02:00
|
|
|
router.get('/status', (req, res) => {
|
2022-05-15 00:23:22 +02:00
|
|
|
// status check for client to see if server has been initialized
|
|
|
|
// server has been initialized if a root user exists
|
|
|
|
const payload = {
|
2022-11-09 01:09:07 +01:00
|
|
|
isInit: this.db.hasRootUser,
|
|
|
|
language: this.db.serverSettings.language
|
2022-05-15 00:23:22 +02:00
|
|
|
}
|
|
|
|
if (!payload.isInit) {
|
|
|
|
payload.ConfigPath = global.ConfigPath
|
|
|
|
payload.MetadataPath = global.MetadataPath
|
|
|
|
}
|
|
|
|
res.json(payload)
|
|
|
|
})
|
2022-10-01 23:07:30 +02:00
|
|
|
router.get('/ping', (req, res) => {
|
2022-07-30 15:37:35 +02:00
|
|
|
Logger.info('Received ping')
|
2021-08-18 00:01:11 +02:00
|
|
|
res.json({ success: true })
|
|
|
|
})
|
2022-07-24 22:46:19 +02:00
|
|
|
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
this.server.listen(this.Port, this.Host, () => {
|
2022-03-11 01:45:02 +01:00
|
|
|
Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
2021-08-18 00:01:11 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
2022-03-11 01:45:02 +01:00
|
|
|
Logger.info('[Server] Socket Connected', socket.id)
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
socket.on('auth', (token) => this.authenticateSocket(socket, token))
|
2021-09-12 23:10:12 +02:00
|
|
|
|
|
|
|
// Scanning
|
2021-08-25 03:24:40 +02:00
|
|
|
socket.on('cancel_scan', this.cancelScan.bind(this))
|
2021-09-12 23:10:12 +02:00
|
|
|
|
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))
|
2022-07-08 00:25:52 +02:00
|
|
|
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
|
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
|
|
|
|
2022-04-12 02:42:09 +02:00
|
|
|
socket.on('ping', () => {
|
|
|
|
var client = this.clients[socket.id] || {}
|
|
|
|
var user = client.user || {}
|
|
|
|
Logger.debug(`[Server] Received ping from socket ${user.username || 'No User'}`)
|
|
|
|
socket.emit('pong')
|
|
|
|
})
|
|
|
|
|
2022-07-08 00:25:52 +02:00
|
|
|
socket.on('disconnect', (reason) => {
|
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) {
|
2022-07-08 00:25:52 +02:00
|
|
|
Logger.warn(`[Server] Socket ${socket.id} disconnect, no client (Reason: ${reason})`)
|
2021-08-18 00:01:11 +02:00
|
|
|
} else if (!_client.user) {
|
2022-07-08 00:25:52 +02:00
|
|
|
Logger.info(`[Server] Unauth socket ${socket.id} disconnected (Reason: ${reason})`)
|
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)
|
2022-03-18 01:10:47 +01:00
|
|
|
this.io.emit('user_offline', _client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
2021-10-13 03:07:42 +02:00
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
const disconnectTime = Date.now() - _client.connected_at
|
2022-07-08 00:25:52 +02:00
|
|
|
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
|
2021-08-18 00:01:11 +02:00
|
|
|
delete this.clients[socket.id]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-15 00:23:22 +02:00
|
|
|
async initializeServer(req, res) {
|
|
|
|
Logger.info(`[Server] Initializing new server`)
|
|
|
|
const newRoot = req.body.newRoot
|
|
|
|
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
|
|
|
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
2022-07-19 00:19:16 +02:00
|
|
|
let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username })
|
2022-05-15 00:23:22 +02:00
|
|
|
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
|
|
|
|
|
|
|
|
res.sendStatus(200)
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-03-19 16:13:10 +01:00
|
|
|
// Remove unused /metadata/items/{id} folders
|
2021-10-05 05:11:42 +02:00
|
|
|
async purgeMetadata() {
|
2022-03-19 16:13:10 +01:00
|
|
|
var itemsMetadata = Path.join(global.MetadataPath, 'items')
|
|
|
|
if (!(await fs.pathExists(itemsMetadata))) return
|
|
|
|
var foldersInItemsMetadata = await fs.readdir(itemsMetadata)
|
2021-10-05 05:11:42 +02:00
|
|
|
|
|
|
|
var purged = 0
|
2022-03-19 16:13:10 +01:00
|
|
|
await Promise.all(foldersInItemsMetadata.map(async foldername => {
|
|
|
|
var hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername)
|
|
|
|
if (!hasMatchingItem) {
|
|
|
|
var folderPath = Path.join(itemsMetadata, foldername)
|
2021-10-05 05:11:42 +02:00
|
|
|
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) {
|
2022-03-19 16:13:10 +01:00
|
|
|
Logger.info(`[Server] Purged ${purged} unused library item metadata`)
|
2021-10-05 05:11:42 +02:00
|
|
|
}
|
|
|
|
return purged
|
|
|
|
}
|
|
|
|
|
2022-09-29 00:12:27 +02:00
|
|
|
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
|
|
|
|
async cleanUserData() {
|
2021-11-04 13:59:28 +01:00
|
|
|
for (let i = 0; i < this.db.users.length; i++) {
|
|
|
|
var _user = this.db.users[i]
|
2022-09-29 00:12:27 +02:00
|
|
|
var hasUpdated = false
|
|
|
|
if (_user.mediaProgress.length) {
|
|
|
|
const lengthBefore = _user.mediaProgress.length
|
|
|
|
_user.mediaProgress = _user.mediaProgress.filter(mp => {
|
|
|
|
const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId)
|
|
|
|
if (!libraryItem) return false
|
|
|
|
if (mp.episodeId && (libraryItem.mediaType !== 'podcast' || !libraryItem.media.checkHasEpisode(mp.episodeId))) return false // Episode not found
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
if (lengthBefore > _user.mediaProgress.length) {
|
|
|
|
Logger.debug(`[Server] Removing ${_user.mediaProgress.length - lengthBefore} media progress data from user ${_user.username}`)
|
|
|
|
hasUpdated = true
|
2021-11-04 13:59:28 +01:00
|
|
|
}
|
|
|
|
}
|
2022-09-29 00:12:27 +02:00
|
|
|
if (_user.seriesHideFromContinueListening.length) {
|
|
|
|
_user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => {
|
|
|
|
if (!this.db.series.some(se => se.id === seriesId)) { // Series removed
|
|
|
|
hasUpdated = true
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if (hasUpdated) {
|
|
|
|
await this.db.updateEntity('user', _user)
|
|
|
|
}
|
2021-11-04 13:59:28 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-05 05:11:42 +02:00
|
|
|
// 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)
|
2022-03-18 01:10:47 +01:00
|
|
|
this.io.emit('user_offline', client.user.toJSONForPublic(null, this.db.libraryItems))
|
2021-10-23 03:08:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
delete this.clients[socketId].user
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2022-04-12 23:05:16 +02:00
|
|
|
Logger.debug(`[Server] Authenticating socket client already has user`, client.user.username)
|
2021-10-23 03:08:02 +02:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-08-28 00:27:55 +02:00
|
|
|
Logger.debug(`[Server] User Online ${client.user.username}`)
|
2022-06-18 20:11:15 +02:00
|
|
|
|
2022-03-18 01:10:47 +01:00
|
|
|
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
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 = {
|
2022-02-27 20:47:52 +01:00
|
|
|
metadataPath: global.MetadataPath,
|
|
|
|
configPath: global.ConfigPath,
|
2021-08-18 00:01:11 +02:00
|
|
|
user: client.user.toJSONForBrowser(),
|
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') {
|
2022-11-11 00:42:20 +01:00
|
|
|
initialPayload.usersOnline = this.getUsersOnline()
|
2021-10-23 03:08:02 +02:00
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
client.socket.emit('init', initialPayload)
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2022-03-17 12:06:52 +01:00
|
|
|
module.exports = Server
|