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