]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/redundancy/video-redundancy.ts
Move config in its own file
[github/Chocobozzz/PeerTube.git] / server / models / redundancy / video-redundancy.ts
CommitLineData
c48e82b5 1import {
c48e82b5 2 AllowNull,
25378bc8 3 BeforeDestroy,
c48e82b5
C
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'
6dd9de95 18import { CONSTRAINTS_FIELDS, MIMETYPES } 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'
09209296 31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
6dd9de95 32import { CONFIG } from '../../initializers/config'
c48e82b5
C
33
34export enum ScopeNames {
35 WITH_VIDEO = 'WITH_VIDEO'
36}
37
38@Scopes({
39 [ ScopeNames.WITH_VIDEO ]: {
40 include: [
41 {
42 model: () => VideoFileModel,
09209296
C
43 required: false,
44 include: [
45 {
46 model: () => VideoModel,
47 required: true
48 }
49 ]
50 },
51 {
52 model: () => VideoStreamingPlaylistModel,
53 required: false,
c48e82b5
C
54 include: [
55 {
56 model: () => VideoModel,
57 required: true
58 }
59 ]
60 }
61 ]
62 }
63})
64
65@Table({
66 tableName: 'videoRedundancy',
67 indexes: [
68 {
69 fields: [ 'videoFileId' ]
70 },
71 {
72 fields: [ 'actorId' ]
73 },
74 {
75 fields: [ 'url' ],
76 unique: true
77 }
78 ]
79})
80export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
81
82 @CreatedAt
83 createdAt: Date
84
85 @UpdatedAt
86 updatedAt: Date
87
88 @AllowNull(false)
89 @Column
90 expiresOn: Date
91
92 @AllowNull(false)
93 @Is('VideoRedundancyFileUrl', value => throwIfNotValid(value, isUrlValid, 'fileUrl'))
94 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
95 fileUrl: string
96
97 @AllowNull(false)
98 @Is('VideoRedundancyUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
99 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
100 url: string
101
102 @AllowNull(true)
103 @Column
104 strategy: string // Only used by us
105
106 @ForeignKey(() => VideoFileModel)
107 @Column
108 videoFileId: number
109
110 @BelongsTo(() => VideoFileModel, {
111 foreignKey: {
09209296 112 allowNull: true
c48e82b5
C
113 },
114 onDelete: 'cascade'
115 })
116 VideoFile: VideoFileModel
117
09209296
C
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
c48e82b5
C
130 @ForeignKey(() => ActorModel)
131 @Column
132 actorId: number
133
134 @BelongsTo(() => ActorModel, {
135 foreignKey: {
136 allowNull: false
137 },
138 onDelete: 'cascade'
139 })
140 Actor: ActorModel
141
25378bc8
C
142 @BeforeDestroy
143 static async removeFile (instance: VideoRedundancyModel) {
8d1fa36a 144 if (!instance.isOwned()) return
c48e82b5 145
09209296
C
146 if (instance.videoFileId) {
147 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
c48e82b5 148
09209296
C
149 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
150 logger.info('Removing duplicated video file %s.', logIdentifier)
25378bc8 151
09209296
C
152 videoFile.Video.removeFile(videoFile, true)
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 }
d0ae9490
C
165
166 return undefined
c48e82b5
C
167 }
168
46f8d69b
C
169 static async loadLocalByFileId (videoFileId: number) {
170 const actor = await getServerActor()
171
c48e82b5
C
172 const query = {
173 where: {
46f8d69b 174 actorId: actor.id,
c48e82b5
C
175 videoFileId
176 }
177 }
178
179 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
180 }
181
09209296
C
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
e5565833 195 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
c48e82b5
C
196 const query = {
197 where: {
198 url
e5565833
C
199 },
200 transaction
c48e82b5
C
201 }
202
203 return VideoRedundancyModel.findOne(query)
204 }
205
5ce1208a
C
206 static async isLocalByVideoUUIDExists (uuid: string) {
207 const actor = await getServerActor()
208
209 const query = {
210 raw: true,
211 attributes: [ 'id' ],
212 where: {
213 actorId: actor.id
214 },
215 include: [
216 {
217 attributes: [ ],
218 model: VideoFileModel,
219 required: true,
220 include: [
221 {
222 attributes: [ ],
223 model: VideoModel,
224 required: true,
225 where: {
226 uuid
227 }
228 }
229 ]
230 }
231 ]
232 }
233
234 return VideoRedundancyModel.findOne(query)
235 .then(r => !!r)
236 }
237
3f6b6a56
C
238 static async getVideoSample (p: Bluebird<VideoModel[]>) {
239 const rows = await p
b36f41ca
C
240 const ids = rows.map(r => r.id)
241 const id = sample(ids)
242
09209296 243 return VideoModel.loadWithFiles(id, undefined, !isTestInstance())
b36f41ca
C
244 }
245
c48e82b5
C
246 static async findMostViewToDuplicate (randomizedFactor: number) {
247 // On VideoModel!
248 const query = {
b36f41ca 249 attributes: [ 'id', 'views' ],
c48e82b5 250 limit: randomizedFactor,
b36f41ca 251 order: getVideoSort('-views'),
4a08f669
C
252 where: {
253 privacy: VideoPrivacy.PUBLIC
254 },
c48e82b5 255 include: [
b36f41ca
C
256 await VideoRedundancyModel.buildVideoFileForDuplication(),
257 VideoRedundancyModel.buildServerRedundancyInclude()
258 ]
259 }
260
3f6b6a56 261 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
b36f41ca
C
262 }
263
264 static async findTrendingToDuplicate (randomizedFactor: number) {
265 // On VideoModel!
266 const query = {
267 attributes: [ 'id', 'views' ],
268 subQuery: false,
b36f41ca
C
269 group: 'VideoModel.id',
270 limit: randomizedFactor,
271 order: getVideoSort('-trending'),
4a08f669
C
272 where: {
273 privacy: VideoPrivacy.PUBLIC
274 },
b36f41ca
C
275 include: [
276 await VideoRedundancyModel.buildVideoFileForDuplication(),
277 VideoRedundancyModel.buildServerRedundancyInclude(),
278
279 VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
c48e82b5
C
280 ]
281 }
282
3f6b6a56
C
283 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
284 }
285
286 static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
287 // On VideoModel!
288 const query = {
289 attributes: [ 'id', 'publishedAt' ],
3f6b6a56
C
290 limit: randomizedFactor,
291 order: getVideoSort('-publishedAt'),
292 where: {
4a08f669 293 privacy: VideoPrivacy.PUBLIC,
3f6b6a56
C
294 views: {
295 [ Sequelize.Op.gte ]: minViews
296 }
297 },
298 include: [
299 await VideoRedundancyModel.buildVideoFileForDuplication(),
300 VideoRedundancyModel.buildServerRedundancyInclude()
301 ]
302 }
c48e82b5 303
3f6b6a56 304 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
c48e82b5
C
305 }
306
e5565833
C
307 static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
308 const expiredDate = new Date()
309 expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
310
311 const actor = await getServerActor()
312
313 const query = {
314 where: {
315 actorId: actor.id,
316 strategy,
317 createdAt: {
318 [ Sequelize.Op.lt ]: expiredDate
319 }
320 }
321 }
322
323 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
324 }
325
3f6b6a56 326 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
c48e82b5
C
327 const actor = await getServerActor()
328
3f6b6a56 329 const options = {
3f6b6a56
C
330 include: [
331 {
332 attributes: [],
333 model: VideoRedundancyModel,
334 required: true,
335 where: {
336 actorId: actor.id,
337 strategy
338 }
339 }
340 ]
c48e82b5
C
341 }
342
e5565833 343 return VideoFileModel.sum('size', options as any) // FIXME: typings
742ddee1
C
344 .then(v => {
345 if (!v || isNaN(v)) return 0
346
347 return v
348 })
c48e82b5
C
349 }
350
e5565833
C
351 static async listLocalExpired () {
352 const actor = await getServerActor()
353
354 const query = {
355 where: {
356 actorId: actor.id,
357 expiresOn: {
358 [ Sequelize.Op.lt ]: new Date()
359 }
360 }
361 }
362
363 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
364 }
365
366 static async listRemoteExpired () {
367 const actor = await getServerActor()
368
c48e82b5 369 const query = {
c48e82b5 370 where: {
e5565833
C
371 actorId: {
372 [Sequelize.Op.ne]: actor.id
373 },
c48e82b5 374 expiresOn: {
b36f41ca 375 [ Sequelize.Op.lt ]: new Date()
c48e82b5
C
376 }
377 }
378 }
379
e5565833 380 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
c48e82b5
C
381 }
382
161b061d
C
383 static async listLocalOfServer (serverId: number) {
384 const actor = await getServerActor()
09209296
C
385 const buildVideoInclude = () => ({
386 model: VideoModel,
387 required: true,
161b061d
C
388 include: [
389 {
09209296
C
390 attributes: [],
391 model: VideoChannelModel.unscoped(),
161b061d
C
392 required: true,
393 include: [
394 {
09209296
C
395 attributes: [],
396 model: ActorModel.unscoped(),
161b061d 397 required: true,
09209296
C
398 where: {
399 serverId
400 }
161b061d
C
401 }
402 ]
403 }
404 ]
09209296
C
405 })
406
407 const query = {
408 where: {
409 actorId: actor.id
410 },
411 include: [
412 {
413 model: VideoFileModel,
414 required: false,
415 include: [ buildVideoInclude() ]
416 },
417 {
418 model: VideoStreamingPlaylistModel,
419 required: false,
420 include: [ buildVideoInclude() ]
421 }
422 ]
161b061d
C
423 }
424
425 return VideoRedundancyModel.findAll(query)
426 }
427
4b5384f6
C
428 static async getStats (strategy: VideoRedundancyStrategy) {
429 const actor = await getServerActor()
430
431 const query = {
432 raw: true,
433 attributes: [
434 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
ebdb6124
C
435 [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ],
436 [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ]
4b5384f6
C
437 ],
438 where: {
439 strategy,
440 actorId: actor.id
441 },
442 include: [
443 {
444 attributes: [],
445 model: VideoFileModel,
446 required: true
447 }
448 ]
449 }
450
44b9c0ba 451 return VideoRedundancyModel.findOne(query as any) // FIXME: typings
4b5384f6
C
452 .then((r: any) => ({
453 totalUsed: parseInt(r.totalUsed.toString(), 10),
454 totalVideos: r.totalVideos,
455 totalVideoFiles: r.totalVideoFiles
456 }))
457 }
458
09209296
C
459 getVideo () {
460 if (this.VideoFile) return this.VideoFile.Video
461
462 return this.VideoStreamingPlaylist.Video
463 }
464
8d1fa36a
C
465 isOwned () {
466 return !!this.strategy
467 }
468
c48e82b5 469 toActivityPubObject (): CacheFileObject {
09209296
C
470 if (this.VideoStreamingPlaylist) {
471 return {
472 id: this.url,
473 type: 'CacheFile' as 'CacheFile',
474 object: this.VideoStreamingPlaylist.Video.url,
475 expires: this.expiresOn.toISOString(),
476 url: {
477 type: 'Link',
478 mimeType: 'application/x-mpegURL',
479 mediaType: 'application/x-mpegURL',
480 href: this.fileUrl
481 }
482 }
483 }
484
c48e82b5
C
485 return {
486 id: this.url,
487 type: 'CacheFile' as 'CacheFile',
488 object: this.VideoFile.Video.url,
489 expires: this.expiresOn.toISOString(),
490 url: {
491 type: 'Link',
14e2014a
C
492 mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
493 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
c48e82b5
C
494 href: this.fileUrl,
495 height: this.VideoFile.resolution,
496 size: this.VideoFile.size,
497 fps: this.VideoFile.fps
498 }
499 }
500 }
501
b36f41ca
C
502 // Don't include video files we already duplicated
503 private static async buildVideoFileForDuplication () {
c48e82b5
C
504 const actor = await getServerActor()
505
b36f41ca 506 const notIn = Sequelize.literal(
c48e82b5 507 '(' +
09209296 508 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
c48e82b5
C
509 ')'
510 )
b36f41ca
C
511
512 return {
513 attributes: [],
514 model: VideoFileModel.unscoped(),
515 required: true,
516 where: {
517 id: {
518 [ Sequelize.Op.notIn ]: notIn
519 }
520 }
521 }
522 }
523
524 private static buildServerRedundancyInclude () {
525 return {
526 attributes: [],
527 model: VideoChannelModel.unscoped(),
528 required: true,
529 include: [
530 {
531 attributes: [],
532 model: ActorModel.unscoped(),
533 required: true,
534 include: [
535 {
536 attributes: [],
537 model: ServerModel.unscoped(),
538 required: true,
539 where: {
540 redundancyAllowed: true
541 }
542 }
543 ]
544 }
545 ]
546 }
c48e82b5
C
547 }
548}