diff options
Diffstat (limited to 'server/models/redundancy')
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 192 |
1 files changed, 131 insertions, 61 deletions
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 8f2ef2d9a..eb2222256 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -13,9 +13,9 @@ import { | |||
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../activitypub/actor' |
16 | import { getVideoSort, throwIfNotValid } from '../utils' | 16 | import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
18 | import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers' | 18 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
19 | import { VideoFileModel } from '../video/video-file' | 19 | import { VideoFileModel } from '../video/video-file' |
20 | import { getServerActor } from '../../helpers/utils' | 20 | import { getServerActor } from '../../helpers/utils' |
21 | import { VideoModel } from '../video/video' | 21 | import { VideoModel } from '../video/video' |
@@ -27,28 +27,40 @@ import { ServerModel } from '../server/server' | |||
27 | import { sample } from 'lodash' | 27 | import { sample } from 'lodash' |
28 | import { isTestInstance } from '../../helpers/core-utils' | 28 | import { isTestInstance } from '../../helpers/core-utils' |
29 | import * as Bluebird from 'bluebird' | 29 | import * as Bluebird from 'bluebird' |
30 | import * as Sequelize from 'sequelize' | 30 | import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize' |
31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' | ||
32 | import { CONFIG } from '../../initializers/config' | ||
31 | 33 | ||
32 | export enum ScopeNames { | 34 | export enum ScopeNames { |
33 | WITH_VIDEO = 'WITH_VIDEO' | 35 | WITH_VIDEO = 'WITH_VIDEO' |
34 | } | 36 | } |
35 | 37 | ||
36 | @Scopes({ | 38 | @Scopes(() => ({ |
37 | [ ScopeNames.WITH_VIDEO ]: { | 39 | [ ScopeNames.WITH_VIDEO ]: { |
38 | include: [ | 40 | include: [ |
39 | { | 41 | { |
40 | model: () => VideoFileModel, | 42 | model: VideoFileModel, |
41 | required: true, | 43 | required: false, |
42 | include: [ | 44 | include: [ |
43 | { | 45 | { |
44 | model: () => VideoModel, | 46 | model: VideoModel, |
47 | required: true | ||
48 | } | ||
49 | ] | ||
50 | }, | ||
51 | { | ||
52 | model: VideoStreamingPlaylistModel, | ||
53 | required: false, | ||
54 | include: [ | ||
55 | { | ||
56 | model: VideoModel, | ||
45 | required: true | 57 | required: true |
46 | } | 58 | } |
47 | ] | 59 | ] |
48 | } | 60 | } |
49 | ] | 61 | ] |
50 | } | 62 | } |
51 | }) | 63 | })) |
52 | 64 | ||
53 | @Table({ | 65 | @Table({ |
54 | tableName: 'videoRedundancy', | 66 | tableName: 'videoRedundancy', |
@@ -97,12 +109,24 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
97 | 109 | ||
98 | @BelongsTo(() => VideoFileModel, { | 110 | @BelongsTo(() => VideoFileModel, { |
99 | foreignKey: { | 111 | foreignKey: { |
100 | allowNull: false | 112 | allowNull: true |
101 | }, | 113 | }, |
102 | onDelete: 'cascade' | 114 | onDelete: 'cascade' |
103 | }) | 115 | }) |
104 | VideoFile: VideoFileModel | 116 | VideoFile: VideoFileModel |
105 | 117 | ||
118 | @ForeignKey(() => VideoStreamingPlaylistModel) | ||
119 | @Column | ||
120 | videoStreamingPlaylistId: number | ||
121 | |||
122 | @BelongsTo(() => VideoStreamingPlaylistModel, { | ||
123 | foreignKey: { | ||
124 | allowNull: true | ||
125 | }, | ||
126 | onDelete: 'cascade' | ||
127 | }) | ||
128 | VideoStreamingPlaylist: VideoStreamingPlaylistModel | ||
129 | |||
106 | @ForeignKey(() => ActorModel) | 130 | @ForeignKey(() => ActorModel) |
107 | @Column | 131 | @Column |
108 | actorId: number | 132 | actorId: number |
@@ -119,13 +143,25 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
119 | static async removeFile (instance: VideoRedundancyModel) { | 143 | static async removeFile (instance: VideoRedundancyModel) { |
120 | if (!instance.isOwned()) return | 144 | if (!instance.isOwned()) return |
121 | 145 | ||
122 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | 146 | if (instance.videoFileId) { |
147 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | ||
123 | 148 | ||
124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 149 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
125 | logger.info('Removing duplicated video file %s.', logIdentifier) | 150 | logger.info('Removing duplicated video file %s.', logIdentifier) |
126 | 151 | ||
127 | videoFile.Video.removeFile(videoFile, true) | 152 | videoFile.Video.removeFile(videoFile, true) |
128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 153 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
154 | } | ||
155 | |||
156 | if (instance.videoStreamingPlaylistId) { | ||
157 | const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId) | ||
158 | |||
159 | const videoUUID = videoStreamingPlaylist.Video.uuid | ||
160 | logger.info('Removing duplicated video streaming playlist %s.', videoUUID) | ||
161 | |||
162 | videoStreamingPlaylist.Video.removeStreamingPlaylist(true) | ||
163 | .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err })) | ||
164 | } | ||
129 | 165 | ||
130 | return undefined | 166 | return undefined |
131 | } | 167 | } |
@@ -143,7 +179,20 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
143 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | 179 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) |
144 | } | 180 | } |
145 | 181 | ||
146 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 182 | static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) { |
183 | const actor = await getServerActor() | ||
184 | |||
185 | const query = { | ||
186 | where: { | ||
187 | actorId: actor.id, | ||
188 | videoStreamingPlaylistId | ||
189 | } | ||
190 | } | ||
191 | |||
192 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | ||
193 | } | ||
194 | |||
195 | static loadByUrl (url: string, transaction?: Transaction) { | ||
147 | const query = { | 196 | const query = { |
148 | where: { | 197 | where: { |
149 | url | 198 | url |
@@ -191,7 +240,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
191 | const ids = rows.map(r => r.id) | 240 | const ids = rows.map(r => r.id) |
192 | const id = sample(ids) | 241 | const id = sample(ids) |
193 | 242 | ||
194 | return VideoModel.loadWithFile(id, undefined, !isTestInstance()) | 243 | return VideoModel.loadWithFiles(id, undefined, !isTestInstance()) |
195 | } | 244 | } |
196 | 245 | ||
197 | static async findMostViewToDuplicate (randomizedFactor: number) { | 246 | static async findMostViewToDuplicate (randomizedFactor: number) { |
@@ -243,7 +292,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
243 | where: { | 292 | where: { |
244 | privacy: VideoPrivacy.PUBLIC, | 293 | privacy: VideoPrivacy.PUBLIC, |
245 | views: { | 294 | views: { |
246 | [ Sequelize.Op.gte ]: minViews | 295 | [ Op.gte ]: minViews |
247 | } | 296 | } |
248 | }, | 297 | }, |
249 | include: [ | 298 | include: [ |
@@ -266,7 +315,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
266 | actorId: actor.id, | 315 | actorId: actor.id, |
267 | strategy, | 316 | strategy, |
268 | createdAt: { | 317 | createdAt: { |
269 | [ Sequelize.Op.lt ]: expiredDate | 318 | [ Op.lt ]: expiredDate |
270 | } | 319 | } |
271 | } | 320 | } |
272 | } | 321 | } |
@@ -277,7 +326,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
277 | static async getTotalDuplicated (strategy: VideoRedundancyStrategy) { | 326 | static async getTotalDuplicated (strategy: VideoRedundancyStrategy) { |
278 | const actor = await getServerActor() | 327 | const actor = await getServerActor() |
279 | 328 | ||
280 | const options = { | 329 | const query: FindOptions = { |
281 | include: [ | 330 | include: [ |
282 | { | 331 | { |
283 | attributes: [], | 332 | attributes: [], |
@@ -291,12 +340,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
291 | ] | 340 | ] |
292 | } | 341 | } |
293 | 342 | ||
294 | return VideoFileModel.sum('size', options as any) // FIXME: typings | 343 | return VideoFileModel.aggregate('size', 'SUM', query) |
295 | .then(v => { | 344 | .then(result => parseAggregateResult(result)) |
296 | if (!v || isNaN(v)) return 0 | ||
297 | |||
298 | return v | ||
299 | }) | ||
300 | } | 345 | } |
301 | 346 | ||
302 | static async listLocalExpired () { | 347 | static async listLocalExpired () { |
@@ -306,7 +351,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
306 | where: { | 351 | where: { |
307 | actorId: actor.id, | 352 | actorId: actor.id, |
308 | expiresOn: { | 353 | expiresOn: { |
309 | [ Sequelize.Op.lt ]: new Date() | 354 | [ Op.lt ]: new Date() |
310 | } | 355 | } |
311 | } | 356 | } |
312 | } | 357 | } |
@@ -320,10 +365,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
320 | const query = { | 365 | const query = { |
321 | where: { | 366 | where: { |
322 | actorId: { | 367 | actorId: { |
323 | [Sequelize.Op.ne]: actor.id | 368 | [Op.ne]: actor.id |
324 | }, | 369 | }, |
325 | expiresOn: { | 370 | expiresOn: { |
326 | [ Sequelize.Op.lt ]: new Date() | 371 | [ Op.lt ]: new Date() |
327 | } | 372 | } |
328 | } | 373 | } |
329 | } | 374 | } |
@@ -333,40 +378,44 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
333 | 378 | ||
334 | static async listLocalOfServer (serverId: number) { | 379 | static async listLocalOfServer (serverId: number) { |
335 | const actor = await getServerActor() | 380 | const actor = await getServerActor() |
336 | 381 | const buildVideoInclude = () => ({ | |
337 | const query = { | 382 | model: VideoModel, |
338 | where: { | 383 | required: true, |
339 | actorId: actor.id | ||
340 | }, | ||
341 | include: [ | 384 | include: [ |
342 | { | 385 | { |
343 | model: VideoFileModel, | 386 | attributes: [], |
387 | model: VideoChannelModel.unscoped(), | ||
344 | required: true, | 388 | required: true, |
345 | include: [ | 389 | include: [ |
346 | { | 390 | { |
347 | model: VideoModel, | 391 | attributes: [], |
392 | model: ActorModel.unscoped(), | ||
348 | required: true, | 393 | required: true, |
349 | include: [ | 394 | where: { |
350 | { | 395 | serverId |
351 | attributes: [], | 396 | } |
352 | model: VideoChannelModel.unscoped(), | ||
353 | required: true, | ||
354 | include: [ | ||
355 | { | ||
356 | attributes: [], | ||
357 | model: ActorModel.unscoped(), | ||
358 | required: true, | ||
359 | where: { | ||
360 | serverId | ||
361 | } | ||
362 | } | ||
363 | ] | ||
364 | } | ||
365 | ] | ||
366 | } | 397 | } |
367 | ] | 398 | ] |
368 | } | 399 | } |
369 | ] | 400 | ] |
401 | }) | ||
402 | |||
403 | const query = { | ||
404 | where: { | ||
405 | actorId: actor.id | ||
406 | }, | ||
407 | include: [ | ||
408 | { | ||
409 | model: VideoFileModel, | ||
410 | required: false, | ||
411 | include: [ buildVideoInclude() ] | ||
412 | }, | ||
413 | { | ||
414 | model: VideoStreamingPlaylistModel, | ||
415 | required: false, | ||
416 | include: [ buildVideoInclude() ] | ||
417 | } | ||
418 | ] | ||
370 | } | 419 | } |
371 | 420 | ||
372 | return VideoRedundancyModel.findAll(query) | 421 | return VideoRedundancyModel.findAll(query) |
@@ -375,12 +424,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
375 | static async getStats (strategy: VideoRedundancyStrategy) { | 424 | static async getStats (strategy: VideoRedundancyStrategy) { |
376 | const actor = await getServerActor() | 425 | const actor = await getServerActor() |
377 | 426 | ||
378 | const query = { | 427 | const query: FindOptions = { |
379 | raw: true, | 428 | raw: true, |
380 | attributes: [ | 429 | attributes: [ |
381 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ], | 430 | [ fn('COALESCE', fn('SUM', col('VideoFile.size')), '0'), 'totalUsed' ], |
382 | [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ], | 431 | [ fn('COUNT', fn('DISTINCT', col('videoId'))), 'totalVideos' ], |
383 | [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ] | 432 | [ fn('COUNT', col('videoFileId')), 'totalVideoFiles' ] |
384 | ], | 433 | ], |
385 | where: { | 434 | where: { |
386 | strategy, | 435 | strategy, |
@@ -395,19 +444,40 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
395 | ] | 444 | ] |
396 | } | 445 | } |
397 | 446 | ||
398 | return VideoRedundancyModel.findOne(query as any) // FIXME: typings | 447 | return VideoRedundancyModel.findOne(query) |
399 | .then((r: any) => ({ | 448 | .then((r: any) => ({ |
400 | totalUsed: parseInt(r.totalUsed.toString(), 10), | 449 | totalUsed: parseAggregateResult(r.totalUsed), |
401 | totalVideos: r.totalVideos, | 450 | totalVideos: r.totalVideos, |
402 | totalVideoFiles: r.totalVideoFiles | 451 | totalVideoFiles: r.totalVideoFiles |
403 | })) | 452 | })) |
404 | } | 453 | } |
405 | 454 | ||
455 | getVideo () { | ||
456 | if (this.VideoFile) return this.VideoFile.Video | ||
457 | |||
458 | return this.VideoStreamingPlaylist.Video | ||
459 | } | ||
460 | |||
406 | isOwned () { | 461 | isOwned () { |
407 | return !!this.strategy | 462 | return !!this.strategy |
408 | } | 463 | } |
409 | 464 | ||
410 | toActivityPubObject (): CacheFileObject { | 465 | toActivityPubObject (): CacheFileObject { |
466 | if (this.VideoStreamingPlaylist) { | ||
467 | return { | ||
468 | id: this.url, | ||
469 | type: 'CacheFile' as 'CacheFile', | ||
470 | object: this.VideoStreamingPlaylist.Video.url, | ||
471 | expires: this.expiresOn.toISOString(), | ||
472 | url: { | ||
473 | type: 'Link', | ||
474 | mimeType: 'application/x-mpegURL', | ||
475 | mediaType: 'application/x-mpegURL', | ||
476 | href: this.fileUrl | ||
477 | } | ||
478 | } | ||
479 | } | ||
480 | |||
411 | return { | 481 | return { |
412 | id: this.url, | 482 | id: this.url, |
413 | type: 'CacheFile' as 'CacheFile', | 483 | type: 'CacheFile' as 'CacheFile', |
@@ -429,9 +499,9 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
429 | private static async buildVideoFileForDuplication () { | 499 | private static async buildVideoFileForDuplication () { |
430 | const actor = await getServerActor() | 500 | const actor = await getServerActor() |
431 | 501 | ||
432 | const notIn = Sequelize.literal( | 502 | const notIn = literal( |
433 | '(' + | 503 | '(' + |
434 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` + | 504 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + |
435 | ')' | 505 | ')' |
436 | ) | 506 | ) |
437 | 507 | ||
@@ -441,7 +511,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
441 | required: true, | 511 | required: true, |
442 | where: { | 512 | where: { |
443 | id: { | 513 | id: { |
444 | [ Sequelize.Op.notIn ]: notIn | 514 | [ Op.notIn ]: notIn |
445 | } | 515 | } |
446 | } | 516 | } |
447 | } | 517 | } |