1 import { isTestInstance } from '@server/helpers/core-utils'
2 import { logger, loggerTagsFactory } from '@server/helpers/logger'
3 import { VIEW_LIFETIME } from '@server/initializers/constants'
4 import { VideoModel } from '@server/models/video/video'
5 import { MVideo } from '@server/types/models'
6 import { PeerTubeSocket } from './peertube-socket'
7 import { Redis } from './redis'
9 const lTags = loggerTagsFactory('views')
11 export class VideoViews {
13 // Values are Date().getTime()
14 private readonly viewersPerVideo = new Map<number, number[]>()
16 private static instance: VideoViews
18 private constructor () {
22 setInterval(() => this.cleanViewers(), VIEW_LIFETIME.VIEWER)
25 async processView (options: {
30 const { video, ip, viewerExpires } = options
32 logger.debug('Processing view for %s and ip %s.', video.url, ip, lTags())
34 let success = await this.addView(video, ip)
37 const successViewer = await this.addViewer(video, ip, viewerExpires)
38 success ||= successViewer
44 getViewers (video: MVideo) {
45 const viewers = this.viewersPerVideo.get(video.id)
46 if (!viewers) return 0
51 buildViewerExpireTime () {
52 return new Date().getTime() + VIEW_LIFETIME.VIEWER
55 private async addView (video: MVideo, ip: string | null) {
56 const promises: Promise<any>[] = []
59 const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid)
60 if (viewExists) return false
62 promises.push(Redis.Instance.setIPVideoView(ip, video.uuid))
65 if (video.isOwned()) {
66 promises.push(Redis.Instance.addLocalVideoView(video.id))
69 promises.push(Redis.Instance.addVideoViewStats(video.id))
71 await Promise.all(promises)
76 private async addViewer (video: MVideo, ip: string | null, viewerExpires?: Date) {
78 const viewExists = await Redis.Instance.doesVideoIPViewerExist(ip, video.uuid)
79 if (viewExists) return false
81 await Redis.Instance.setIPVideoViewer(ip, video.uuid)
84 let watchers = this.viewersPerVideo.get(video.id)
88 this.viewersPerVideo.set(video.id, watchers)
91 const expiration = viewerExpires
92 ? viewerExpires.getTime()
93 : this.buildViewerExpireTime()
95 watchers.push(expiration)
96 await this.notifyClients(video.id, watchers.length)
101 private async cleanViewers () {
102 if (!isTestInstance()) logger.info('Cleaning video viewers.', lTags())
104 for (const videoId of this.viewersPerVideo.keys()) {
105 const notBefore = new Date().getTime()
107 const viewers = this.viewersPerVideo.get(videoId)
109 // Only keep not expired viewers
110 const newViewers = viewers.filter(w => w > notBefore)
112 if (newViewers.length === 0) this.viewersPerVideo.delete(videoId)
113 else this.viewersPerVideo.set(videoId, newViewers)
115 await this.notifyClients(videoId, newViewers.length)
119 private async notifyClients (videoId: string | number, viewersLength: number) {
120 const video = await VideoModel.loadImmutableAttributes(videoId)
123 PeerTubeSocket.Instance.sendVideoViewsUpdate(video, viewersLength)
125 logger.debug('Live video views update for %s is %d.', video.url, viewersLength, lTags())
128 static get Instance () {
129 return this.instance || (this.instance = new this())