1 import { logger, loggerTagsFactory } from '@server/helpers/logger'
2 import { VIEW_LIFETIME } from '@server/initializers/constants'
3 import { VideoModel } from '@server/models/video/video'
4 import { MVideo } from '@server/types/models'
5 import { PeerTubeSocket } from './peertube-socket'
6 import { Redis } from './redis'
8 const lTags = loggerTagsFactory('views')
10 export class VideoViews {
12 // Values are Date().getTime()
13 private readonly viewersPerVideo = new Map<number, number[]>()
15 private static instance: VideoViews
17 private constructor () {
21 setInterval(() => this.cleanViewers(), VIEW_LIFETIME.VIEWER)
24 async processView (options: {
29 const { video, ip, viewerExpires } = options
31 logger.debug('Processing view for %s and ip %s.', video.url, ip, lTags())
33 let success = await this.addView(video, ip)
36 const successViewer = await this.addViewer(video, ip, viewerExpires)
37 success ||= successViewer
43 getViewers (video: MVideo) {
44 const viewers = this.viewersPerVideo.get(video.id)
45 if (!viewers) return 0
50 buildViewerExpireTime () {
51 return new Date().getTime() + VIEW_LIFETIME.VIEWER
54 private async addView (video: MVideo, ip: string | null) {
55 const promises: Promise<any>[] = []
58 const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid)
59 if (viewExists) return false
61 promises.push(Redis.Instance.setIPVideoView(ip, video.uuid))
64 if (video.isOwned()) {
65 promises.push(Redis.Instance.addLocalVideoView(video.id))
68 promises.push(Redis.Instance.addVideoViewStats(video.id))
70 await Promise.all(promises)
75 private async addViewer (video: MVideo, ip: string | null, viewerExpires?: Date) {
77 const viewExists = await Redis.Instance.doesVideoIPViewerExist(ip, video.uuid)
78 if (viewExists) return false
80 await Redis.Instance.setIPVideoViewer(ip, video.uuid)
83 let watchers = this.viewersPerVideo.get(video.id)
87 this.viewersPerVideo.set(video.id, watchers)
90 const expiration = viewerExpires
91 ? viewerExpires.getTime()
92 : this.buildViewerExpireTime()
94 watchers.push(expiration)
95 await this.notifyClients(video.id, watchers.length)
100 private async cleanViewers () {
101 logger.info('Cleaning video viewers.', lTags())
103 for (const videoId of this.viewersPerVideo.keys()) {
104 const notBefore = new Date().getTime()
106 const viewers = this.viewersPerVideo.get(videoId)
108 // Only keep not expired viewers
109 const newViewers = viewers.filter(w => w > notBefore)
111 if (newViewers.length === 0) this.viewersPerVideo.delete(videoId)
112 else this.viewersPerVideo.set(videoId, newViewers)
114 await this.notifyClients(videoId, newViewers.length)
118 private async notifyClients (videoId: string | number, viewersLength: number) {
119 const video = await VideoModel.loadImmutableAttributes(videoId)
122 PeerTubeSocket.Instance.sendVideoViewsUpdate(video, viewersLength)
124 logger.debug('Live video views update for %s is %d.', video.url, viewersLength, lTags())
127 static get Instance () {
128 return this.instance || (this.instance = new this())