]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/redundancy/video-redundancy.ts
Use random names for VOD HLS playlists
[github/Chocobozzz/PeerTube.git] / server / models / redundancy / video-redundancy.ts
CommitLineData
b49f22d8 1import { sample } from 'lodash'
6c8a99d1 2import { literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize'
c48e82b5 3import {
c48e82b5 4 AllowNull,
25378bc8 5 BeforeDestroy,
c48e82b5
C
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 ForeignKey,
11 Is,
12 Model,
13 Scopes,
c48e82b5
C
14 Table,
15 UpdatedAt
16} from 'sequelize-typescript'
b49f22d8 17import { getServerActor } from '@server/models/application/application'
7448551f 18import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models'
16c016e8 19import { AttributesOnly } from '@shared/core-utils'
b764380a
C
20import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model'
21import {
22 FileRedundancyInformation,
23 StreamingPlaylistRedundancyInformation,
24 VideoRedundancy
25} from '@shared/models/redundancy/video-redundancy.model'
b49f22d8
C
26import { CacheFileObject, VideoPrivacy } from '../../../shared'
27import { VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../shared/models/redundancy'
28import { isTestInstance } from '../../helpers/core-utils'
29import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
30import { logger } from '../../helpers/logger'
31import { CONFIG } from '../../initializers/config'
32import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
7d9ba5c0 33import { ActorModel } from '../actor/actor'
b49f22d8
C
34import { ServerModel } from '../server/server'
35import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
93544419 36import { ScheduleVideoUpdateModel } from '../video/schedule-video-update'
b49f22d8
C
37import { VideoModel } from '../video/video'
38import { VideoChannelModel } from '../video/video-channel'
39import { VideoFileModel } from '../video/video-file'
40import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
c48e82b5
C
41
42export enum ScopeNames {
43 WITH_VIDEO = 'WITH_VIDEO'
44}
45
3acc5084 46@Scopes(() => ({
a1587156 47 [ScopeNames.WITH_VIDEO]: {
c48e82b5
C
48 include: [
49 {
3acc5084 50 model: VideoFileModel,
09209296
C
51 required: false,
52 include: [
53 {
3acc5084 54 model: VideoModel,
09209296
C
55 required: true
56 }
57 ]
58 },
59 {
3acc5084 60 model: VideoStreamingPlaylistModel,
09209296 61 required: false,
c48e82b5
C
62 include: [
63 {
3acc5084 64 model: VideoModel,
c48e82b5
C
65 required: true
66 }
67 ]
68 }
3acc5084 69 ]
c48e82b5 70 }
3acc5084 71}))
c48e82b5
C
72
73@Table({
74 tableName: 'videoRedundancy',
75 indexes: [
76 {
77 fields: [ 'videoFileId' ]
78 },
79 {
80 fields: [ 'actorId' ]
81 },
a7944e89
C
82 {
83 fields: [ 'expiresOn' ]
84 },
c48e82b5
C
85 {
86 fields: [ 'url' ],
87 unique: true
88 }
89 ]
90})
16c016e8 91export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedundancyModel>>> {
c48e82b5
C
92
93 @CreatedAt
94 createdAt: Date
95
96 @UpdatedAt
97 updatedAt: Date
98
b764380a 99 @AllowNull(true)
c48e82b5
C
100 @Column
101 expiresOn: Date
102
103 @AllowNull(false)
104 @Is('VideoRedundancyFileUrl', value => throwIfNotValid(value, isUrlValid, 'fileUrl'))
105 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
106 fileUrl: string
107
108 @AllowNull(false)
109 @Is('VideoRedundancyUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
110 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
111 url: string
112
113 @AllowNull(true)
114 @Column
115 strategy: string // Only used by us
116
117 @ForeignKey(() => VideoFileModel)
118 @Column
119 videoFileId: number
120
121 @BelongsTo(() => VideoFileModel, {
122 foreignKey: {
09209296 123 allowNull: true
c48e82b5
C
124 },
125 onDelete: 'cascade'
126 })
127 VideoFile: VideoFileModel
128
09209296
C
129 @ForeignKey(() => VideoStreamingPlaylistModel)
130 @Column
131 videoStreamingPlaylistId: number
132
133 @BelongsTo(() => VideoStreamingPlaylistModel, {
134 foreignKey: {
135 allowNull: true
136 },
137 onDelete: 'cascade'
138 })
139 VideoStreamingPlaylist: VideoStreamingPlaylistModel
140
c48e82b5
C
141 @ForeignKey(() => ActorModel)
142 @Column
143 actorId: number
144
145 @BelongsTo(() => ActorModel, {
146 foreignKey: {
147 allowNull: false
148 },
149 onDelete: 'cascade'
150 })
151 Actor: ActorModel
152
25378bc8
C
153 @BeforeDestroy
154 static async removeFile (instance: VideoRedundancyModel) {
8d1fa36a 155 if (!instance.isOwned()) return
c48e82b5 156
09209296
C
157 if (instance.videoFileId) {
158 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
c48e82b5 159
09209296
C
160 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
161 logger.info('Removing duplicated video file %s.', logIdentifier)
25378bc8 162
764b1a14
C
163 videoFile.Video.removeFileAndTorrent(videoFile, true)
164 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
09209296
C
165 }
166
167 if (instance.videoStreamingPlaylistId) {
168 const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId)
169
170 const videoUUID = videoStreamingPlaylist.Video.uuid
171 logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
172
ffc65cbd 173 videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true)
a1587156 174 .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
09209296 175 }
d0ae9490
C
176
177 return undefined
c48e82b5
C
178 }
179
453e83ea 180 static async loadLocalByFileId (videoFileId: number): Promise<MVideoRedundancyVideo> {
46f8d69b
C
181 const actor = await getServerActor()
182
c48e82b5
C
183 const query = {
184 where: {
46f8d69b 185 actorId: actor.id,
c48e82b5
C
186 videoFileId
187 }
188 }
189
190 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
191 }
192
89613cb4
C
193 static async listLocalByVideoId (videoId: number): Promise<MVideoRedundancyVideo[]> {
194 const actor = await getServerActor()
195
196 const queryStreamingPlaylist = {
197 where: {
198 actorId: actor.id
199 },
200 include: [
201 {
202 model: VideoStreamingPlaylistModel.unscoped(),
203 required: true,
204 include: [
205 {
206 model: VideoModel.unscoped(),
207 required: true,
208 where: {
209 id: videoId
210 }
211 }
212 ]
213 }
214 ]
215 }
216
217 const queryFiles = {
218 where: {
219 actorId: actor.id
220 },
221 include: [
222 {
223 model: VideoFileModel,
224 required: true,
225 include: [
226 {
227 model: VideoModel,
228 required: true,
229 where: {
230 id: videoId
231 }
232 }
233 ]
234 }
235 ]
236 }
237
238 return Promise.all([
239 VideoRedundancyModel.findAll(queryStreamingPlaylist),
240 VideoRedundancyModel.findAll(queryFiles)
241 ]).then(([ r1, r2 ]) => r1.concat(r2))
242 }
243
453e83ea 244 static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number): Promise<MVideoRedundancyVideo> {
09209296
C
245 const actor = await getServerActor()
246
247 const query = {
248 where: {
249 actorId: actor.id,
250 videoStreamingPlaylistId
251 }
252 }
253
254 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
255 }
256
b49f22d8 257 static loadByIdWithVideo (id: number, transaction?: Transaction): Promise<MVideoRedundancyVideo> {
b764380a
C
258 const query = {
259 where: { id },
260 transaction
261 }
262
263 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
264 }
265
b49f22d8 266 static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoRedundancy> {
c48e82b5
C
267 const query = {
268 where: {
269 url
e5565833
C
270 },
271 transaction
c48e82b5
C
272 }
273
274 return VideoRedundancyModel.findOne(query)
275 }
276
5ce1208a
C
277 static async isLocalByVideoUUIDExists (uuid: string) {
278 const actor = await getServerActor()
279
280 const query = {
281 raw: true,
282 attributes: [ 'id' ],
283 where: {
284 actorId: actor.id
285 },
286 include: [
287 {
a1587156 288 attributes: [],
5ce1208a
C
289 model: VideoFileModel,
290 required: true,
291 include: [
292 {
a1587156 293 attributes: [],
5ce1208a
C
294 model: VideoModel,
295 required: true,
296 where: {
297 uuid
298 }
299 }
300 ]
301 }
302 ]
303 }
304
305 return VideoRedundancyModel.findOne(query)
a1587156 306 .then(r => !!r)
5ce1208a
C
307 }
308
b49f22d8 309 static async getVideoSample (p: Promise<VideoModel[]>) {
3f6b6a56 310 const rows = await p
9e3e3617
C
311 if (rows.length === 0) return undefined
312
b36f41ca
C
313 const ids = rows.map(r => r.id)
314 const id = sample(ids)
315
09209296 316 return VideoModel.loadWithFiles(id, undefined, !isTestInstance())
b36f41ca
C
317 }
318
c48e82b5 319 static async findMostViewToDuplicate (randomizedFactor: number) {
7448551f
C
320 const peertubeActor = await getServerActor()
321
c48e82b5
C
322 // On VideoModel!
323 const query = {
b36f41ca 324 attributes: [ 'id', 'views' ],
c48e82b5 325 limit: randomizedFactor,
b36f41ca 326 order: getVideoSort('-views'),
4a08f669 327 where: {
9e2b2e76 328 privacy: VideoPrivacy.PUBLIC,
7448551f
C
329 isLive: false,
330 ...this.buildVideoIdsForDuplication(peertubeActor)
4a08f669 331 },
c48e82b5 332 include: [
b36f41ca
C
333 VideoRedundancyModel.buildServerRedundancyInclude()
334 ]
335 }
336
3f6b6a56 337 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
b36f41ca
C
338 }
339
340 static async findTrendingToDuplicate (randomizedFactor: number) {
7448551f
C
341 const peertubeActor = await getServerActor()
342
b36f41ca
C
343 // On VideoModel!
344 const query = {
345 attributes: [ 'id', 'views' ],
346 subQuery: false,
b36f41ca
C
347 group: 'VideoModel.id',
348 limit: randomizedFactor,
349 order: getVideoSort('-trending'),
4a08f669 350 where: {
9e2b2e76 351 privacy: VideoPrivacy.PUBLIC,
7448551f
C
352 isLive: false,
353 ...this.buildVideoIdsForDuplication(peertubeActor)
4a08f669 354 },
b36f41ca 355 include: [
b36f41ca
C
356 VideoRedundancyModel.buildServerRedundancyInclude(),
357
358 VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
c48e82b5
C
359 ]
360 }
361
3f6b6a56
C
362 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
363 }
364
365 static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
7448551f
C
366 const peertubeActor = await getServerActor()
367
3f6b6a56
C
368 // On VideoModel!
369 const query = {
370 attributes: [ 'id', 'publishedAt' ],
3f6b6a56
C
371 limit: randomizedFactor,
372 order: getVideoSort('-publishedAt'),
373 where: {
4a08f669 374 privacy: VideoPrivacy.PUBLIC,
9e2b2e76 375 isLive: false,
3f6b6a56 376 views: {
a1587156 377 [Op.gte]: minViews
7448551f
C
378 },
379 ...this.buildVideoIdsForDuplication(peertubeActor)
3f6b6a56
C
380 },
381 include: [
93544419
C
382 VideoRedundancyModel.buildServerRedundancyInclude(),
383
384 // Required by publishedAt sort
385 {
386 model: ScheduleVideoUpdateModel.unscoped(),
387 required: false
388 }
3f6b6a56
C
389 ]
390 }
c48e82b5 391
3f6b6a56 392 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
c48e82b5
C
393 }
394
453e83ea 395 static async loadOldestLocalExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number): Promise<MVideoRedundancyVideo> {
e5565833
C
396 const expiredDate = new Date()
397 expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
398
399 const actor = await getServerActor()
400
401 const query = {
402 where: {
403 actorId: actor.id,
404 strategy,
405 createdAt: {
a1587156 406 [Op.lt]: expiredDate
e5565833
C
407 }
408 }
409 }
410
411 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
412 }
413
e5565833
C
414 static async listLocalExpired () {
415 const actor = await getServerActor()
416
417 const query = {
418 where: {
419 actorId: actor.id,
420 expiresOn: {
a1587156 421 [Op.lt]: new Date()
e5565833
C
422 }
423 }
424 }
425
426 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
427 }
428
429 static async listRemoteExpired () {
430 const actor = await getServerActor()
431
c48e82b5 432 const query = {
c48e82b5 433 where: {
e5565833 434 actorId: {
3acc5084 435 [Op.ne]: actor.id
e5565833 436 },
c48e82b5 437 expiresOn: {
a1587156
C
438 [Op.lt]: new Date(),
439 [Op.ne]: null
c48e82b5
C
440 }
441 }
442 }
443
e5565833 444 return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
c48e82b5
C
445 }
446
161b061d
C
447 static async listLocalOfServer (serverId: number) {
448 const actor = await getServerActor()
09209296
C
449 const buildVideoInclude = () => ({
450 model: VideoModel,
451 required: true,
161b061d
C
452 include: [
453 {
09209296
C
454 attributes: [],
455 model: VideoChannelModel.unscoped(),
161b061d
C
456 required: true,
457 include: [
458 {
09209296
C
459 attributes: [],
460 model: ActorModel.unscoped(),
161b061d 461 required: true,
09209296
C
462 where: {
463 serverId
464 }
161b061d
C
465 }
466 ]
467 }
468 ]
09209296
C
469 })
470
471 const query = {
472 where: {
473 actorId: actor.id
474 },
475 include: [
476 {
477 model: VideoFileModel,
478 required: false,
479 include: [ buildVideoInclude() ]
480 },
481 {
482 model: VideoStreamingPlaylistModel,
483 required: false,
484 include: [ buildVideoInclude() ]
485 }
486 ]
161b061d
C
487 }
488
489 return VideoRedundancyModel.findAll(query)
490 }
491
b764380a 492 static listForApi (options: {
a1587156
C
493 start: number
494 count: number
495 sort: string
496 target: VideoRedundanciesTarget
b764380a
C
497 strategy?: string
498 }) {
499 const { start, count, sort, target, strategy } = options
a1587156
C
500 const redundancyWhere: WhereOptions = {}
501 const videosWhere: WhereOptions = {}
b764380a
C
502 let redundancySqlSuffix = ''
503
504 if (target === 'my-videos') {
505 Object.assign(videosWhere, { remote: false })
506 } else if (target === 'remote-videos') {
507 Object.assign(videosWhere, { remote: true })
508 Object.assign(redundancyWhere, { strategy: { [Op.ne]: null } })
509 redundancySqlSuffix = ' AND "videoRedundancy"."strategy" IS NOT NULL'
510 }
511
512 if (strategy) {
513 Object.assign(redundancyWhere, { strategy: strategy })
514 }
515
516 const videoFilterWhere = {
517 [Op.and]: [
518 {
a1587156 519 [Op.or]: [
b764380a
C
520 {
521 id: {
a1587156 522 [Op.in]: literal(
b764380a
C
523 '(' +
524 'SELECT "videoId" FROM "videoFile" ' +
525 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoFileId" = "videoFile".id' +
526 redundancySqlSuffix +
527 ')'
528 )
529 }
530 },
531 {
532 id: {
a1587156 533 [Op.in]: literal(
b764380a
C
534 '(' +
535 'select "videoId" FROM "videoStreamingPlaylist" ' +
536 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' +
537 redundancySqlSuffix +
538 ')'
539 )
540 }
541 }
542 ]
543 },
544
545 videosWhere
546 ]
547 }
548
549 // /!\ On video model /!\
550 const findOptions = {
551 offset: start,
552 limit: count,
553 order: getSort(sort),
554 include: [
555 {
556 required: false,
8319d6ae 557 model: VideoFileModel,
b764380a
C
558 include: [
559 {
560 model: VideoRedundancyModel.unscoped(),
561 required: false,
562 where: redundancyWhere
563 }
564 ]
565 },
566 {
567 required: false,
568 model: VideoStreamingPlaylistModel.unscoped(),
569 include: [
570 {
571 model: VideoRedundancyModel.unscoped(),
572 required: false,
573 where: redundancyWhere
574 },
575 {
8319d6ae 576 model: VideoFileModel,
b764380a
C
577 required: false
578 }
579 ]
580 }
581 ],
582 where: videoFilterWhere
583 }
584
585 // /!\ On video model /!\
586 const countOptions = {
587 where: videoFilterWhere
588 }
589
590 return Promise.all([
591 VideoModel.findAll(findOptions),
592
593 VideoModel.count(countOptions)
594 ]).then(([ data, total ]) => ({ total, data }))
595 }
596
597 static async getStats (strategy: VideoRedundancyStrategyWithManual) {
4b5384f6
C
598 const actor = await getServerActor()
599
7448551f
C
600 const sql = `WITH "tmp" AS ` +
601 `(` +
602 `SELECT "videoFile"."size" AS "videoFileSize", "videoStreamingFile"."size" AS "videoStreamingFileSize", ` +
603 `"videoFile"."videoId" AS "videoFileVideoId", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
604 `FROM "videoRedundancy" AS "videoRedundancy" ` +
605 `LEFT JOIN "videoFile" AS "videoFile" ON "videoRedundancy"."videoFileId" = "videoFile"."id" ` +
606 `LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
607 `LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
608 `ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
609 `WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
610 `), ` +
611 `"videoIds" AS (` +
612 `SELECT "videoFileVideoId" AS "videoId" FROM "tmp" ` +
613 `UNION SELECT "videoStreamingVideoId" AS "videoId" FROM "tmp" ` +
614 `) ` +
615 `SELECT ` +
616 `COALESCE(SUM("videoFileSize"), '0') + COALESCE(SUM("videoStreamingFileSize"), '0') AS "totalUsed", ` +
617 `(SELECT COUNT("videoIds"."videoId") FROM "videoIds") AS "totalVideos", ` +
618 `COUNT(*) AS "totalVideoFiles" ` +
619 `FROM "tmp"`
620
621 return VideoRedundancyModel.sequelize.query<any>(sql, {
622 replacements: { strategy, actorId: actor.id },
623 type: QueryTypes.SELECT
624 }).then(([ row ]) => ({
625 totalUsed: parseAggregateResult(row.totalUsed),
626 totalVideos: row.totalVideos,
627 totalVideoFiles: row.totalVideoFiles
628 }))
4b5384f6
C
629 }
630
b764380a 631 static toFormattedJSONStatic (video: MVideoForRedundancyAPI): VideoRedundancy {
a1587156
C
632 const filesRedundancies: FileRedundancyInformation[] = []
633 const streamingPlaylistsRedundancies: StreamingPlaylistRedundancyInformation[] = []
b764380a
C
634
635 for (const file of video.VideoFiles) {
636 for (const redundancy of file.RedundancyVideos) {
637 filesRedundancies.push({
638 id: redundancy.id,
639 fileUrl: redundancy.fileUrl,
640 strategy: redundancy.strategy,
641 createdAt: redundancy.createdAt,
642 updatedAt: redundancy.updatedAt,
643 expiresOn: redundancy.expiresOn,
644 size: file.size
645 })
646 }
647 }
648
649 for (const playlist of video.VideoStreamingPlaylists) {
650 const size = playlist.VideoFiles.reduce((a, b) => a + b.size, 0)
651
652 for (const redundancy of playlist.RedundancyVideos) {
653 streamingPlaylistsRedundancies.push({
654 id: redundancy.id,
655 fileUrl: redundancy.fileUrl,
656 strategy: redundancy.strategy,
657 createdAt: redundancy.createdAt,
658 updatedAt: redundancy.updatedAt,
659 expiresOn: redundancy.expiresOn,
660 size
661 })
662 }
663 }
664
665 return {
666 id: video.id,
667 name: video.name,
668 url: video.url,
669 uuid: video.uuid,
670
671 redundancies: {
672 files: filesRedundancies,
673 streamingPlaylists: streamingPlaylistsRedundancies
674 }
675 }
676 }
677
09209296 678 getVideo () {
2e1e4af0 679 if (this.VideoFile?.Video) return this.VideoFile.Video
09209296 680
2e1e4af0 681 if (this.VideoStreamingPlaylist?.Video) return this.VideoStreamingPlaylist.Video
9cfeb3cf
C
682
683 return undefined
09209296
C
684 }
685
8d1fa36a
C
686 isOwned () {
687 return !!this.strategy
688 }
689
b5fecbf4 690 toActivityPubObject (this: MVideoRedundancyAP): CacheFileObject {
09209296
C
691 if (this.VideoStreamingPlaylist) {
692 return {
693 id: this.url,
694 type: 'CacheFile' as 'CacheFile',
695 object: this.VideoStreamingPlaylist.Video.url,
b764380a 696 expires: this.expiresOn ? this.expiresOn.toISOString() : null,
09209296
C
697 url: {
698 type: 'Link',
09209296
C
699 mediaType: 'application/x-mpegURL',
700 href: this.fileUrl
701 }
702 }
703 }
704
c48e82b5
C
705 return {
706 id: this.url,
707 type: 'CacheFile' as 'CacheFile',
708 object: this.VideoFile.Video.url,
b764380a 709 expires: this.expiresOn ? this.expiresOn.toISOString() : null,
c48e82b5
C
710 url: {
711 type: 'Link',
a1587156 712 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[this.VideoFile.extname] as any,
c48e82b5
C
713 href: this.fileUrl,
714 height: this.VideoFile.resolution,
715 size: this.VideoFile.size,
716 fps: this.VideoFile.fps
717 }
718 }
719 }
720
b36f41ca 721 // Don't include video files we already duplicated
7448551f 722 private static buildVideoIdsForDuplication (peertubeActor: MActor) {
3acc5084 723 const notIn = literal(
c48e82b5 724 '(' +
7448551f
C
725 `SELECT "videoFile"."videoId" AS "videoId" FROM "videoRedundancy" ` +
726 `INNER JOIN "videoFile" ON "videoFile"."id" = "videoRedundancy"."videoFileId" ` +
727 `WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
728 `UNION ` +
729 `SELECT "videoStreamingPlaylist"."videoId" AS "videoId" FROM "videoRedundancy" ` +
730 `INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoRedundancy"."videoStreamingPlaylistId" ` +
731 `WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
c48e82b5
C
732 ')'
733 )
b36f41ca
C
734
735 return {
7448551f
C
736 id: {
737 [Op.notIn]: notIn
b36f41ca
C
738 }
739 }
740 }
741
742 private static buildServerRedundancyInclude () {
743 return {
744 attributes: [],
745 model: VideoChannelModel.unscoped(),
746 required: true,
747 include: [
748 {
749 attributes: [],
750 model: ActorModel.unscoped(),
751 required: true,
752 include: [
753 {
754 attributes: [],
755 model: ServerModel.unscoped(),
756 required: true,
757 where: {
758 redundancyAllowed: true
759 }
760 }
761 ]
762 }
763 ]
764 }
c48e82b5
C
765 }
766}