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