]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/redundancy/video-redundancy.ts
Move zxx to its own group in select-languages component (#4664)
[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'
6b5f72be 19import { AttributesOnly } from '@shared/typescript-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
b46cf4b9 163 videoFile.Video.removeWebTorrentFileAndTorrent(videoFile, true)
764b1a14 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
52b1fd15 414 static async listLocalExpired (): Promise<MVideoRedundancyVideo[]> {
e5565833
C
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: {
7b6cf83e
C
473 [Op.and]: [
474 {
475 actorId: actor.id
476 },
477 {
478 [Op.or]: [
479 {
480 '$VideoStreamingPlaylist.id$': {
481 [Op.ne]: null
482 }
483 },
484 {
485 '$VideoFile.id$': {
486 [Op.ne]: null
487 }
488 }
489 ]
490 }
491 ]
09209296
C
492 },
493 include: [
494 {
7b6cf83e 495 model: VideoFileModel.unscoped(),
09209296
C
496 required: false,
497 include: [ buildVideoInclude() ]
498 },
499 {
7b6cf83e 500 model: VideoStreamingPlaylistModel.unscoped(),
09209296
C
501 required: false,
502 include: [ buildVideoInclude() ]
503 }
504 ]
161b061d
C
505 }
506
507 return VideoRedundancyModel.findAll(query)
508 }
509
b764380a 510 static listForApi (options: {
a1587156
C
511 start: number
512 count: number
513 sort: string
514 target: VideoRedundanciesTarget
b764380a
C
515 strategy?: string
516 }) {
517 const { start, count, sort, target, strategy } = options
a1587156
C
518 const redundancyWhere: WhereOptions = {}
519 const videosWhere: WhereOptions = {}
b764380a
C
520 let redundancySqlSuffix = ''
521
522 if (target === 'my-videos') {
523 Object.assign(videosWhere, { remote: false })
524 } else if (target === 'remote-videos') {
525 Object.assign(videosWhere, { remote: true })
526 Object.assign(redundancyWhere, { strategy: { [Op.ne]: null } })
527 redundancySqlSuffix = ' AND "videoRedundancy"."strategy" IS NOT NULL'
528 }
529
530 if (strategy) {
531 Object.assign(redundancyWhere, { strategy: strategy })
532 }
533
534 const videoFilterWhere = {
535 [Op.and]: [
536 {
a1587156 537 [Op.or]: [
b764380a
C
538 {
539 id: {
a1587156 540 [Op.in]: literal(
b764380a
C
541 '(' +
542 'SELECT "videoId" FROM "videoFile" ' +
543 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoFileId" = "videoFile".id' +
544 redundancySqlSuffix +
545 ')'
546 )
547 }
548 },
549 {
550 id: {
a1587156 551 [Op.in]: literal(
b764380a
C
552 '(' +
553 'select "videoId" FROM "videoStreamingPlaylist" ' +
554 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' +
555 redundancySqlSuffix +
556 ')'
557 )
558 }
559 }
560 ]
561 },
562
563 videosWhere
564 ]
565 }
566
567 // /!\ On video model /!\
568 const findOptions = {
569 offset: start,
570 limit: count,
571 order: getSort(sort),
572 include: [
573 {
574 required: false,
8319d6ae 575 model: VideoFileModel,
b764380a
C
576 include: [
577 {
578 model: VideoRedundancyModel.unscoped(),
579 required: false,
580 where: redundancyWhere
581 }
582 ]
583 },
584 {
585 required: false,
586 model: VideoStreamingPlaylistModel.unscoped(),
587 include: [
588 {
589 model: VideoRedundancyModel.unscoped(),
590 required: false,
591 where: redundancyWhere
592 },
593 {
8319d6ae 594 model: VideoFileModel,
b764380a
C
595 required: false
596 }
597 ]
598 }
599 ],
600 where: videoFilterWhere
601 }
602
603 // /!\ On video model /!\
604 const countOptions = {
605 where: videoFilterWhere
606 }
607
608 return Promise.all([
609 VideoModel.findAll(findOptions),
610
611 VideoModel.count(countOptions)
612 ]).then(([ data, total ]) => ({ total, data }))
613 }
614
615 static async getStats (strategy: VideoRedundancyStrategyWithManual) {
4b5384f6
C
616 const actor = await getServerActor()
617
7448551f
C
618 const sql = `WITH "tmp" AS ` +
619 `(` +
620 `SELECT "videoFile"."size" AS "videoFileSize", "videoStreamingFile"."size" AS "videoStreamingFileSize", ` +
621 `"videoFile"."videoId" AS "videoFileVideoId", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
622 `FROM "videoRedundancy" AS "videoRedundancy" ` +
623 `LEFT JOIN "videoFile" AS "videoFile" ON "videoRedundancy"."videoFileId" = "videoFile"."id" ` +
624 `LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
625 `LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
626 `ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
627 `WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
628 `), ` +
629 `"videoIds" AS (` +
630 `SELECT "videoFileVideoId" AS "videoId" FROM "tmp" ` +
631 `UNION SELECT "videoStreamingVideoId" AS "videoId" FROM "tmp" ` +
632 `) ` +
633 `SELECT ` +
634 `COALESCE(SUM("videoFileSize"), '0') + COALESCE(SUM("videoStreamingFileSize"), '0') AS "totalUsed", ` +
635 `(SELECT COUNT("videoIds"."videoId") FROM "videoIds") AS "totalVideos", ` +
636 `COUNT(*) AS "totalVideoFiles" ` +
637 `FROM "tmp"`
638
639 return VideoRedundancyModel.sequelize.query<any>(sql, {
640 replacements: { strategy, actorId: actor.id },
641 type: QueryTypes.SELECT
642 }).then(([ row ]) => ({
643 totalUsed: parseAggregateResult(row.totalUsed),
644 totalVideos: row.totalVideos,
645 totalVideoFiles: row.totalVideoFiles
646 }))
4b5384f6
C
647 }
648
b764380a 649 static toFormattedJSONStatic (video: MVideoForRedundancyAPI): VideoRedundancy {
a1587156
C
650 const filesRedundancies: FileRedundancyInformation[] = []
651 const streamingPlaylistsRedundancies: StreamingPlaylistRedundancyInformation[] = []
b764380a
C
652
653 for (const file of video.VideoFiles) {
654 for (const redundancy of file.RedundancyVideos) {
655 filesRedundancies.push({
656 id: redundancy.id,
657 fileUrl: redundancy.fileUrl,
658 strategy: redundancy.strategy,
659 createdAt: redundancy.createdAt,
660 updatedAt: redundancy.updatedAt,
661 expiresOn: redundancy.expiresOn,
662 size: file.size
663 })
664 }
665 }
666
667 for (const playlist of video.VideoStreamingPlaylists) {
668 const size = playlist.VideoFiles.reduce((a, b) => a + b.size, 0)
669
670 for (const redundancy of playlist.RedundancyVideos) {
671 streamingPlaylistsRedundancies.push({
672 id: redundancy.id,
673 fileUrl: redundancy.fileUrl,
674 strategy: redundancy.strategy,
675 createdAt: redundancy.createdAt,
676 updatedAt: redundancy.updatedAt,
677 expiresOn: redundancy.expiresOn,
678 size
679 })
680 }
681 }
682
683 return {
684 id: video.id,
685 name: video.name,
686 url: video.url,
687 uuid: video.uuid,
688
689 redundancies: {
690 files: filesRedundancies,
691 streamingPlaylists: streamingPlaylistsRedundancies
692 }
693 }
694 }
695
09209296 696 getVideo () {
2e1e4af0 697 if (this.VideoFile?.Video) return this.VideoFile.Video
09209296 698
2e1e4af0 699 if (this.VideoStreamingPlaylist?.Video) return this.VideoStreamingPlaylist.Video
9cfeb3cf
C
700
701 return undefined
09209296
C
702 }
703
52b1fd15
C
704 getVideoUUID () {
705 const video = this.getVideo()
706 if (!video) return undefined
707
708 return video.uuid
709 }
710
8d1fa36a
C
711 isOwned () {
712 return !!this.strategy
713 }
714
b5fecbf4 715 toActivityPubObject (this: MVideoRedundancyAP): CacheFileObject {
09209296
C
716 if (this.VideoStreamingPlaylist) {
717 return {
718 id: this.url,
719 type: 'CacheFile' as 'CacheFile',
720 object: this.VideoStreamingPlaylist.Video.url,
b764380a 721 expires: this.expiresOn ? this.expiresOn.toISOString() : null,
09209296
C
722 url: {
723 type: 'Link',
09209296
C
724 mediaType: 'application/x-mpegURL',
725 href: this.fileUrl
726 }
727 }
728 }
729
c48e82b5
C
730 return {
731 id: this.url,
732 type: 'CacheFile' as 'CacheFile',
733 object: this.VideoFile.Video.url,
b764380a 734 expires: this.expiresOn ? this.expiresOn.toISOString() : null,
c48e82b5
C
735 url: {
736 type: 'Link',
a1587156 737 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[this.VideoFile.extname] as any,
c48e82b5
C
738 href: this.fileUrl,
739 height: this.VideoFile.resolution,
740 size: this.VideoFile.size,
741 fps: this.VideoFile.fps
742 }
743 }
744 }
745
b36f41ca 746 // Don't include video files we already duplicated
7448551f 747 private static buildVideoIdsForDuplication (peertubeActor: MActor) {
3acc5084 748 const notIn = literal(
c48e82b5 749 '(' +
7448551f
C
750 `SELECT "videoFile"."videoId" AS "videoId" FROM "videoRedundancy" ` +
751 `INNER JOIN "videoFile" ON "videoFile"."id" = "videoRedundancy"."videoFileId" ` +
752 `WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
753 `UNION ` +
754 `SELECT "videoStreamingPlaylist"."videoId" AS "videoId" FROM "videoRedundancy" ` +
755 `INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoRedundancy"."videoStreamingPlaylistId" ` +
756 `WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
c48e82b5
C
757 ')'
758 )
b36f41ca
C
759
760 return {
7448551f
C
761 id: {
762 [Op.notIn]: notIn
b36f41ca
C
763 }
764 }
765 }
766
767 private static buildServerRedundancyInclude () {
768 return {
769 attributes: [],
770 model: VideoChannelModel.unscoped(),
771 required: true,
772 include: [
773 {
774 attributes: [],
775 model: ActorModel.unscoped(),
776 required: true,
777 include: [
778 {
779 attributes: [],
780 model: ServerModel.unscoped(),
781 required: true,
782 where: {
783 redundancyAllowed: true
784 }
785 }
786 ]
787 }
788 ]
789 }
c48e82b5
C
790 }
791}