]>
Commit | Line | Data |
---|---|---|
3455c265 | 1 | import { isTestInstance } from '@server/helpers/core-utils' |
51353d9a C |
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' | |
8 | ||
9 | const lTags = loggerTagsFactory('views') | |
10 | ||
11 | export class VideoViews { | |
12 | ||
13 | // Values are Date().getTime() | |
14 | private readonly viewersPerVideo = new Map<number, number[]>() | |
15 | ||
16 | private static instance: VideoViews | |
17 | ||
18 | private constructor () { | |
19 | } | |
20 | ||
21 | init () { | |
22 | setInterval(() => this.cleanViewers(), VIEW_LIFETIME.VIEWER) | |
23 | } | |
24 | ||
25 | async processView (options: { | |
26 | video: MVideo | |
27 | ip: string | null | |
28 | viewerExpires?: Date | |
29 | }) { | |
30 | const { video, ip, viewerExpires } = options | |
31 | ||
32 | logger.debug('Processing view for %s and ip %s.', video.url, ip, lTags()) | |
33 | ||
34 | let success = await this.addView(video, ip) | |
35 | ||
36 | if (video.isLive) { | |
37 | const successViewer = await this.addViewer(video, ip, viewerExpires) | |
38 | success ||= successViewer | |
39 | } | |
40 | ||
41 | return success | |
42 | } | |
43 | ||
44 | getViewers (video: MVideo) { | |
45 | const viewers = this.viewersPerVideo.get(video.id) | |
46 | if (!viewers) return 0 | |
47 | ||
48 | return viewers.length | |
49 | } | |
50 | ||
51 | buildViewerExpireTime () { | |
52 | return new Date().getTime() + VIEW_LIFETIME.VIEWER | |
53 | } | |
54 | ||
55 | private async addView (video: MVideo, ip: string | null) { | |
56 | const promises: Promise<any>[] = [] | |
57 | ||
58 | if (ip !== null) { | |
59 | const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid) | |
60 | if (viewExists) return false | |
61 | ||
62 | promises.push(Redis.Instance.setIPVideoView(ip, video.uuid)) | |
63 | } | |
64 | ||
65 | if (video.isOwned()) { | |
66 | promises.push(Redis.Instance.addLocalVideoView(video.id)) | |
67 | } | |
68 | ||
69 | promises.push(Redis.Instance.addVideoViewStats(video.id)) | |
70 | ||
71 | await Promise.all(promises) | |
72 | ||
73 | return true | |
74 | } | |
75 | ||
76 | private async addViewer (video: MVideo, ip: string | null, viewerExpires?: Date) { | |
77 | if (ip !== null) { | |
78 | const viewExists = await Redis.Instance.doesVideoIPViewerExist(ip, video.uuid) | |
79 | if (viewExists) return false | |
80 | ||
81 | await Redis.Instance.setIPVideoViewer(ip, video.uuid) | |
82 | } | |
83 | ||
84 | let watchers = this.viewersPerVideo.get(video.id) | |
85 | ||
86 | if (!watchers) { | |
87 | watchers = [] | |
88 | this.viewersPerVideo.set(video.id, watchers) | |
89 | } | |
90 | ||
91 | const expiration = viewerExpires | |
92 | ? viewerExpires.getTime() | |
93 | : this.buildViewerExpireTime() | |
94 | ||
95 | watchers.push(expiration) | |
96 | await this.notifyClients(video.id, watchers.length) | |
97 | ||
98 | return true | |
99 | } | |
100 | ||
101 | private async cleanViewers () { | |
3455c265 | 102 | if (!isTestInstance()) logger.info('Cleaning video viewers.', lTags()) |
51353d9a C |
103 | |
104 | for (const videoId of this.viewersPerVideo.keys()) { | |
105 | const notBefore = new Date().getTime() | |
106 | ||
107 | const viewers = this.viewersPerVideo.get(videoId) | |
108 | ||
109 | // Only keep not expired viewers | |
110 | const newViewers = viewers.filter(w => w > notBefore) | |
111 | ||
112 | if (newViewers.length === 0) this.viewersPerVideo.delete(videoId) | |
113 | else this.viewersPerVideo.set(videoId, newViewers) | |
114 | ||
115 | await this.notifyClients(videoId, newViewers.length) | |
116 | } | |
117 | } | |
118 | ||
119 | private async notifyClients (videoId: string | number, viewersLength: number) { | |
120 | const video = await VideoModel.loadImmutableAttributes(videoId) | |
121 | if (!video) return | |
122 | ||
123 | PeerTubeSocket.Instance.sendVideoViewsUpdate(video, viewersLength) | |
124 | ||
125 | logger.debug('Live video views update for %s is %d.', video.url, viewersLength, lTags()) | |
126 | } | |
127 | ||
128 | static get Instance () { | |
129 | return this.instance || (this.instance = new this()) | |
130 | } | |
131 | } |