const Database = require('../Database') const Logger = require('../Logger') /** * @typedef OpenMediaItemShareObject * @property {string} id * @property {import('../models/MediaItemShare').MediaItemShareObject} mediaItemShare * @property {NodeJS.Timeout} timeout */ class ShareManager { constructor() { /** @type {OpenMediaItemShareObject[]} */ this.openMediaItemShares = [] /** @type {import('../objects/PlaybackSession')[]} */ this.openSharePlaybackSessions = [] } init() { this.loadMediaItemShares() } /** * @param {import('../objects/PlaybackSession')} playbackSession */ addOpenSharePlaybackSession(playbackSession) { Logger.info(`[ShareManager] Adding new open share playback session "${playbackSession.displayTitle}"`) this.openSharePlaybackSessions.push(playbackSession) } /** * * @param {import('../objects/PlaybackSession')} playbackSession */ closeSharePlaybackSession(playbackSession) { Logger.info(`[ShareManager] Closing share playback session "${playbackSession.displayTitle}"`) this.openSharePlaybackSessions = this.openSharePlaybackSessions.filter((s) => s.id !== playbackSession.id) } /** * Find an open media item share by media item ID * @param {string} mediaItemId * @returns {import('../models/MediaItemShare').MediaItemShareForClient} */ findByMediaItemId(mediaItemId) { const mediaItemShareObject = this.openMediaItemShares.find((s) => s.mediaItemShare.mediaItemId === mediaItemId)?.mediaItemShare if (mediaItemShareObject) { const mediaItemShareObjectForClient = { ...mediaItemShareObject } delete mediaItemShareObjectForClient.pash delete mediaItemShareObjectForClient.userId delete mediaItemShareObjectForClient.extraData return mediaItemShareObjectForClient } return null } /** * Find an open media item share by slug * @param {string} slug * @returns {import('../models/MediaItemShare').MediaItemShareForClient} */ findBySlug(slug) { const mediaItemShareObject = this.openMediaItemShares.find((s) => s.mediaItemShare.slug === slug)?.mediaItemShare if (mediaItemShareObject) { const mediaItemShareObjectForClient = { ...mediaItemShareObject } delete mediaItemShareObjectForClient.pash delete mediaItemShareObjectForClient.userId delete mediaItemShareObjectForClient.extraData return mediaItemShareObjectForClient } return null } /** * @param {string} shareSessionId * @returns {import('../objects/PlaybackSession')} */ findPlaybackSessionBySessionId(shareSessionId) { return this.openSharePlaybackSessions.find((s) => s.shareSessionId === shareSessionId) } /** * Load all media item shares from the database * Remove expired & schedule active */ async loadMediaItemShares() { /** @type {import('../models/MediaItemShare').MediaItemShareModel[]} */ const mediaItemShares = await Database.models.mediaItemShare.findAll() for (const mediaItemShare of mediaItemShares) { if (mediaItemShare.expiresAt && mediaItemShare.expiresAt.valueOf() < Date.now()) { Logger.info(`[ShareManager] Removing expired media item share "${mediaItemShare.id}"`) await this.destroyMediaItemShare(mediaItemShare.id) } else if (mediaItemShare.expiresAt) { this.scheduleMediaItemShare(mediaItemShare) } else { Logger.info(`[ShareManager] Loaded permanent media item share "${mediaItemShare.id}"`) this.openMediaItemShares.push({ id: mediaItemShare.id, mediaItemShare: mediaItemShare.toJSON() }) } } } /** * * @param {import('../models/MediaItemShare').MediaItemShareModel} mediaItemShare */ scheduleMediaItemShare(mediaItemShare) { if (!mediaItemShare?.expiresAt) return const expiresAtDuration = mediaItemShare.expiresAt.valueOf() - Date.now() if (expiresAtDuration <= 0) { Logger.warn(`[ShareManager] Attempted to schedule expired media item share "${mediaItemShare.id}"`) this.destroyMediaItemShare(mediaItemShare.id) return } const timeout = setTimeout(() => { Logger.info(`[ShareManager] Removing expired media item share "${mediaItemShare.id}"`) this.removeMediaItemShare(mediaItemShare.id) }, expiresAtDuration) this.openMediaItemShares.push({ id: mediaItemShare.id, mediaItemShare: mediaItemShare.toJSON(), timeout }) Logger.info(`[ShareManager] Scheduled media item share "${mediaItemShare.id}" to expire in ${expiresAtDuration}ms`) } /** * * @param {import('../models/MediaItemShare').MediaItemShareModel} mediaItemShare */ openMediaItemShare(mediaItemShare) { if (mediaItemShare.expiresAt) { this.scheduleMediaItemShare(mediaItemShare) } else { this.openMediaItemShares.push({ id: mediaItemShare.id, mediaItemShare: mediaItemShare.toJSON() }) } } /** * * @param {string} mediaItemShareId */ async removeMediaItemShare(mediaItemShareId) { const mediaItemShare = this.openMediaItemShares.find((s) => s.id === mediaItemShareId) if (!mediaItemShare) return if (mediaItemShare.timeout) { clearTimeout(mediaItemShare.timeout) } this.openMediaItemShares = this.openMediaItemShares.filter((s) => s.id !== mediaItemShareId) this.openSharePlaybackSessions = this.openSharePlaybackSessions.filter((s) => s.mediaItemShareId !== mediaItemShareId) await this.destroyMediaItemShare(mediaItemShareId) } /** * * @param {string} mediaItemShareId */ destroyMediaItemShare(mediaItemShareId) { return Database.models.mediaItemShare.destroy({ where: { id: mediaItemShareId } }) } /** * Close open share sessions that have not been updated in the last 24 hours */ closeStaleOpenShareSessions() { const updatedAtTimeCutoff = Date.now() - 1000 * 60 * 60 * 24 const staleSessions = this.openSharePlaybackSessions.filter((session) => session.updatedAt < updatedAtTimeCutoff) for (const session of staleSessions) { const sessionLastUpdate = new Date(session.updatedAt) Logger.info(`[PlaybackSessionManager] Closing stale session "${session.displayTitle}" (${session.id}) last updated at ${sessionLastUpdate}`) this.closeSharePlaybackSession(session) } } } module.exports = new ShareManager()