]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/redundancy/video-redundancy.ts
Fix videos list user NSFW policy
[github/Chocobozzz/PeerTube.git] / server / models / redundancy / video-redundancy.ts
CommitLineData
c48e82b5
C
1import {
2 AfterDestroy,
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 ForeignKey,
9 Is,
10 Model,
11 Scopes,
12 Sequelize,
13 Table,
14 UpdatedAt
15} from 'sequelize-typescript'
16import { ActorModel } from '../activitypub/actor'
b36f41ca 17import { getVideoSort, throwIfNotValid } from '../utils'
c48e82b5 18import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
b36f41ca 19import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
c48e82b5 20import { VideoFileModel } from '../video/video-file'
c48e82b5
C
21import { getServerActor } from '../../helpers/utils'
22import { VideoModel } from '../video/video'
23import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
24import { logger } from '../../helpers/logger'
25import { CacheFileObject } from '../../../shared'
26import { VideoChannelModel } from '../video/video-channel'
27import { ServerModel } from '../server/server'
28import { sample } from 'lodash'
29import { isTestInstance } from '../../helpers/core-utils'
3f6b6a56 30import * as Bluebird from 'bluebird'
c48e82b5
C
31
32export enum ScopeNames {
33 WITH_VIDEO = 'WITH_VIDEO'
34}
35
36@Scopes({
37 [ ScopeNames.WITH_VIDEO ]: {
38 include: [
39 {
40 model: () => VideoFileModel,
41 required: true,
42 include: [
43 {
44 model: () => VideoModel,
45 required: true
46 }
47 ]
48 }
49 ]
50 }
51})
52
53@Table({
54 tableName: 'videoRedundancy',
55 indexes: [
56 {
57 fields: [ 'videoFileId' ]
58 },
59 {
60 fields: [ 'actorId' ]
61 },
62 {
63 fields: [ 'url' ],
64 unique: true
65 }
66 ]
67})
68export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
69
70 @CreatedAt
71 createdAt: Date
72
73 @UpdatedAt
74 updatedAt: Date
75
76 @AllowNull(false)
77 @Column
78 expiresOn: Date
79
80 @AllowNull(false)
81 @Is('VideoRedundancyFileUrl', value => throwIfNotValid(value, isUrlValid, 'fileUrl'))
82 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
83 fileUrl: string
84
85 @AllowNull(false)
86 @Is('VideoRedundancyUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
87 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
88 url: string
89
90 @AllowNull(true)
91 @Column
92 strategy: string // Only used by us
93
94 @ForeignKey(() => VideoFileModel)
95 @Column
96 videoFileId: number
97
98 @BelongsTo(() => VideoFileModel, {
99 foreignKey: {
100 allowNull: false
101 },
102 onDelete: 'cascade'
103 })
104 VideoFile: VideoFileModel
105
106 @ForeignKey(() => ActorModel)
107 @Column
108 actorId: number
109
110 @BelongsTo(() => ActorModel, {
111 foreignKey: {
112 allowNull: false
113 },
114 onDelete: 'cascade'
115 })
116 Actor: ActorModel
117
118 @AfterDestroy
119 static removeFilesAndSendDelete (instance: VideoRedundancyModel) {
120 // Not us
121 if (!instance.strategy) return
122
123 logger.info('Removing video file %s-.', instance.VideoFile.Video.uuid, instance.VideoFile.resolution)
124
125 return instance.VideoFile.Video.removeFile(instance.VideoFile)
126 }
127
128 static loadByFileId (videoFileId: number) {
129 const query = {
130 where: {
131 videoFileId
132 }
133 }
134
135 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
136 }
137
138 static loadByUrl (url: string) {
139 const query = {
140 where: {
141 url
142 }
143 }
144
145 return VideoRedundancyModel.findOne(query)
146 }
147
3f6b6a56
C
148 static async getVideoSample (p: Bluebird<VideoModel[]>) {
149 const rows = await p
b36f41ca
C
150 const ids = rows.map(r => r.id)
151 const id = sample(ids)
152
153 return VideoModel.loadWithFile(id, undefined, !isTestInstance())
154 }
155
c48e82b5
C
156 static async findMostViewToDuplicate (randomizedFactor: number) {
157 // On VideoModel!
158 const query = {
b36f41ca 159 attributes: [ 'id', 'views' ],
c48e82b5
C
160 logging: !isTestInstance(),
161 limit: randomizedFactor,
b36f41ca 162 order: getVideoSort('-views'),
c48e82b5 163 include: [
b36f41ca
C
164 await VideoRedundancyModel.buildVideoFileForDuplication(),
165 VideoRedundancyModel.buildServerRedundancyInclude()
166 ]
167 }
168
3f6b6a56 169 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
b36f41ca
C
170 }
171
172 static async findTrendingToDuplicate (randomizedFactor: number) {
173 // On VideoModel!
174 const query = {
175 attributes: [ 'id', 'views' ],
176 subQuery: false,
177 logging: !isTestInstance(),
178 group: 'VideoModel.id',
179 limit: randomizedFactor,
180 order: getVideoSort('-trending'),
181 include: [
182 await VideoRedundancyModel.buildVideoFileForDuplication(),
183 VideoRedundancyModel.buildServerRedundancyInclude(),
184
185 VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
c48e82b5
C
186 ]
187 }
188
3f6b6a56
C
189 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
190 }
191
192 static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
193 // On VideoModel!
194 const query = {
195 attributes: [ 'id', 'publishedAt' ],
098eb377 196 logging: !isTestInstance(),
3f6b6a56
C
197 limit: randomizedFactor,
198 order: getVideoSort('-publishedAt'),
199 where: {
200 views: {
201 [ Sequelize.Op.gte ]: minViews
202 }
203 },
204 include: [
205 await VideoRedundancyModel.buildVideoFileForDuplication(),
206 VideoRedundancyModel.buildServerRedundancyInclude()
207 ]
208 }
c48e82b5 209
3f6b6a56 210 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
c48e82b5
C
211 }
212
3f6b6a56 213 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
c48e82b5
C
214 const actor = await getServerActor()
215
3f6b6a56 216 const options = {
c48e82b5 217 logging: !isTestInstance(),
3f6b6a56
C
218 include: [
219 {
220 attributes: [],
221 model: VideoRedundancyModel,
222 required: true,
223 where: {
224 actorId: actor.id,
225 strategy
226 }
227 }
228 ]
c48e82b5
C
229 }
230
3f6b6a56 231 return VideoFileModel.sum('size', options)
c48e82b5
C
232 }
233
234 static listAllExpired () {
235 const query = {
236 logging: !isTestInstance(),
237 where: {
238 expiresOn: {
b36f41ca 239 [ Sequelize.Op.lt ]: new Date()
c48e82b5
C
240 }
241 }
242 }
243
244 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
245 .findAll(query)
246 }
247
4b5384f6
C
248 static async getStats (strategy: VideoRedundancyStrategy) {
249 const actor = await getServerActor()
250
251 const query = {
252 raw: true,
253 attributes: [
254 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
255 [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', 'videoId')), 'totalVideos' ],
256 [ Sequelize.fn('COUNT', 'videoFileId'), 'totalVideoFiles' ]
257 ],
258 where: {
259 strategy,
260 actorId: actor.id
261 },
262 include: [
263 {
264 attributes: [],
265 model: VideoFileModel,
266 required: true
267 }
268 ]
269 }
270
271 return VideoRedundancyModel.find(query as any) // FIXME: typings
272 .then((r: any) => ({
273 totalUsed: parseInt(r.totalUsed.toString(), 10),
274 totalVideos: r.totalVideos,
275 totalVideoFiles: r.totalVideoFiles
276 }))
277 }
278
c48e82b5
C
279 toActivityPubObject (): CacheFileObject {
280 return {
281 id: this.url,
282 type: 'CacheFile' as 'CacheFile',
283 object: this.VideoFile.Video.url,
284 expires: this.expiresOn.toISOString(),
285 url: {
286 type: 'Link',
287 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
288 href: this.fileUrl,
289 height: this.VideoFile.resolution,
290 size: this.VideoFile.size,
291 fps: this.VideoFile.fps
292 }
293 }
294 }
295
b36f41ca
C
296 // Don't include video files we already duplicated
297 private static async buildVideoFileForDuplication () {
c48e82b5
C
298 const actor = await getServerActor()
299
b36f41ca 300 const notIn = Sequelize.literal(
c48e82b5
C
301 '(' +
302 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "expiresOn" >= NOW()` +
303 ')'
304 )
b36f41ca
C
305
306 return {
307 attributes: [],
308 model: VideoFileModel.unscoped(),
309 required: true,
310 where: {
311 id: {
312 [ Sequelize.Op.notIn ]: notIn
313 }
314 }
315 }
316 }
317
318 private static buildServerRedundancyInclude () {
319 return {
320 attributes: [],
321 model: VideoChannelModel.unscoped(),
322 required: true,
323 include: [
324 {
325 attributes: [],
326 model: ActorModel.unscoped(),
327 required: true,
328 include: [
329 {
330 attributes: [],
331 model: ServerModel.unscoped(),
332 required: true,
333 where: {
334 redundancyAllowed: true
335 }
336 }
337 ]
338 }
339 ]
340 }
c48e82b5
C
341 }
342}