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