mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			
		
			
				
	
	
		
			132 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			132 lines
		
	
	
		
			3.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
| import { isTestInstance } from '@server/helpers/core-utils'
 | |
| import { logger, loggerTagsFactory } from '@server/helpers/logger'
 | |
| import { VIEW_LIFETIME } from '@server/initializers/constants'
 | |
| import { VideoModel } from '@server/models/video/video'
 | |
| import { MVideo } from '@server/types/models'
 | |
| import { PeerTubeSocket } from './peertube-socket'
 | |
| import { Redis } from './redis'
 | |
| 
 | |
| const lTags = loggerTagsFactory('views')
 | |
| 
 | |
| export class VideoViews {
 | |
| 
 | |
|   // Values are Date().getTime()
 | |
|   private readonly viewersPerVideo = new Map<number, number[]>()
 | |
| 
 | |
|   private static instance: VideoViews
 | |
| 
 | |
|   private constructor () {
 | |
|   }
 | |
| 
 | |
|   init () {
 | |
|     setInterval(() => this.cleanViewers(), VIEW_LIFETIME.VIEWER)
 | |
|   }
 | |
| 
 | |
|   async processView (options: {
 | |
|     video: MVideo
 | |
|     ip: string | null
 | |
|     viewerExpires?: Date
 | |
|   }) {
 | |
|     const { video, ip, viewerExpires } = options
 | |
| 
 | |
|     logger.debug('Processing view for %s and ip %s.', video.url, ip, lTags())
 | |
| 
 | |
|     let success = await this.addView(video, ip)
 | |
| 
 | |
|     if (video.isLive) {
 | |
|       const successViewer = await this.addViewer(video, ip, viewerExpires)
 | |
|       success ||= successViewer
 | |
|     }
 | |
| 
 | |
|     return success
 | |
|   }
 | |
| 
 | |
|   getViewers (video: MVideo) {
 | |
|     const viewers = this.viewersPerVideo.get(video.id)
 | |
|     if (!viewers) return 0
 | |
| 
 | |
|     return viewers.length
 | |
|   }
 | |
| 
 | |
|   buildViewerExpireTime () {
 | |
|     return new Date().getTime() + VIEW_LIFETIME.VIEWER
 | |
|   }
 | |
| 
 | |
|   private async addView (video: MVideo, ip: string | null) {
 | |
|     const promises: Promise<any>[] = []
 | |
| 
 | |
|     if (ip !== null) {
 | |
|       const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid)
 | |
|       if (viewExists) return false
 | |
| 
 | |
|       promises.push(Redis.Instance.setIPVideoView(ip, video.uuid))
 | |
|     }
 | |
| 
 | |
|     if (video.isOwned()) {
 | |
|       promises.push(Redis.Instance.addLocalVideoView(video.id))
 | |
|     }
 | |
| 
 | |
|     promises.push(Redis.Instance.addVideoViewStats(video.id))
 | |
| 
 | |
|     await Promise.all(promises)
 | |
| 
 | |
|     return true
 | |
|   }
 | |
| 
 | |
|   private async addViewer (video: MVideo, ip: string | null, viewerExpires?: Date) {
 | |
|     if (ip !== null) {
 | |
|       const viewExists = await Redis.Instance.doesVideoIPViewerExist(ip, video.uuid)
 | |
|       if (viewExists) return false
 | |
| 
 | |
|       await Redis.Instance.setIPVideoViewer(ip, video.uuid)
 | |
|     }
 | |
| 
 | |
|     let watchers = this.viewersPerVideo.get(video.id)
 | |
| 
 | |
|     if (!watchers) {
 | |
|       watchers = []
 | |
|       this.viewersPerVideo.set(video.id, watchers)
 | |
|     }
 | |
| 
 | |
|     const expiration = viewerExpires
 | |
|       ? viewerExpires.getTime()
 | |
|       : this.buildViewerExpireTime()
 | |
| 
 | |
|     watchers.push(expiration)
 | |
|     await this.notifyClients(video.id, watchers.length)
 | |
| 
 | |
|     return true
 | |
|   }
 | |
| 
 | |
|   private async cleanViewers () {
 | |
|     if (!isTestInstance()) logger.info('Cleaning video viewers.', lTags())
 | |
| 
 | |
|     for (const videoId of this.viewersPerVideo.keys()) {
 | |
|       const notBefore = new Date().getTime()
 | |
| 
 | |
|       const viewers = this.viewersPerVideo.get(videoId)
 | |
| 
 | |
|       // Only keep not expired viewers
 | |
|       const newViewers = viewers.filter(w => w > notBefore)
 | |
| 
 | |
|       if (newViewers.length === 0) this.viewersPerVideo.delete(videoId)
 | |
|       else this.viewersPerVideo.set(videoId, newViewers)
 | |
| 
 | |
|       await this.notifyClients(videoId, newViewers.length)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async notifyClients (videoId: string | number, viewersLength: number) {
 | |
|     const video = await VideoModel.loadImmutableAttributes(videoId)
 | |
|     if (!video) return
 | |
| 
 | |
|     PeerTubeSocket.Instance.sendVideoViewsUpdate(video, viewersLength)
 | |
| 
 | |
|     logger.debug('Live video views update for %s is %d.', video.url, viewersLength, lTags())
 | |
|   }
 | |
| 
 | |
|   static get Instance () {
 | |
|     return this.instance || (this.instance = new this())
 | |
|   }
 | |
| }
 |