This commit is contained in:
Finn Dittmar 2025-07-21 19:51:44 -05:00 committed by GitHub
commit 0faa419624
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 144 additions and 4 deletions

View File

@ -37,6 +37,7 @@ const CronManager = require('./managers/CronManager')
const ApiCacheManager = require('./managers/ApiCacheManager')
const BinaryManager = require('./managers/BinaryManager')
const ShareManager = require('./managers/ShareManager')
const LastSeenManager = require('./managers/LastSeenManager')
const LibraryScanner = require('./scanner/LibraryScanner')
//Import the main Passport and Express-Session library
@ -107,6 +108,7 @@ class Server {
this.cronManager = new CronManager(this.podcastManager, this.playbackSessionManager)
this.apiCacheManager = new ApiCacheManager()
this.binaryManager = new BinaryManager()
this.lastSeenManager = new LastSeenManager()
// Routers
this.apiRouter = new ApiRouter(this)
@ -130,6 +132,20 @@ class Server {
this.auth.isAuthenticated(req, res, next)
}
/**
* Middleware to track user activity for lastSeen updates
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
lastSeenMiddleware(req, res, next) {
if (req.user && req.user.id) {
this.lastSeenManager.addActiveUser(req.user.id)
}
next()
}
cancelLibraryScan(libraryId) {
LibraryScanner.setCancelLibraryScan(libraryId)
}
@ -171,6 +187,7 @@ class Server {
const libraries = await Database.libraryModel.getAllWithFolders()
await this.cronManager.init(libraries)
this.apiCacheManager.init()
this.lastSeenManager.init()
if (Database.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`)
@ -310,7 +327,7 @@ class Server {
// Skip JSON parsing for internal-api routes
router.use(/^(?!\/internal-api).*/, express.json({ limit: '10mb' }))
router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router)
router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.lastSeenMiddleware.bind(this), this.apiRouter.router)
router.use('/hls', this.hlsRouter.router)
router.use('/public', this.publicRouter.router)
@ -499,6 +516,13 @@ class Server {
*/
async stop() {
Logger.info('=== Stopping Server ===')
// Cleanup LastSeenManager first to flush any pending updates
if (this.lastSeenManager) {
await this.lastSeenManager.cleanup()
Logger.info('[Server] LastSeenManager Cleaned Up')
}
Watcher.close()
Logger.info('[Server] Watcher Closed')
await SocketAuthority.close()

View File

@ -282,9 +282,11 @@ class SocketAuthority {
client.user = user
this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions))
// Update user lastSeen without firing sequelize bulk update hooks
user.lastSeen = Date.now()
await user.save({ hooks: false })
if (this.Server.lastSeenManager) {
this.Server.lastSeenManager.addActiveUser(user.id)
// To ensure actual lastSeen behaviour does not change, we just flush when connecting. This should not add much overhead as this is only for the authenticated socket.
this.Server.lastSeenManager.flushActiveUsers()
}
const initialPayload = {
userId: client.user.id,

View File

@ -0,0 +1,114 @@
const Logger = require('../Logger')
const Database = require('../Database')
/**
* Manager for handling lastSeen updates
*/
class LastSeenManager {
constructor() {
/** @type {Set<string>} Set of user IDs that have made requests */
this.activeUsers = new Set()
/** @type {NodeJS.Timeout} Flush interval timer */
this.flushInterval = null
/** @type {number} Flush interval */
this.flushIntervalMs = 1000 * 60 * 5
}
/**
* Initialize the LastSeenManager
* Start the periodic flush process
*/
init() {
Logger.info('[LastSeenManager] Initializing')
this.startFlushInterval()
}
/**
* Start the periodic flush interval
*/
startFlushInterval() {
if (this.flushInterval) {
clearInterval(this.flushInterval)
}
this.flushInterval = setInterval(() => {
this.flushActiveUsers()
}, this.flushIntervalMs)
Logger.info(`[LastSeenManager] Started flush interval every ${this.flushIntervalMs / 1000} seconds`)
}
/**
* Stop the flush interval
*/
stopFlushInterval() {
if (this.flushInterval) {
clearInterval(this.flushInterval)
this.flushInterval = null
Logger.info('[LastSeenManager] Stopped flush interval')
}
}
/**
* Add a user to the active users set
* @param {string} userId - User ID
*/
addActiveUser(userId) {
if (userId) {
this.activeUsers.add(userId)
}
}
/**
* Flush all active users to the database and clear the set
* Updates lastSeen timestamp for all users in the set
*/
async flushActiveUsers() {
if (this.activeUsers.size === 0) {
Logger.debug('[LastSeenManager] No active users to flush')
return
}
const userIds = Array.from(this.activeUsers)
const currentTime = Date.now()
Logger.debug(`[LastSeenManager] Flushing ${userIds.length} active users to database`)
try {
const affectedRows = await Database.userModel.update(
{ lastSeen: new Date(currentTime) },
{
where: {
id: userIds
},
hooks: false
}
)
Logger.debug(`[LastSeenManager] Successfully updated lastSeen for ${affectedRows[0]} users`)
// Clear the active users set
this.activeUsers.clear()
} catch (error) {
Logger.error(`[LastSeenManager] Failed to flush active users:`, error)
}
}
async forceFlush() {
Logger.info('[LastSeenManager] Force flushing active users')
await this.flushActiveUsers()
}
/**
* Cleanup and stop all processes
*/
async cleanup() {
Logger.info('[LastSeenManager] Cleaning up')
this.stopFlushInterval()
await this.forceFlush()
}
}
module.exports = LastSeenManager