AP mimeType -> mediaType
[oweals/peertube.git] / server / models / redundancy / video-redundancy.ts
1 import {
2   AllowNull,
3   BeforeDestroy,
4   BelongsTo,
5   Column,
6   CreatedAt,
7   DataType,
8   ForeignKey,
9   Is,
10   Model,
11   Scopes,
12   Table,
13   UpdatedAt
14 } from 'sequelize-typescript'
15 import { ActorModel } from '../activitypub/actor'
16 import { getVideoSort, throwIfNotValid } from '../utils'
17 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18 import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
19 import { VideoFileModel } from '../video/video-file'
20 import { getServerActor } from '../../helpers/utils'
21 import { VideoModel } from '../video/video'
22 import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
23 import { logger } from '../../helpers/logger'
24 import { CacheFileObject, VideoPrivacy } from '../../../shared'
25 import { VideoChannelModel } from '../video/video-channel'
26 import { ServerModel } from '../server/server'
27 import { sample } from 'lodash'
28 import { isTestInstance } from '../../helpers/core-utils'
29 import * as Bluebird from 'bluebird'
30 import * as Sequelize from 'sequelize'
31
32 export 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 })
68 export 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   @BeforeDestroy
119   static async removeFile (instance: VideoRedundancyModel) {
120     // Not us
121     if (!instance.strategy) return
122
123     const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
124
125     const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
126     logger.info('Removing duplicated video file %s.', logIdentifier)
127
128     videoFile.Video.removeFile(videoFile)
129              .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
130
131     return undefined
132   }
133
134   static async loadLocalByFileId (videoFileId: number) {
135     const actor = await getServerActor()
136
137     const query = {
138       where: {
139         actorId: actor.id,
140         videoFileId
141       }
142     }
143
144     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
145   }
146
147   static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
148     const query = {
149       where: {
150         url
151       },
152       transaction
153     }
154
155     return VideoRedundancyModel.findOne(query)
156   }
157
158   static async isLocalByVideoUUIDExists (uuid: string) {
159     const actor = await getServerActor()
160
161     const query = {
162       raw: true,
163       attributes: [ 'id' ],
164       where: {
165         actorId: actor.id
166       },
167       include: [
168         {
169           attributes: [ ],
170           model: VideoFileModel,
171           required: true,
172           include: [
173             {
174               attributes: [ ],
175               model: VideoModel,
176               required: true,
177               where: {
178                 uuid
179               }
180             }
181           ]
182         }
183       ]
184     }
185
186     return VideoRedundancyModel.findOne(query)
187       .then(r => !!r)
188   }
189
190   static async getVideoSample (p: Bluebird<VideoModel[]>) {
191     const rows = await p
192     const ids = rows.map(r => r.id)
193     const id = sample(ids)
194
195     return VideoModel.loadWithFile(id, undefined, !isTestInstance())
196   }
197
198   static async findMostViewToDuplicate (randomizedFactor: number) {
199     // On VideoModel!
200     const query = {
201       attributes: [ 'id', 'views' ],
202       limit: randomizedFactor,
203       order: getVideoSort('-views'),
204       where: {
205         privacy: VideoPrivacy.PUBLIC
206       },
207       include: [
208         await VideoRedundancyModel.buildVideoFileForDuplication(),
209         VideoRedundancyModel.buildServerRedundancyInclude()
210       ]
211     }
212
213     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
214   }
215
216   static async findTrendingToDuplicate (randomizedFactor: number) {
217     // On VideoModel!
218     const query = {
219       attributes: [ 'id', 'views' ],
220       subQuery: false,
221       group: 'VideoModel.id',
222       limit: randomizedFactor,
223       order: getVideoSort('-trending'),
224       where: {
225         privacy: VideoPrivacy.PUBLIC
226       },
227       include: [
228         await VideoRedundancyModel.buildVideoFileForDuplication(),
229         VideoRedundancyModel.buildServerRedundancyInclude(),
230
231         VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
232       ]
233     }
234
235     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
236   }
237
238   static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
239     // On VideoModel!
240     const query = {
241       attributes: [ 'id', 'publishedAt' ],
242       limit: randomizedFactor,
243       order: getVideoSort('-publishedAt'),
244       where: {
245         privacy: VideoPrivacy.PUBLIC,
246         views: {
247           [ Sequelize.Op.gte ]: minViews
248         }
249       },
250       include: [
251         await VideoRedundancyModel.buildVideoFileForDuplication(),
252         VideoRedundancyModel.buildServerRedundancyInclude()
253       ]
254     }
255
256     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
257   }
258
259   static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
260     const expiredDate = new Date()
261     expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
262
263     const actor = await getServerActor()
264
265     const query = {
266       where: {
267         actorId: actor.id,
268         strategy,
269         createdAt: {
270           [ Sequelize.Op.lt ]: expiredDate
271         }
272       }
273     }
274
275     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
276   }
277
278   static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
279     const actor = await getServerActor()
280
281     const options = {
282       include: [
283         {
284           attributes: [],
285           model: VideoRedundancyModel,
286           required: true,
287           where: {
288             actorId: actor.id,
289             strategy
290           }
291         }
292       ]
293     }
294
295     return VideoFileModel.sum('size', options as any) // FIXME: typings
296   }
297
298   static async listLocalExpired () {
299     const actor = await getServerActor()
300
301     const query = {
302       where: {
303         actorId: actor.id,
304         expiresOn: {
305           [ Sequelize.Op.lt ]: new Date()
306         }
307       }
308     }
309
310     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
311   }
312
313   static async listRemoteExpired () {
314     const actor = await getServerActor()
315
316     const query = {
317       where: {
318         actorId: {
319           [Sequelize.Op.ne]: actor.id
320         },
321         expiresOn: {
322           [ Sequelize.Op.lt ]: new Date()
323         }
324       }
325     }
326
327     return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
328   }
329
330   static async listLocalOfServer (serverId: number) {
331     const actor = await getServerActor()
332
333     const query = {
334       where: {
335         actorId: actor.id
336       },
337       include: [
338         {
339           model: VideoFileModel,
340           required: true,
341           include: [
342             {
343               model: VideoModel,
344               required: true,
345               include: [
346                 {
347                   attributes: [],
348                   model: VideoChannelModel.unscoped(),
349                   required: true,
350                   include: [
351                     {
352                       attributes: [],
353                       model: ActorModel.unscoped(),
354                       required: true,
355                       where: {
356                         serverId
357                       }
358                     }
359                   ]
360                 }
361               ]
362             }
363           ]
364         }
365       ]
366     }
367
368     return VideoRedundancyModel.findAll(query)
369   }
370
371   static async getStats (strategy: VideoRedundancyStrategy) {
372     const actor = await getServerActor()
373
374     const query = {
375       raw: true,
376       attributes: [
377         [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
378         [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ],
379         [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ]
380       ],
381       where: {
382         strategy,
383         actorId: actor.id
384       },
385       include: [
386         {
387           attributes: [],
388           model: VideoFileModel,
389           required: true
390         }
391       ]
392     }
393
394     return VideoRedundancyModel.find(query as any) // FIXME: typings
395       .then((r: any) => ({
396         totalUsed: parseInt(r.totalUsed.toString(), 10),
397         totalVideos: r.totalVideos,
398         totalVideoFiles: r.totalVideoFiles
399       }))
400   }
401
402   toActivityPubObject (): CacheFileObject {
403     return {
404       id: this.url,
405       type: 'CacheFile' as 'CacheFile',
406       object: this.VideoFile.Video.url,
407       expires: this.expiresOn.toISOString(),
408       url: {
409         type: 'Link',
410         mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
411         mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
412         href: this.fileUrl,
413         height: this.VideoFile.resolution,
414         size: this.VideoFile.size,
415         fps: this.VideoFile.fps
416       }
417     }
418   }
419
420   // Don't include video files we already duplicated
421   private static async buildVideoFileForDuplication () {
422     const actor = await getServerActor()
423
424     const notIn = Sequelize.literal(
425       '(' +
426         `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` +
427       ')'
428     )
429
430     return {
431       attributes: [],
432       model: VideoFileModel.unscoped(),
433       required: true,
434       where: {
435         id: {
436           [ Sequelize.Op.notIn ]: notIn
437         }
438       }
439     }
440   }
441
442   private static buildServerRedundancyInclude () {
443     return {
444       attributes: [],
445       model: VideoChannelModel.unscoped(),
446       required: true,
447       include: [
448         {
449           attributes: [],
450           model: ActorModel.unscoped(),
451           required: true,
452           include: [
453             {
454               attributes: [],
455               model: ServerModel.unscoped(),
456               required: true,
457               where: {
458                 redundancyAllowed: true
459               }
460             }
461           ]
462         }
463       ]
464     }
465   }
466 }