Basic video redundancy implementation
[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 { throwIfNotValid } from '../utils'
18 import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
19 import { CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
20 import { VideoFileModel } from '../video/video-file'
21 import { isDateValid } from '../../helpers/custom-validators/misc'
22 import { getServerActor } from '../../helpers/utils'
23 import { VideoModel } from '../video/video'
24 import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
25 import { logger } from '../../helpers/logger'
26 import { CacheFileObject } from '../../../shared'
27 import { VideoChannelModel } from '../video/video-channel'
28 import { ServerModel } from '../server/server'
29 import { sample } from 'lodash'
30 import { isTestInstance } from '../../helpers/core-utils'
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 findMostViewToDuplicate (randomizedFactor: number) {
149     // On VideoModel!
150     const query = {
151       logging: !isTestInstance(),
152       limit: randomizedFactor,
153       order: [ [ 'views', 'DESC' ] ],
154       include: [
155         {
156           model: VideoFileModel.unscoped(),
157           required: true,
158           where: {
159             id: {
160               [ Sequelize.Op.notIn ]: await VideoRedundancyModel.buildExcludeIn()
161             }
162           }
163         },
164         {
165           attributes: [],
166           model: VideoChannelModel.unscoped(),
167           required: true,
168           include: [
169             {
170               attributes: [],
171               model: ActorModel.unscoped(),
172               required: true,
173               include: [
174                 {
175                   attributes: [],
176                   model: ServerModel.unscoped(),
177                   required: true,
178                   where: {
179                     redundancyAllowed: true
180                   }
181                 }
182               ]
183             }
184           ]
185         }
186       ]
187     }
188
189     const rows = await VideoModel.unscoped().findAll(query)
190
191     return sample(rows)
192   }
193
194   static async getVideoFiles (strategy: VideoRedundancyStrategy) {
195     const actor = await getServerActor()
196
197     const queryVideoFiles = {
198       logging: !isTestInstance(),
199       where: {
200         actorId: actor.id,
201         strategy
202       }
203     }
204
205     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
206                                .findAll(queryVideoFiles)
207   }
208
209   static listAllExpired () {
210     const query = {
211       logging: !isTestInstance(),
212       where: {
213         expiresOn: {
214           [Sequelize.Op.lt]: new Date()
215         }
216       }
217     }
218
219     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO)
220                                .findAll(query)
221   }
222
223   toActivityPubObject (): CacheFileObject {
224     return {
225       id: this.url,
226       type: 'CacheFile' as 'CacheFile',
227       object: this.VideoFile.Video.url,
228       expires: this.expiresOn.toISOString(),
229       url: {
230         type: 'Link',
231         mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
232         href: this.fileUrl,
233         height: this.VideoFile.resolution,
234         size: this.VideoFile.size,
235         fps: this.VideoFile.fps
236       }
237     }
238   }
239
240   private static async buildExcludeIn () {
241     const actor = await getServerActor()
242
243     return Sequelize.literal(
244       '(' +
245         `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "expiresOn" >= NOW()` +
246       ')'
247     )
248   }
249 }