Reduce video.ts file size by moving some methods in other files
[oweals/peertube.git] / server / models / redundancy / video-redundancy.ts
1 import {
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'
16 import { ActorModel } from '../activitypub/actor'
17 import { getVideoSort, throwIfNotValid } from '../utils'
18 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
19 import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
20 import { VideoFileModel } from '../video/video-file'
21 import { getServerActor } from '../../helpers/utils'
22 import { VideoModel } from '../video/video'
23 import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
24 import { logger } from '../../helpers/logger'
25 import { CacheFileObject } from '../../../shared'
26 import { VideoChannelModel } from '../video/video-channel'
27 import { ServerModel } from '../server/server'
28 import { sample } from 'lodash'
29 import { isTestInstance } from '../../helpers/core-utils'
30 import * as Bluebird from 'bluebird'
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   @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
148   static async getVideoSample (p: Bluebird<VideoModel[]>) {
149     const rows = await p
150     const ids = rows.map(r => r.id)
151     const id = sample(ids)
152
153     return VideoModel.loadWithFile(id, undefined, !isTestInstance())
154   }
155
156   static async findMostViewToDuplicate (randomizedFactor: number) {
157     // On VideoModel!
158     const query = {
159       attributes: [ 'id', 'views' ],
160       logging: !isTestInstance(),
161       limit: randomizedFactor,
162       order: getVideoSort('-views'),
163       include: [
164         await VideoRedundancyModel.buildVideoFileForDuplication(),
165         VideoRedundancyModel.buildServerRedundancyInclude()
166       ]
167     }
168
169     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
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)
186       ]
187     }
188
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' ],
196       logging: !isTestInstance(),
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     }
209
210     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
211   }
212
213   static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
214     const actor = await getServerActor()
215
216     const options = {
217       logging: !isTestInstance(),
218       include: [
219         {
220           attributes: [],
221           model: VideoRedundancyModel,
222           required: true,
223           where: {
224             actorId: actor.id,
225             strategy
226           }
227         }
228       ]
229     }
230
231     return VideoFileModel.sum('size', options)
232   }
233
234   static listAllExpired () {
235     const query = {
236       logging: !isTestInstance(),
237       where: {
238         expiresOn: {
239           [ Sequelize.Op.lt ]: new Date()
240         }
241       }
242     }
243
244     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
245                                .findAll(query)
246   }
247
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
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
296   // Don't include video files we already duplicated
297   private static async buildVideoFileForDuplication () {
298     const actor = await getServerActor()
299
300     const notIn = Sequelize.literal(
301       '(' +
302         `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "expiresOn" >= NOW()` +
303       ')'
304     )
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     }
341   }
342 }