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