]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Split ffmpeg utils with ffprobe utils
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
CommitLineData
39445ead 1import * as Bluebird from 'bluebird'
d95d1559 2import { remove } from 'fs-extra'
6b842050 3import { maxBy, minBy, pick } from 'lodash'
098eb377 4import { join } from 'path'
5f3e2425 5import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
3fd3ab2d 6import {
4ba3b8ea
C
7 AllowNull,
8 BeforeDestroy,
9 BelongsTo,
10 BelongsToMany,
11 Column,
12 CreatedAt,
13 DataType,
14 Default,
15 ForeignKey,
16 HasMany,
2baea0c7 17 HasOne,
4ba3b8ea
C
18 Is,
19 IsInt,
20 IsUUID,
21 Min,
22 Model,
23 Scopes,
24 Table,
9a629c6e 25 UpdatedAt
3fd3ab2d 26} from 'sequelize-typescript'
d95d1559 27import { buildNSFWFilter } from '@server/helpers/express-utils'
68e70a74 28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
af4ae64f 29import { LiveManager } from '@server/lib/live-manager'
d95d1559
C
30import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
31import { getServerActor } from '@server/models/application/application'
32import { ModelCache } from '@server/models/model-cache'
33import { VideoFile } from '@shared/models/videos/video-file.model'
34import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
de6310b2 35import { VideoObject } from '../../../shared/models/activitypub/objects'
d7a25329 36import { Video, VideoDetails } from '../../../shared/models/videos'
d95d1559 37import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
066e94c5 38import { VideoFilter } from '../../../shared/models/videos/video-query.type'
d95d1559 39import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
30ff39e7 40import { peertubeTruncate } from '../../helpers/core-utils'
da854ddd 41import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
d7a25329 42import { isBooleanValid } from '../../helpers/custom-validators/misc'
3fd3ab2d 43import {
4ba3b8ea
C
44 isVideoCategoryValid,
45 isVideoDescriptionValid,
46 isVideoDurationValid,
47 isVideoLanguageValid,
48 isVideoLicenceValid,
418d092a 49 isVideoNameValid,
2baea0c7
C
50 isVideoPrivacyValid,
51 isVideoStateValid,
b64c950a 52 isVideoSupportValid
3fd3ab2d 53} from '../../helpers/custom-validators/videos'
daf6e480 54import { getVideoFileResolution } from '../../helpers/ffprobe-utils'
da854ddd 55import { logger } from '../../helpers/logger'
d95d1559 56import { CONFIG } from '../../initializers/config'
65fcc311 57import {
1297eb5d 58 ACTIVITY_PUB,
4ba3b8ea 59 API_VERSION,
418d092a 60 CONSTRAINTS_FIELDS,
557b13ae 61 LAZY_STATIC_PATHS,
4ba3b8ea 62 REMOTE_SCHEME,
02756fbd 63 STATIC_DOWNLOAD_PATHS,
4ba3b8ea 64 STATIC_PATHS,
4ba3b8ea
C
65 VIDEO_CATEGORIES,
66 VIDEO_LANGUAGES,
67 VIDEO_LICENCES,
2baea0c7 68 VIDEO_PRIVACIES,
6dd9de95
C
69 VIDEO_STATES,
70 WEBSERVER
74dc3bca 71} from '../../initializers/constants'
50d6de9c 72import { sendDeleteVideo } from '../../lib/activitypub/send'
453e83ea
C
73import {
74 MChannel,
0283eaac 75 MChannelAccountDefault,
453e83ea 76 MChannelId,
d7a25329
C
77 MStreamingPlaylist,
78 MStreamingPlaylistFilesVideo,
453e83ea
C
79 MUserAccountId,
80 MUserId,
453e83ea 81 MVideoAccountLight,
0283eaac 82 MVideoAccountLightBlacklistAllFiles,
b5fecbf4 83 MVideoAP,
453e83ea 84 MVideoDetails,
d7a25329 85 MVideoFileVideo,
b5fecbf4
C
86 MVideoFormattable,
87 MVideoFormattableDetails,
0283eaac 88 MVideoForUser,
453e83ea 89 MVideoFullLight,
5f3e2425
C
90 MVideoIdThumbnail,
91 MVideoImmutable,
453e83ea 92 MVideoThumbnail,
d636ab58 93 MVideoThumbnailBlacklist,
b5fecbf4
C
94 MVideoWithAllFiles,
95 MVideoWithFile,
0374b6b5 96 MVideoWithRights
26d6bf65 97} from '../../types/models'
26d6bf65 98import { MThumbnail } from '../../types/models/video/thumbnail'
a293ac71 99import { MVideoFile, MVideoFileRedundanciesOpt, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
d95d1559
C
100import { VideoAbuseModel } from '../abuse/video-abuse'
101import { AccountModel } from '../account/account'
102import { AccountVideoRateModel } from '../account/account-video-rate'
103import { UserVideoHistoryModel } from '../account/user-video-history'
104import { ActorModel } from '../activitypub/actor'
105import { AvatarModel } from '../avatar/avatar'
106import { VideoRedundancyModel } from '../redundancy/video-redundancy'
107import { ServerModel } from '../server/server'
108import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
109import { ScheduleVideoUpdateModel } from './schedule-video-update'
110import { TagModel } from './tag'
111import { ThumbnailModel } from './thumbnail'
112import { VideoBlacklistModel } from './video-blacklist'
113import { VideoCaptionModel } from './video-caption'
114import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
115import { VideoCommentModel } from './video-comment'
116import { VideoFileModel } from './video-file'
117import {
118 videoFilesModelToFormattedJSON,
119 VideoFormattingJSONOptions,
120 videoModelToActivityPubObject,
121 videoModelToFormattedDetailsJSON,
122 videoModelToFormattedJSON
123} from './video-format-utils'
124import { VideoImportModel } from './video-import'
af4ae64f 125import { VideoLiveModel } from './video-live'
d95d1559 126import { VideoPlaylistElementModel } from './video-playlist-element'
6b842050 127import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
d95d1559
C
128import { VideoShareModel } from './video-share'
129import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
130import { VideoTagModel } from './video-tag'
131import { VideoViewModel } from './video-view'
6e46de09 132
2baea0c7 133export enum ScopeNames {
afd2cba5
C
134 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
135 FOR_API = 'FOR_API',
4cb6d457 136 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
d48ff09d 137 WITH_TAGS = 'WITH_TAGS',
d7a25329 138 WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
191764f3 139 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
6e46de09 140 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
09209296
C
141 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
142 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
e8bafea3 143 WITH_USER_ID = 'WITH_USER_ID',
943e5193 144 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
af4ae64f
C
145 WITH_THUMBNAILS = 'WITH_THUMBNAILS',
146 WITH_LIVE = 'WITH_LIVE'
d48ff09d
C
147}
148
bfbd9128
C
149export type ForAPIOptions = {
150 ids?: number[]
418d092a
C
151
152 videoPlaylistId?: number
153
afd2cba5 154 withFiles?: boolean
bfbd9128
C
155
156 withAccountBlockerIds?: number[]
afd2cba5
C
157}
158
bfbd9128 159export type AvailableForListIDsOptions = {
7ad9b984 160 serverAccountId: number
4e74e803 161 followerActorId: number
afd2cba5 162 includeLocalVideos: boolean
418d092a 163
bfbd9128 164 attributesType?: 'none' | 'id' | 'all'
8519cc92 165
afd2cba5
C
166 filter?: VideoFilter
167 categoryOneOf?: number[]
168 nsfw?: boolean
169 licenceOneOf?: number[]
170 languageOneOf?: string[]
171 tagsOneOf?: string[]
172 tagsAllOf?: string[]
418d092a 173
afd2cba5 174 withFiles?: boolean
418d092a 175
afd2cba5 176 accountId?: number
d525fc39 177 videoChannelId?: number
418d092a
C
178
179 videoPlaylistId?: number
180
9a629c6e 181 trendingDays?: number
453e83ea
C
182 user?: MUserAccountId
183 historyOfUser?: MUserId
3caf77d3
C
184
185 baseWhere?: WhereOptions[]
d525fc39
C
186}
187
3acc5084 188@Scopes(() => ({
943e5193
C
189 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
190 attributes: [ 'id', 'url', 'uuid', 'remote' ]
191 },
a1587156 192 [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
1735c825 193 const query: FindOptions = {
418d092a
C
194 include: [
195 {
bfbd9128
C
196 model: VideoChannelModel.scope({
197 method: [
198 VideoChannelScopeNames.SUMMARY, {
199 withAccount: true,
200 withAccountBlockerIds: options.withAccountBlockerIds
201 } as SummaryOptions
202 ]
203 }),
76564702 204 required: true
2fb5b3a5
C
205 },
206 {
207 attributes: [ 'type', 'filename' ],
208 model: ThumbnailModel,
209 required: false
418d092a
C
210 }
211 ]
afd2cba5
C
212 }
213
bfbd9128
C
214 if (options.ids) {
215 query.where = {
216 id: {
a1587156 217 [Op.in]: options.ids
bfbd9128
C
218 }
219 }
220 }
221
afd2cba5
C
222 if (options.withFiles === true) {
223 query.include.push({
8319d6ae 224 model: VideoFileModel,
afd2cba5
C
225 required: true
226 })
227 }
228
418d092a
C
229 if (options.videoPlaylistId) {
230 query.include.push({
231 model: VideoPlaylistElementModel.unscoped(),
15e9d5ca
C
232 required: true,
233 where: {
234 videoPlaylistId: options.videoPlaylistId
235 }
418d092a
C
236 })
237 }
238
afd2cba5
C
239 return query
240 },
a1587156 241 [ScopeNames.WITH_THUMBNAILS]: {
e8bafea3
C
242 include: [
243 {
3acc5084 244 model: ThumbnailModel,
e8bafea3
C
245 required: false
246 }
247 ]
248 },
af4ae64f
C
249 [ScopeNames.WITH_LIVE]: {
250 include: [
251 {
97969c4e 252 model: VideoLiveModel.unscoped(),
af4ae64f
C
253 required: false
254 }
255 ]
256 },
a1587156 257 [ScopeNames.WITH_USER_ID]: {
09209296
C
258 include: [
259 {
260 attributes: [ 'accountId' ],
3acc5084 261 model: VideoChannelModel.unscoped(),
09209296
C
262 required: true,
263 include: [
264 {
265 attributes: [ 'userId' ],
3acc5084 266 model: AccountModel.unscoped(),
09209296
C
267 required: true
268 }
269 ]
270 }
3acc5084 271 ]
09209296 272 },
a1587156 273 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
d48ff09d
C
274 include: [
275 {
3acc5084 276 model: VideoChannelModel.unscoped(),
d48ff09d
C
277 required: true,
278 include: [
6120941f
C
279 {
280 attributes: {
281 exclude: [ 'privateKey', 'publicKey' ]
282 },
3acc5084 283 model: ActorModel.unscoped(),
3e500247
C
284 required: true,
285 include: [
286 {
287 attributes: [ 'host' ],
3acc5084 288 model: ServerModel.unscoped(),
3e500247 289 required: false
52d9f792
C
290 },
291 {
3acc5084 292 model: AvatarModel.unscoped(),
52d9f792 293 required: false
3e500247
C
294 }
295 ]
6120941f 296 },
d48ff09d 297 {
3acc5084 298 model: AccountModel.unscoped(),
d48ff09d
C
299 required: true,
300 include: [
301 {
3acc5084 302 model: ActorModel.unscoped(),
6120941f
C
303 attributes: {
304 exclude: [ 'privateKey', 'publicKey' ]
305 },
50d6de9c
C
306 required: true,
307 include: [
308 {
3e500247 309 attributes: [ 'host' ],
3acc5084 310 model: ServerModel.unscoped(),
50d6de9c 311 required: false
b6a4fd6b
C
312 },
313 {
3acc5084 314 model: AvatarModel.unscoped(),
b6a4fd6b 315 required: false
50d6de9c
C
316 }
317 ]
d48ff09d
C
318 }
319 ]
320 }
321 ]
322 }
3acc5084 323 ]
d48ff09d 324 },
a1587156 325 [ScopeNames.WITH_TAGS]: {
3acc5084 326 include: [ TagModel ]
d48ff09d 327 },
a1587156 328 [ScopeNames.WITH_BLACKLISTED]: {
191764f3
C
329 include: [
330 {
453e83ea 331 attributes: [ 'id', 'reason', 'unfederated' ],
3acc5084 332 model: VideoBlacklistModel,
191764f3
C
333 required: false
334 }
335 ]
336 },
a1587156 337 [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => {
09209296
C
338 let subInclude: any[] = []
339
340 if (withRedundancies === true) {
341 subInclude = [
342 {
343 attributes: [ 'fileUrl' ],
344 model: VideoRedundancyModel.unscoped(),
345 required: false
346 }
347 ]
348 }
349
350 return {
351 include: [
352 {
8319d6ae 353 model: VideoFileModel,
3acc5084 354 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
09209296
C
355 required: false,
356 include: subInclude
357 }
358 ]
359 }
360 },
a1587156 361 [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
d7a25329
C
362 const subInclude: IncludeOptions[] = [
363 {
8319d6ae 364 model: VideoFileModel,
d7a25329
C
365 required: false
366 }
367 ]
09209296
C
368
369 if (withRedundancies === true) {
d7a25329
C
370 subInclude.push({
371 attributes: [ 'fileUrl' ],
372 model: VideoRedundancyModel.unscoped(),
373 required: false
374 })
09209296
C
375 }
376
377 return {
378 include: [
379 {
380 model: VideoStreamingPlaylistModel.unscoped(),
3acc5084 381 separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
09209296
C
382 required: false,
383 include: subInclude
384 }
385 ]
386 }
bbe0f064 387 },
a1587156 388 [ScopeNames.WITH_SCHEDULED_UPDATE]: {
bbe0f064
C
389 include: [
390 {
3acc5084 391 model: ScheduleVideoUpdateModel.unscoped(),
bbe0f064
C
392 required: false
393 }
394 ]
6e46de09 395 },
a1587156 396 [ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
6e46de09
C
397 return {
398 include: [
399 {
400 attributes: [ 'currentTime' ],
401 model: UserVideoHistoryModel.unscoped(),
402 required: false,
403 where: {
404 userId
405 }
406 }
407 ]
408 }
d48ff09d 409 }
3acc5084 410}))
3fd3ab2d
C
411@Table({
412 tableName: 'video',
0374b6b5
C
413 indexes: [
414 buildTrigramSearchIndex('video_name_trigram', 'name'),
415
416 { fields: [ 'createdAt' ] },
417 {
418 fields: [
419 { name: 'publishedAt', order: 'DESC' },
420 { name: 'id', order: 'ASC' }
421 ]
422 },
423 { fields: [ 'duration' ] },
424 { fields: [ 'views' ] },
425 { fields: [ 'channelId' ] },
426 {
427 fields: [ 'originallyPublishedAt' ],
428 where: {
429 originallyPublishedAt: {
430 [Op.ne]: null
431 }
432 }
433 },
434 {
435 fields: [ 'category' ], // We don't care videos with an unknown category
436 where: {
437 category: {
438 [Op.ne]: null
439 }
440 }
441 },
442 {
443 fields: [ 'licence' ], // We don't care videos with an unknown licence
444 where: {
445 licence: {
446 [Op.ne]: null
447 }
448 }
449 },
450 {
451 fields: [ 'language' ], // We don't care videos with an unknown language
452 where: {
453 language: {
454 [Op.ne]: null
455 }
456 }
457 },
458 {
459 fields: [ 'nsfw' ], // Most of the videos are not NSFW
460 where: {
461 nsfw: true
462 }
463 },
464 {
465 fields: [ 'remote' ], // Only index local videos
466 where: {
467 remote: false
468 }
469 },
470 {
471 fields: [ 'uuid' ],
472 unique: true
473 },
474 {
475 fields: [ 'url' ],
476 unique: true
477 }
478 ]
3fd3ab2d
C
479})
480export class VideoModel extends Model<VideoModel> {
481
482 @AllowNull(false)
483 @Default(DataType.UUIDV4)
484 @IsUUID(4)
485 @Column(DataType.UUID)
486 uuid: string
487
488 @AllowNull(false)
489 @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name'))
490 @Column
491 name: string
492
493 @AllowNull(true)
494 @Default(null)
1735c825 495 @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category', true))
3fd3ab2d
C
496 @Column
497 category: number
498
499 @AllowNull(true)
500 @Default(null)
1735c825 501 @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence', true))
3fd3ab2d
C
502 @Column
503 licence: number
504
505 @AllowNull(true)
506 @Default(null)
1735c825 507 @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language', true))
9d3ef9fe
C
508 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max))
509 language: string
3fd3ab2d
C
510
511 @AllowNull(false)
512 @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
513 @Column
1ba471c5 514 privacy: VideoPrivacy
3fd3ab2d
C
515
516 @AllowNull(false)
47564bbe 517 @Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
3fd3ab2d
C
518 @Column
519 nsfw: boolean
520
521 @AllowNull(true)
522 @Default(null)
1735c825 523 @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description', true))
3fd3ab2d
C
524 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
525 description: string
526
2422c46b
C
527 @AllowNull(true)
528 @Default(null)
1735c825 529 @Is('VideoSupport', value => throwIfNotValid(value, isVideoSupportValid, 'support', true))
2422c46b
C
530 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max))
531 support: string
532
3fd3ab2d
C
533 @AllowNull(false)
534 @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration'))
535 @Column
536 duration: number
537
538 @AllowNull(false)
539 @Default(0)
540 @IsInt
541 @Min(0)
542 @Column
543 views: number
544
545 @AllowNull(false)
546 @Default(0)
547 @IsInt
548 @Min(0)
549 @Column
550 likes: number
551
552 @AllowNull(false)
553 @Default(0)
554 @IsInt
555 @Min(0)
556 @Column
557 dislikes: number
558
559 @AllowNull(false)
560 @Column
561 remote: boolean
562
563 @AllowNull(false)
c6c0fa6c
C
564 @Default(false)
565 @Column
566 isLive: boolean
567
568 @AllowNull(false)
3fd3ab2d
C
569 @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
570 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
571 url: string
572
47564bbe
C
573 @AllowNull(false)
574 @Column
575 commentsEnabled: boolean
576
156c50af
LD
577 @AllowNull(false)
578 @Column
7f2cfe3a 579 downloadEnabled: boolean
156c50af 580
2186386c
C
581 @AllowNull(false)
582 @Column
583 waitTranscoding: boolean
584
585 @AllowNull(false)
586 @Default(null)
587 @Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
588 @Column
589 state: VideoState
590
3fd3ab2d
C
591 @CreatedAt
592 createdAt: Date
593
594 @UpdatedAt
595 updatedAt: Date
596
2922e048 597 @AllowNull(false)
1735c825 598 @Default(DataType.NOW)
2922e048
JLB
599 @Column
600 publishedAt: Date
601
7519127b
C
602 @AllowNull(true)
603 @Default(null)
c8034165 604 @Column
605 originallyPublishedAt: Date
606
3fd3ab2d
C
607 @ForeignKey(() => VideoChannelModel)
608 @Column
609 channelId: number
610
611 @BelongsTo(() => VideoChannelModel, {
feb4bdfd 612 foreignKey: {
50d6de9c 613 allowNull: true
feb4bdfd 614 },
6b738c7a 615 hooks: true
feb4bdfd 616 })
3fd3ab2d 617 VideoChannel: VideoChannelModel
7920c273 618
3fd3ab2d 619 @BelongsToMany(() => TagModel, {
7920c273 620 foreignKey: 'videoId',
3fd3ab2d
C
621 through: () => VideoTagModel,
622 onDelete: 'CASCADE'
7920c273 623 })
3fd3ab2d 624 Tags: TagModel[]
55fa55a9 625
e8bafea3
C
626 @HasMany(() => ThumbnailModel, {
627 foreignKey: {
628 name: 'videoId',
629 allowNull: true
630 },
631 hooks: true,
632 onDelete: 'cascade'
633 })
634 Thumbnails: ThumbnailModel[]
635
418d092a
C
636 @HasMany(() => VideoPlaylistElementModel, {
637 foreignKey: {
638 name: 'videoId',
bfbd9128 639 allowNull: true
418d092a 640 },
bfbd9128 641 onDelete: 'set null'
418d092a
C
642 })
643 VideoPlaylistElements: VideoPlaylistElementModel[]
644
3fd3ab2d 645 @HasMany(() => VideoAbuseModel, {
55fa55a9
C
646 foreignKey: {
647 name: 'videoId',
68d19a0a 648 allowNull: true
55fa55a9 649 },
68d19a0a 650 onDelete: 'set null'
55fa55a9 651 })
3fd3ab2d 652 VideoAbuses: VideoAbuseModel[]
93e1258c 653
3fd3ab2d 654 @HasMany(() => VideoFileModel, {
93e1258c
C
655 foreignKey: {
656 name: 'videoId',
d7a25329 657 allowNull: true
93e1258c 658 },
c48e82b5 659 hooks: true,
93e1258c
C
660 onDelete: 'cascade'
661 })
3fd3ab2d 662 VideoFiles: VideoFileModel[]
e71bcc0f 663
09209296
C
664 @HasMany(() => VideoStreamingPlaylistModel, {
665 foreignKey: {
666 name: 'videoId',
667 allowNull: false
668 },
669 hooks: true,
670 onDelete: 'cascade'
671 })
672 VideoStreamingPlaylists: VideoStreamingPlaylistModel[]
673
3fd3ab2d 674 @HasMany(() => VideoShareModel, {
e71bcc0f
C
675 foreignKey: {
676 name: 'videoId',
677 allowNull: false
678 },
679 onDelete: 'cascade'
680 })
3fd3ab2d 681 VideoShares: VideoShareModel[]
16b90975 682
3fd3ab2d 683 @HasMany(() => AccountVideoRateModel, {
16b90975
C
684 foreignKey: {
685 name: 'videoId',
686 allowNull: false
687 },
688 onDelete: 'cascade'
689 })
3fd3ab2d 690 AccountVideoRates: AccountVideoRateModel[]
f285faa0 691
da854ddd
C
692 @HasMany(() => VideoCommentModel, {
693 foreignKey: {
694 name: 'videoId',
695 allowNull: false
696 },
f05a1c30
C
697 onDelete: 'cascade',
698 hooks: true
da854ddd
C
699 })
700 VideoComments: VideoCommentModel[]
701
9a629c6e
C
702 @HasMany(() => VideoViewModel, {
703 foreignKey: {
704 name: 'videoId',
705 allowNull: false
706 },
6e46de09 707 onDelete: 'cascade'
9a629c6e
C
708 })
709 VideoViews: VideoViewModel[]
710
6e46de09
C
711 @HasMany(() => UserVideoHistoryModel, {
712 foreignKey: {
713 name: 'videoId',
714 allowNull: false
715 },
716 onDelete: 'cascade'
717 })
718 UserVideoHistories: UserVideoHistoryModel[]
719
2baea0c7
C
720 @HasOne(() => ScheduleVideoUpdateModel, {
721 foreignKey: {
722 name: 'videoId',
723 allowNull: false
724 },
725 onDelete: 'cascade'
726 })
727 ScheduleVideoUpdate: ScheduleVideoUpdateModel
728
26b7305a
C
729 @HasOne(() => VideoBlacklistModel, {
730 foreignKey: {
731 name: 'videoId',
732 allowNull: false
733 },
734 onDelete: 'cascade'
735 })
736 VideoBlacklist: VideoBlacklistModel
737
31c82cd9
C
738 @HasOne(() => VideoLiveModel, {
739 foreignKey: {
740 name: 'videoId',
741 allowNull: false
742 },
743 onDelete: 'cascade'
744 })
745 VideoLive: VideoLiveModel
746
dc133480
C
747 @HasOne(() => VideoImportModel, {
748 foreignKey: {
749 name: 'videoId',
750 allowNull: true
751 },
752 onDelete: 'set null'
753 })
754 VideoImport: VideoImportModel
755
40e87e9e
C
756 @HasMany(() => VideoCaptionModel, {
757 foreignKey: {
758 name: 'videoId',
759 allowNull: false
760 },
761 onDelete: 'cascade',
762 hooks: true,
a1587156 763 ['separate' as any]: true
40e87e9e
C
764 })
765 VideoCaptions: VideoCaptionModel[]
766
f05a1c30 767 @BeforeDestroy
453e83ea 768 static async sendDelete (instance: MVideoAccountLight, options) {
f05a1c30
C
769 if (instance.isOwned()) {
770 if (!instance.VideoChannel) {
771 instance.VideoChannel = await instance.$get('VideoChannel', {
772 include: [
453e83ea
C
773 ActorModel,
774 AccountModel
f05a1c30
C
775 ],
776 transaction: options.transaction
0283eaac 777 }) as MChannelAccountDefault
f05a1c30
C
778 }
779
f05a1c30
C
780 return sendDeleteVideo(instance, options.transaction)
781 }
782
783 return undefined
784 }
785
6b738c7a 786 @BeforeDestroy
40e87e9e 787 static async removeFiles (instance: VideoModel) {
f05a1c30 788 const tasks: Promise<any>[] = []
f285faa0 789
8e0fd45e 790 logger.info('Removing files of video %s.', instance.url)
6b738c7a 791
3fd3ab2d 792 if (instance.isOwned()) {
f05a1c30 793 if (!Array.isArray(instance.VideoFiles)) {
e6122097 794 instance.VideoFiles = await instance.$get('VideoFiles')
f05a1c30
C
795 }
796
3fd3ab2d
C
797 // Remove physical files and torrents
798 instance.VideoFiles.forEach(file => {
799 tasks.push(instance.removeFile(file))
800 tasks.push(instance.removeTorrent(file))
801 })
09209296
C
802
803 // Remove playlists file
ffc65cbd
C
804 if (!Array.isArray(instance.VideoStreamingPlaylists)) {
805 instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists')
806 }
807
808 for (const p of instance.VideoStreamingPlaylists) {
809 tasks.push(instance.removeStreamingPlaylistFiles(p))
810 }
3fd3ab2d 811 }
40298b02 812
6b738c7a
C
813 // Do not wait video deletion because we could be in a transaction
814 Promise.all(tasks)
8ea6f49a
C
815 .catch(err => {
816 logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err })
817 })
6b738c7a
C
818
819 return undefined
3fd3ab2d 820 }
f285faa0 821
a5cf76af
C
822 @BeforeDestroy
823 static stopLiveIfNeeded (instance: VideoModel) {
824 if (!instance.isLive) return
825
68e70a74
C
826 logger.info('Stopping live of video %s after video deletion.', instance.uuid)
827
a5cf76af
C
828 return LiveManager.Instance.stopSessionOf(instance.id)
829 }
830
7eba5e1f
C
831 @BeforeDestroy
832 static invalidateCache (instance: VideoModel) {
833 ModelCache.Instance.invalidateCache('video', instance.id)
834 }
835
68d19a0a
RK
836 @BeforeDestroy
837 static async saveEssentialDataToAbuses (instance: VideoModel, options) {
838 const tasks: Promise<any>[] = []
839
68d19a0a
RK
840 if (!Array.isArray(instance.VideoAbuses)) {
841 instance.VideoAbuses = await instance.$get('VideoAbuses')
842
843 if (instance.VideoAbuses.length === 0) return undefined
844 }
845
57f6896f
C
846 logger.info('Saving video abuses details of video %s.', instance.url)
847
86521a67 848 const details = instance.toFormattedDetailsJSON()
68d19a0a
RK
849
850 for (const abuse of instance.VideoAbuses) {
0251197e
RK
851 abuse.deletedVideo = details
852 tasks.push(abuse.save({ transaction: options.transaction }))
68d19a0a
RK
853 }
854
855 Promise.all(tasks)
856 .catch(err => {
857 logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err })
858 })
859
860 return undefined
861 }
862
453e83ea 863 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
9f1ddd24
C
864 const query = {
865 where: {
866 remote: false
867 }
868 }
869
e8bafea3 870 return VideoModel.scope([
d7a25329 871 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
872 ScopeNames.WITH_STREAMING_PLAYLISTS,
873 ScopeNames.WITH_THUMBNAILS
874 ]).findAll(query)
9f1ddd24
C
875 }
876
50d6de9c 877 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
3fd3ab2d
C
878 function getRawQuery (select: string) {
879 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
880 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
50d6de9c
C
881 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
882 'WHERE "Account"."actorId" = ' + actorId
3fd3ab2d
C
883 const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
884 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
50d6de9c 885 'WHERE "VideoShare"."actorId" = ' + actorId
558d7c23 886
3fd3ab2d
C
887 return `(${queryVideo}) UNION (${queryVideoShare})`
888 }
aaf61f38 889
3fd3ab2d
C
890 const rawQuery = getRawQuery('"Video"."id"')
891 const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
892
893 const query = {
894 distinct: true,
895 offset: start,
896 limit: count,
71398458 897 order: getVideoSort('-createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
3fd3ab2d
C
898 where: {
899 id: {
a1587156 900 [Op.in]: Sequelize.literal('(' + rawQuery + ')')
3c75ce12 901 },
3092e9bb 902 [Op.or]: getPrivaciesForFederation()
3fd3ab2d
C
903 },
904 include: [
40e87e9e 905 {
ca6d3622 906 attributes: [ 'language', 'fileUrl' ],
40e87e9e
C
907 model: VideoCaptionModel.unscoped(),
908 required: false
909 },
3fd3ab2d 910 {
1d230c44 911 attributes: [ 'id', 'url' ],
2c897999 912 model: VideoShareModel.unscoped(),
3fd3ab2d 913 required: false,
e3d5ea4f
C
914 // We only want videos shared by this actor
915 where: {
a1587156 916 [Op.and]: [
e3d5ea4f
C
917 {
918 id: {
a1587156 919 [Op.not]: null
e3d5ea4f
C
920 }
921 },
922 {
923 actorId
924 }
925 ]
926 },
50d6de9c
C
927 include: [
928 {
2c897999
C
929 attributes: [ 'id', 'url' ],
930 model: ActorModel.unscoped()
50d6de9c
C
931 }
932 ]
3fd3ab2d
C
933 },
934 {
2c897999 935 model: VideoChannelModel.unscoped(),
3fd3ab2d
C
936 required: true,
937 include: [
938 {
2c897999
C
939 attributes: [ 'name' ],
940 model: AccountModel.unscoped(),
941 required: true,
942 include: [
943 {
e3d5ea4f 944 attributes: [ 'id', 'url', 'followersUrl' ],
2c897999
C
945 model: ActorModel.unscoped(),
946 required: true
947 }
948 ]
949 },
950 {
e3d5ea4f 951 attributes: [ 'id', 'url', 'followersUrl' ],
2c897999 952 model: ActorModel.unscoped(),
3fd3ab2d
C
953 required: true
954 }
955 ]
956 },
af4ae64f
C
957 {
958 model: VideoStreamingPlaylistModel.unscoped(),
959 required: false,
960 include: [
961 {
962 model: VideoFileModel,
963 required: false
964 }
965 ]
966 },
c8f3cfeb 967 VideoLiveModel.unscoped(),
3fd3ab2d 968 VideoFileModel,
2c897999 969 TagModel
3fd3ab2d
C
970 ]
971 }
164174a6 972
3fd3ab2d 973 return Bluebird.all([
3acc5084
C
974 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
975 VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
3fd3ab2d
C
976 ]).then(([ rows, totals ]) => {
977 // totals: totalVideos + totalVideoShares
978 let totalVideos = 0
979 let totalVideoShares = 0
a1587156
C
980 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
981 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
3fd3ab2d
C
982
983 const total = totalVideos + totalVideoShares
984 return {
985 data: rows,
986 total: total
987 }
988 })
989 }
93e1258c 990
5c0904fc
C
991 static listPublishedLiveIds () {
992 const options = {
993 attributes: [ 'id' ],
994 where: {
995 isLive: true,
996 state: VideoState.PUBLISHED
997 }
998 }
999
1000 return VideoModel.findAll(options)
1001 .map(v => v.id)
1002 }
1003
bf64ed41
RK
1004 static listUserVideosForApi (
1005 accountId: number,
1006 start: number,
1007 count: number,
1008 sort: string,
1009 search?: string
1010 ) {
3acc5084 1011 function buildBaseQuery (): FindOptions {
bf64ed41 1012 let baseQuery = {
3acc5084
C
1013 offset: start,
1014 limit: count,
1015 order: getVideoSort(sort),
1016 include: [
1017 {
1018 model: VideoChannelModel,
1019 required: true,
1020 include: [
1021 {
1022 model: AccountModel,
1023 where: {
1024 id: accountId
1025 },
1026 required: true
1027 }
1028 ]
1029 }
1030 ]
1031 }
bf64ed41
RK
1032
1033 if (search) {
1034 baseQuery = Object.assign(baseQuery, {
1035 where: {
1036 name: {
a1587156 1037 [Op.iLike]: '%' + search + '%'
bf64ed41
RK
1038 }
1039 }
1040 })
1041 }
1042
1043 return baseQuery
3fd3ab2d 1044 }
d8755eed 1045
3acc5084
C
1046 const countQuery = buildBaseQuery()
1047 const findQuery = buildBaseQuery()
1048
bf64ed41 1049 const findScopes: (string | ScopeOptions)[] = [
a18f275d
C
1050 ScopeNames.WITH_SCHEDULED_UPDATE,
1051 ScopeNames.WITH_BLACKLISTED,
1052 ScopeNames.WITH_THUMBNAILS
1053 ]
3acc5084 1054
3acc5084
C
1055 return Promise.all([
1056 VideoModel.count(countQuery),
0283eaac 1057 VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
3acc5084
C
1058 ]).then(([ count, rows ]) => {
1059 return {
0283eaac 1060 data: rows,
3acc5084
C
1061 total: count
1062 }
1063 })
3fd3ab2d 1064 }
93e1258c 1065
48dce1c9 1066 static async listForApi (options: {
a1587156
C
1067 start: number
1068 count: number
1069 sort: string
1070 nsfw: boolean
1071 includeLocalVideos: boolean
1072 withFiles: boolean
1073 categoryOneOf?: number[]
1074 licenceOneOf?: number[]
1075 languageOneOf?: string[]
1076 tagsOneOf?: string[]
1077 tagsAllOf?: string[]
1078 filter?: VideoFilter
1079 accountId?: number
1080 videoChannelId?: number
4e74e803 1081 followerActorId?: number
a1587156
C
1082 videoPlaylistId?: number
1083 trendingDays?: number
1084 user?: MUserAccountId
1085 historyOfUser?: MUserId
fe987656
C
1086 countVideos?: boolean
1087 }) {
0aa52e17 1088 if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
7ad9b984 1089 throw new Error('Try to filter all-local but no user has not the see all videos right')
1cd3facc
C
1090 }
1091
5f3e2425
C
1092 const trendingDays = options.sort.endsWith('trending')
1093 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
1094 : undefined
93e1258c 1095
7ad9b984
C
1096 const serverActor = await getServerActor()
1097
4e74e803 1098 // followerActorId === null has a meaning, so just check undefined
5f3e2425
C
1099 const followerActorId = options.followerActorId !== undefined
1100 ? options.followerActorId
1101 : serverActor.id
06a05d5f 1102
afd2cba5 1103 const queryOptions = {
5f3e2425
C
1104 start: options.start,
1105 count: options.count,
1106 sort: options.sort,
4e74e803 1107 followerActorId,
7ad9b984 1108 serverAccountId: serverActor.Account.id,
afd2cba5
C
1109 nsfw: options.nsfw,
1110 categoryOneOf: options.categoryOneOf,
1111 licenceOneOf: options.licenceOneOf,
1112 languageOneOf: options.languageOneOf,
1113 tagsOneOf: options.tagsOneOf,
1114 tagsAllOf: options.tagsAllOf,
1115 filter: options.filter,
1116 withFiles: options.withFiles,
1117 accountId: options.accountId,
1118 videoChannelId: options.videoChannelId,
418d092a 1119 videoPlaylistId: options.videoPlaylistId,
9a629c6e 1120 includeLocalVideos: options.includeLocalVideos,
7ad9b984 1121 user: options.user,
8b9a525a 1122 historyOfUser: options.historyOfUser,
9a629c6e 1123 trendingDays
48dce1c9
C
1124 }
1125
5f3e2425 1126 return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
93e1258c
C
1127 }
1128
0b18f4aa 1129 static async searchAndPopulateAccountAndServer (options: {
06a05d5f 1130 includeLocalVideos: boolean
d4112450 1131 search?: string
0b18f4aa
C
1132 start?: number
1133 count?: number
1134 sort?: string
1135 startDate?: string // ISO 8601
1136 endDate?: string // ISO 8601
31d065cc
AM
1137 originallyPublishedStartDate?: string
1138 originallyPublishedEndDate?: string
0b18f4aa
C
1139 nsfw?: boolean
1140 categoryOneOf?: number[]
1141 licenceOneOf?: number[]
1142 languageOneOf?: string[]
1143 tagsOneOf?: string[]
1144 tagsAllOf?: string[]
1145 durationMin?: number // seconds
1146 durationMax?: number // seconds
a1587156 1147 user?: MUserAccountId
1cd3facc 1148 filter?: VideoFilter
0b18f4aa 1149 }) {
f05a1c30 1150 const serverActor = await getServerActor()
afd2cba5 1151 const queryOptions = {
4e74e803 1152 followerActorId: serverActor.id,
7ad9b984 1153 serverAccountId: serverActor.Account.id,
afd2cba5
C
1154 includeLocalVideos: options.includeLocalVideos,
1155 nsfw: options.nsfw,
1156 categoryOneOf: options.categoryOneOf,
1157 licenceOneOf: options.licenceOneOf,
1158 languageOneOf: options.languageOneOf,
1159 tagsOneOf: options.tagsOneOf,
6e46de09 1160 tagsAllOf: options.tagsAllOf,
7ad9b984 1161 user: options.user,
3caf77d3 1162 filter: options.filter,
5f3e2425
C
1163 start: options.start,
1164 count: options.count,
1165 sort: options.sort,
1166 startDate: options.startDate,
1167 endDate: options.endDate,
1168 originallyPublishedStartDate: options.originallyPublishedStartDate,
1169 originallyPublishedEndDate: options.originallyPublishedEndDate,
1170
1171 durationMin: options.durationMin,
1172 durationMax: options.durationMax,
1173
1174 search: options.search
48dce1c9 1175 }
f05a1c30 1176
5f3e2425 1177 return VideoModel.getAvailableForApi(queryOptions)
f05a1c30
C
1178 }
1179
a056ca48
C
1180 static countLocalLives () {
1181 const options = {
1182 where: {
1183 remote: false,
875f0610
C
1184 isLive: true,
1185 state: {
1186 [Op.ne]: VideoState.LIVE_ENDED
1187 }
a056ca48
C
1188 }
1189 }
1190
1191 return VideoModel.count(options)
1192 }
1193
1194 static countLivesOfAccount (accountId: number) {
1195 const options = {
1196 where: {
1197 remote: false,
1198 isLive: true
1199 },
1200 include: [
1201 {
1202 required: true,
1203 model: VideoChannelModel.unscoped(),
1204 where: {
1205 accountId
1206 }
1207 }
1208 ]
1209 }
1210
1211 return VideoModel.count(options)
1212 }
1213
453e83ea 1214 static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
418d092a 1215 const where = buildWhereIdOrUUID(id)
627621c1
C
1216 const options = {
1217 where,
1218 transaction: t
3fd3ab2d 1219 }
d8755eed 1220
e8bafea3 1221 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
3fd3ab2d 1222 }
d8755eed 1223
d636ab58
C
1224 static loadWithBlacklist (id: number | string, t?: Transaction): Bluebird<MVideoThumbnailBlacklist> {
1225 const where = buildWhereIdOrUUID(id)
1226 const options = {
1227 where,
1228 transaction: t
1229 }
1230
1231 return VideoModel.scope([
1232 ScopeNames.WITH_THUMBNAILS,
1233 ScopeNames.WITH_BLACKLISTED
1234 ]).findOne(options)
1235 }
1236
7eba5e1f
C
1237 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1238 const fun = () => {
943e5193
C
1239 const query = {
1240 where: buildWhereIdOrUUID(id),
7eba5e1f
C
1241 transaction: t
1242 }
1243
943e5193 1244 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
7eba5e1f
C
1245 }
1246
1247 return ModelCache.Instance.doCache({
943e5193 1248 cacheType: 'load-video-immutable-id',
7eba5e1f
C
1249 key: '' + id,
1250 deleteKey: 'video',
1251 fun
1252 })
1253 }
1254
453e83ea 1255 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
418d092a 1256 const where = buildWhereIdOrUUID(id)
09209296
C
1257 const options = {
1258 where,
1259 transaction: t
1260 }
1261
e8bafea3
C
1262 return VideoModel.scope([
1263 ScopeNames.WITH_BLACKLISTED,
1264 ScopeNames.WITH_USER_ID,
1265 ScopeNames.WITH_THUMBNAILS
1266 ]).findOne(options)
09209296
C
1267 }
1268
453e83ea 1269 static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
418d092a 1270 const where = buildWhereIdOrUUID(id)
627621c1 1271
3fd3ab2d 1272 const options = {
627621c1
C
1273 attributes: [ 'id' ],
1274 where,
1275 transaction: t
3fd3ab2d 1276 }
72c7248b 1277
e8bafea3 1278 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
627621c1
C
1279 }
1280
453e83ea 1281 static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
e2600d8b
C
1282 const where = buildWhereIdOrUUID(id)
1283
1284 const query = {
1285 where,
1286 transaction: t,
1287 logging
1288 }
1289
e8bafea3 1290 return VideoModel.scope([
d7a25329 1291 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
1292 ScopeNames.WITH_STREAMING_PLAYLISTS,
1293 ScopeNames.WITH_THUMBNAILS
e2600d8b 1294 ]).findOne(query)
3fd3ab2d 1295 }
72c7248b 1296
453e83ea 1297 static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
8fa5653a
C
1298 const options = {
1299 where: {
1300 uuid
1301 }
1302 }
1303
e8bafea3 1304 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
8fa5653a
C
1305 }
1306
453e83ea 1307 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
1735c825 1308 const query: FindOptions = {
627621c1
C
1309 where: {
1310 url
4157cdb1
C
1311 },
1312 transaction
627621c1
C
1313 }
1314
e8bafea3 1315 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
4157cdb1
C
1316 }
1317
943e5193
C
1318 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1319 const fun = () => {
1320 const query: FindOptions = {
1321 where: {
1322 url
1323 },
1324 transaction
1325 }
1326
1327 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1328 }
1329
1330 return ModelCache.Instance.doCache({
1331 cacheType: 'load-video-immutable-url',
1332 key: url,
1333 deleteKey: 'video',
1334 fun
1335 })
1336 }
1337
0283eaac 1338 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1735c825 1339 const query: FindOptions = {
4157cdb1
C
1340 where: {
1341 url
1342 },
1343 transaction
1344 }
627621c1 1345
09209296
C
1346 return VideoModel.scope([
1347 ScopeNames.WITH_ACCOUNT_DETAILS,
d7a25329 1348 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3 1349 ScopeNames.WITH_STREAMING_PLAYLISTS,
453e83ea
C
1350 ScopeNames.WITH_THUMBNAILS,
1351 ScopeNames.WITH_BLACKLISTED
09209296 1352 ]).findOne(query)
627621c1
C
1353 }
1354
453e83ea 1355 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
418d092a 1356 const where = buildWhereIdOrUUID(id)
627621c1 1357
3fd3ab2d 1358 const options = {
3acc5084 1359 order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
627621c1 1360 where,
2186386c 1361 transaction: t
3fd3ab2d 1362 }
fd45e8f4 1363
3acc5084 1364 const scopes: (string | ScopeOptions)[] = [
6e46de09
C
1365 ScopeNames.WITH_TAGS,
1366 ScopeNames.WITH_BLACKLISTED,
09209296
C
1367 ScopeNames.WITH_ACCOUNT_DETAILS,
1368 ScopeNames.WITH_SCHEDULED_UPDATE,
d7a25329 1369 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3 1370 ScopeNames.WITH_STREAMING_PLAYLISTS,
af4ae64f
C
1371 ScopeNames.WITH_THUMBNAILS,
1372 ScopeNames.WITH_LIVE
09209296
C
1373 ]
1374
1375 if (userId) {
3acc5084 1376 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
09209296
C
1377 }
1378
1379 return VideoModel
1380 .scope(scopes)
1381 .findOne(options)
1382 }
1383
89cd1275 1384 static loadForGetAPI (parameters: {
a1587156
C
1385 id: number | string
1386 t?: Transaction
89cd1275 1387 userId?: number
453e83ea 1388 }): Bluebird<MVideoDetails> {
89cd1275 1389 const { id, t, userId } = parameters
418d092a 1390 const where = buildWhereIdOrUUID(id)
09209296
C
1391
1392 const options = {
1735c825 1393 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
09209296
C
1394 where,
1395 transaction: t
1396 }
1397
3acc5084 1398 const scopes: (string | ScopeOptions)[] = [
09209296
C
1399 ScopeNames.WITH_TAGS,
1400 ScopeNames.WITH_BLACKLISTED,
6e46de09 1401 ScopeNames.WITH_ACCOUNT_DETAILS,
09209296 1402 ScopeNames.WITH_SCHEDULED_UPDATE,
e8bafea3 1403 ScopeNames.WITH_THUMBNAILS,
af4ae64f 1404 ScopeNames.WITH_LIVE,
d7a25329 1405 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
3acc5084 1406 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
6e46de09
C
1407 ]
1408
1409 if (userId) {
3acc5084 1410 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
6e46de09
C
1411 }
1412
d48ff09d 1413 return VideoModel
6e46de09 1414 .scope(scopes)
da854ddd
C
1415 .findOne(options)
1416 }
1417
09cababd
C
1418 static async getStats () {
1419 const totalLocalVideos = await VideoModel.count({
1420 where: {
1421 remote: false
1422 }
1423 })
09cababd
C
1424
1425 let totalLocalVideoViews = await VideoModel.sum('views', {
1426 where: {
1427 remote: false
1428 }
1429 })
baab47ca 1430
09cababd
C
1431 // Sequelize could return null...
1432 if (!totalLocalVideoViews) totalLocalVideoViews = 0
1433
baab47ca
C
1434 const { total: totalVideos } = await VideoModel.listForApi({
1435 start: 0,
1436 count: 0,
1437 sort: '-publishedAt',
1438 nsfw: buildNSFWFilter(),
1439 includeLocalVideos: true,
1440 withFiles: false
1441 })
1442
09cababd
C
1443 return {
1444 totalLocalVideos,
1445 totalLocalVideoViews,
1446 totalVideos
1447 }
1448 }
1449
6b616860
C
1450 static incrementViews (id: number, views: number) {
1451 return VideoModel.increment('views', {
1452 by: views,
1453 where: {
1454 id
1455 }
1456 })
1457 }
1458
8d427346
C
1459 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1460 // Instances only share videos
1461 const query = 'SELECT 1 FROM "videoShare" ' +
a1587156 1462 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
f046e2fa 1463 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "videoShare"."videoId" = $videoId ' +
a1587156 1464 'LIMIT 1'
8d427346
C
1465
1466 const options = {
d5d9b6d7 1467 type: QueryTypes.SELECT as QueryTypes.SELECT,
8d427346
C
1468 bind: { followerActorId, videoId },
1469 raw: true
1470 }
1471
1472 return VideoModel.sequelize.query(query, options)
1473 .then(results => results.length === 1)
1474 }
1475
453e83ea 1476 static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
7d14d4d2
C
1477 const options = {
1478 where: {
1479 channelId: videoChannel.id
1480 },
1481 transaction: t
1482 }
1483
1484 return VideoModel.update({ support: videoChannel.support }, options)
1485 }
1486
453e83ea 1487 static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
7d14d4d2
C
1488 const query = {
1489 attributes: [ 'id' ],
1490 where: {
1491 channelId: videoChannel.id
1492 }
1493 }
1494
1495 return VideoModel.findAll(query)
a1587156 1496 .then(videos => videos.map(v => v.id))
7d14d4d2
C
1497 }
1498
2d3741d6 1499 // threshold corresponds to how many video the field should have to be returned
7348b1fd 1500 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
65b21c96 1501 const serverActor = await getServerActor()
4e74e803 1502 const followerActorId = serverActor.id
7348b1fd 1503
5f3e2425
C
1504 const queryOptions: BuildVideosQueryOptions = {
1505 attributes: [ `"${field}"` ],
1506 group: `GROUP BY "${field}"`,
1507 having: `HAVING COUNT("${field}") >= ${threshold}`,
1508 start: 0,
1509 sort: 'random',
1510 count,
65b21c96 1511 serverAccountId: serverActor.Account.id,
4e74e803 1512 followerActorId,
5f3e2425 1513 includeLocalVideos: true
7348b1fd
C
1514 }
1515
5f3e2425 1516 const { query, replacements } = buildListQuery(VideoModel, queryOptions)
2d3741d6 1517
5f3e2425
C
1518 return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
1519 .then(rows => rows.map(r => r[field]))
2d3741d6
C
1520 }
1521
b36f41ca
C
1522 static buildTrendingQuery (trendingDays: number) {
1523 return {
1524 attributes: [],
1525 subQuery: false,
1526 model: VideoViewModel,
1527 required: false,
1528 where: {
1529 startDate: {
a1587156 1530 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
b36f41ca
C
1531 }
1532 }
1533 }
1534 }
1535
6e46de09 1536 private static async getAvailableForApi (
5f3e2425 1537 options: BuildVideosQueryOptions,
6e46de09 1538 countVideos = true
b84d4c80 1539 ): Promise<ResultList<VideoModel>> {
6b842050
C
1540 function getCount () {
1541 if (countVideos !== true) return Promise.resolve(undefined)
8ea6f49a 1542
6b842050
C
1543 const countOptions = Object.assign({}, options, { isCount: true })
1544 const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
3caf77d3 1545
6b842050
C
1546 return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
1547 .then(rows => rows.length !== 0 ? rows[0].total : 0)
1548 }
1549
1550 function getModels () {
baab47ca
C
1551 if (options.count === 0) return Promise.resolve([])
1552
6b842050
C
1553 const { query, replacements, order } = buildListQuery(VideoModel, options)
1554 const queryModels = wrapForAPIResults(query, replacements, options, order)
1555
1556 return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
1557 .then(rows => VideoModel.buildAPIResult(rows))
1558 }
1559
1560 const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
afd2cba5 1561
ddc07312
C
1562 return {
1563 data: rows,
1564 total: count
1565 }
1566 }
1567
6b842050 1568 private static buildAPIResult (rows: any[]) {
97816649
C
1569 const videosMemo: { [ id: number ]: VideoModel } = {}
1570 const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {}
6b842050
C
1571
1572 const thumbnailsDone = new Set<number>()
1573 const historyDone = new Set<number>()
1574 const videoFilesDone = new Set<number>()
1575
1576 const videos: VideoModel[] = []
1577
1578 const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ]
1579 const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
1580 const serverKeys = [ 'id', 'host' ]
1581 const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ]
97816649 1582 const videoStreamingPlaylistKeys = [ 'id' ]
6b842050
C
1583 const videoKeys = [
1584 'id',
1585 'uuid',
1586 'name',
1587 'category',
1588 'licence',
1589 'language',
1590 'privacy',
1591 'nsfw',
1592 'description',
1593 'support',
1594 'duration',
1595 'views',
1596 'likes',
1597 'dislikes',
1598 'remote',
1599 'url',
1600 'commentsEnabled',
1601 'downloadEnabled',
1602 'waitTranscoding',
1603 'state',
1604 'publishedAt',
1605 'originallyPublishedAt',
1606 'channelId',
1607 'createdAt',
1608 'updatedAt'
1609 ]
afd2cba5 1610
6b842050
C
1611 function buildActor (rowActor: any) {
1612 const avatarModel = rowActor.Avatar.id !== null
1613 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
1614 : null
df0b219d 1615
6b842050
C
1616 const serverModel = rowActor.Server.id !== null
1617 ? new ServerModel(pick(rowActor.Server, serverKeys))
1618 : null
df0b219d 1619
6b842050
C
1620 const actorModel = new ActorModel(pick(rowActor, actorKeys))
1621 actorModel.Avatar = avatarModel
1622 actorModel.Server = serverModel
1623
1624 return actorModel
df0b219d
C
1625 }
1626
6b842050 1627 for (const row of rows) {
97816649 1628 if (!videosMemo[row.id]) {
6b842050
C
1629 // Build Channel
1630 const channel = row.VideoChannel
1631 const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
1632 channelModel.Actor = buildActor(channel.Actor)
1633
1634 const account = row.VideoChannel.Account
1635 const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
1636 accountModel.Actor = buildActor(account.Actor)
1637
1638 channelModel.Account = accountModel
1639
1640 const videoModel = new VideoModel(pick(row, videoKeys))
1641 videoModel.VideoChannel = channelModel
1642
1643 videoModel.UserVideoHistories = []
1644 videoModel.Thumbnails = []
1645 videoModel.VideoFiles = []
97816649 1646 videoModel.VideoStreamingPlaylists = []
6b842050 1647
97816649 1648 videosMemo[row.id] = videoModel
6b842050
C
1649 // Don't take object value to have a sorted array
1650 videos.push(videoModel)
1651 }
1652
97816649 1653 const videoModel = videosMemo[row.id]
6b842050
C
1654
1655 if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
1656 const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
1657 videoModel.UserVideoHistories.push(historyModel)
1658
1659 historyDone.add(row.userVideoHistory.id)
1660 }
1661
1662 if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
1663 const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]))
1664 videoModel.Thumbnails.push(thumbnailModel)
1665
1666 thumbnailsDone.add(row.Thumbnails.id)
1667 }
1668
1669 if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
1670 const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
1671 videoModel.VideoFiles.push(videoFileModel)
1672
1673 videoFilesDone.add(row.VideoFiles.id)
1674 }
97816649 1675
97816649
C
1676 if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) {
1677 const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys))
1678 streamingPlaylist.VideoFiles = []
1679
1680 videoModel.VideoStreamingPlaylists.push(streamingPlaylist)
1681
1682 videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist
1683 }
1684
1685 if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) {
1686 const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]
1687
1688 const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys))
1689 streamingPlaylist.VideoFiles.push(videoFileModel)
1690
1691 videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id)
1692 }
6b842050 1693 }
df0b219d 1694
6b842050 1695 return videos
afd2cba5 1696 }
066e94c5 1697
098eb377 1698 static getCategoryLabel (id: number) {
a1587156 1699 return VIDEO_CATEGORIES[id] || 'Misc'
ae5a3dd6
C
1700 }
1701
098eb377 1702 static getLicenceLabel (id: number) {
a1587156 1703 return VIDEO_LICENCES[id] || 'Unknown'
ae5a3dd6
C
1704 }
1705
098eb377 1706 static getLanguageLabel (id: string) {
a1587156 1707 return VIDEO_LANGUAGES[id] || 'Unknown'
ae5a3dd6
C
1708 }
1709
098eb377 1710 static getPrivacyLabel (id: number) {
a1587156 1711 return VIDEO_PRIVACIES[id] || 'Unknown'
2186386c 1712 }
2243730c 1713
098eb377 1714 static getStateLabel (id: number) {
a1587156 1715 return VIDEO_STATES[id] || 'Unknown'
2243730c
C
1716 }
1717
5b77537c
C
1718 isBlacklisted () {
1719 return !!this.VideoBlacklist
1720 }
1721
bfbd9128 1722 isBlocked () {
faa9d434 1723 return this.VideoChannel.Account.Actor.Server?.isBlocked() || this.VideoChannel.Account.isBlocked()
bfbd9128
C
1724 }
1725
a1587156 1726 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
d7a25329 1727 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
92e0f42e 1728 const file = fun(this.VideoFiles, file => file.resolution)
d7a25329
C
1729
1730 return Object.assign(file, { Video: this })
1731 }
1732
1733 // No webtorrent files, try with streaming playlist files
1734 if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
1735 const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
1736
92e0f42e 1737 const file = fun(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
d7a25329
C
1738 return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
1739 }
aaf61f38 1740
d7a25329 1741 return undefined
e4f97bab 1742 }
aaf61f38 1743
a1587156 1744 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1745 return this.getQualityFileBy(maxBy)
1746 }
1747
a1587156 1748 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1749 return this.getQualityFileBy(minBy)
1750 }
1751
a1587156 1752 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
29d4e137
C
1753 if (Array.isArray(this.VideoFiles) === false) return undefined
1754
d7a25329
C
1755 const file = this.VideoFiles.find(f => f.resolution === resolution)
1756 if (!file) return undefined
1757
1758 return Object.assign(file, { Video: this })
29d4e137
C
1759 }
1760
453e83ea 1761 async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
3acc5084
C
1762 thumbnail.videoId = this.id
1763
1764 const savedThumbnail = await thumbnail.save({ transaction })
1765
e8bafea3
C
1766 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1767
1768 // Already have this thumbnail, skip
3acc5084 1769 if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
e8bafea3 1770
3acc5084 1771 this.Thumbnails.push(savedThumbnail)
e8bafea3
C
1772 }
1773
e8bafea3
C
1774 generateThumbnailName () {
1775 return this.uuid + '.jpg'
7b1f49de
C
1776 }
1777
3acc5084 1778 getMiniature () {
e8bafea3
C
1779 if (Array.isArray(this.Thumbnails) === false) return undefined
1780
3acc5084 1781 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
e8bafea3
C
1782 }
1783
1784 generatePreviewName () {
1785 return this.uuid + '.jpg'
1786 }
1787
6872996d
C
1788 hasPreview () {
1789 return !!this.getPreview()
1790 }
1791
e8bafea3
C
1792 getPreview () {
1793 if (Array.isArray(this.Thumbnails) === false) return undefined
1794
1795 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
3fd3ab2d 1796 }
7b1f49de 1797
3fd3ab2d
C
1798 isOwned () {
1799 return this.remote === false
9567011b
C
1800 }
1801
cef534ed
C
1802 getWatchStaticPath () {
1803 return '/videos/watch/' + this.uuid
1804 }
1805
40e87e9e 1806 getEmbedStaticPath () {
3fd3ab2d
C
1807 return '/videos/embed/' + this.uuid
1808 }
e4f97bab 1809
3acc5084
C
1810 getMiniatureStaticPath () {
1811 const thumbnail = this.getMiniature()
e8bafea3
C
1812 if (!thumbnail) return null
1813
1814 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
e4f97bab 1815 }
227d02fe 1816
40e87e9e 1817 getPreviewStaticPath () {
e8bafea3
C
1818 const preview = this.getPreview()
1819 if (!preview) return null
1820
1821 // We use a local cache, so specify our cache endpoint instead of potential remote URL
557b13ae 1822 return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
3fd3ab2d 1823 }
40298b02 1824
b5fecbf4 1825 toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
098eb377 1826 return videoModelToFormattedJSON(this, options)
14d3270f 1827 }
14d3270f 1828
b5fecbf4 1829 toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
098eb377 1830 return videoModelToFormattedDetailsJSON(this)
244e76a5
RK
1831 }
1832
1833 getFormattedVideoFilesJSON (): VideoFile[] {
d7a25329 1834 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
97816649
C
1835 let files: MVideoFileRedundanciesOpt[] = []
1836
97816649
C
1837 if (Array.isArray(this.VideoFiles)) {
1838 files = files.concat(this.VideoFiles)
1839 }
1840
1841 for (const p of (this.VideoStreamingPlaylists || [])) {
1842 files = files.concat(p.VideoFiles || [])
1843 }
1844
97816649 1845 return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, files)
3fd3ab2d 1846 }
e4f97bab 1847
de6310b2 1848 toActivityPubObject (this: MVideoAP): VideoObject {
098eb377 1849 return videoModelToActivityPubObject(this)
3fd3ab2d
C
1850 }
1851
1852 getTruncatedDescription () {
1853 if (!this.description) return null
93e1258c 1854
bffbebbe 1855 const maxLength = CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
687c6180 1856 return peertubeTruncate(this.description, { length: maxLength })
93e1258c
C
1857 }
1858
d7a25329
C
1859 getMaxQualityResolution () {
1860 const file = this.getMaxQualityFile()
1861 const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
1862 const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
0d0e8dd0 1863
056aa7f2 1864 return getVideoFileResolution(originalFilePath)
3fd3ab2d 1865 }
0d0e8dd0 1866
96f29c0f 1867 getDescriptionAPIPath () {
3fd3ab2d 1868 return `/api/${API_VERSION}/videos/${this.uuid}/description`
feb4bdfd
C
1869 }
1870
d7a25329 1871 getHLSPlaylist (): MStreamingPlaylistFilesVideo {
e2600d8b
C
1872 if (!this.VideoStreamingPlaylists) return undefined
1873
d7a25329
C
1874 const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
1875 playlist.Video = this
1876
1877 return playlist
e2600d8b
C
1878 }
1879
d7a25329
C
1880 setHLSPlaylist (playlist: MStreamingPlaylist) {
1881 const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
b9fffa29 1882
d7a25329
C
1883 if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
1884 this.VideoStreamingPlaylists = toAdd
1885 return
1886 }
1887
1888 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
a1587156
C
1889 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1890 .concat(toAdd)
d7a25329
C
1891 }
1892
1893 removeFile (videoFile: MVideoFile, isRedundancy = false) {
1894 const filePath = getVideoFilePath(this, videoFile, isRedundancy)
62689b94 1895 return remove(filePath)
ed31c059 1896 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
feb4bdfd
C
1897 }
1898
453e83ea 1899 removeTorrent (videoFile: MVideoFile) {
d7a25329 1900 const torrentPath = getTorrentFilePath(this, videoFile)
62689b94 1901 return remove(torrentPath)
ed31c059 1902 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
aaf61f38
C
1903 }
1904
ffc65cbd 1905 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
66fb2aa3 1906 const directoryPath = getHLSDirectory(this, isRedundancy)
09209296 1907
ffc65cbd
C
1908 await remove(directoryPath)
1909
1910 if (isRedundancy !== true) {
a1587156 1911 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
ffc65cbd
C
1912 streamingPlaylistWithFiles.Video = this
1913
1914 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
1915 streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
1916 }
1917
1918 // Remove physical files and torrents
1919 await Promise.all(
1920 streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file))
1921 )
1922 }
09209296
C
1923 }
1924
1297eb5d
C
1925 isOutdated () {
1926 if (this.isOwned()) return false
1927
9f79ade6 1928 return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
1297eb5d
C
1929 }
1930
22a73cb8 1931 hasPrivacyForFederation () {
3092e9bb 1932 return isPrivacyForFederation(this.privacy)
22a73cb8
C
1933 }
1934
68e70a74
C
1935 hasStateForFederation () {
1936 return isStateForFederation(this.state)
1937 }
1938
22a73cb8 1939 isNewVideo (newPrivacy: VideoPrivacy) {
3092e9bb 1940 return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true
22a73cb8
C
1941 }
1942
04b8c3fb
C
1943 setAsRefreshed () {
1944 this.changed('updatedAt', true)
1945
1946 return this.save()
1947 }
1948
22a73cb8
C
1949 requiresAuth () {
1950 return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
1951 }
1952
1953 setPrivacy (newPrivacy: VideoPrivacy) {
1954 if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) {
1955 this.publishedAt = new Date()
1956 }
1957
1958 this.privacy = newPrivacy
1959 }
1960
1961 isConfidential () {
1962 return this.privacy === VideoPrivacy.PRIVATE ||
1963 this.privacy === VideoPrivacy.UNLISTED ||
1964 this.privacy === VideoPrivacy.INTERNAL
1965 }
1966
d7a25329
C
1967 async publishIfNeededAndSave (t: Transaction) {
1968 if (this.state !== VideoState.PUBLISHED) {
1969 this.state = VideoState.PUBLISHED
1970 this.publishedAt = new Date()
1971 await this.save({ transaction: t })
7920c273 1972
d7a25329 1973 return true
6fcd19ba 1974 }
aaf61f38 1975
d7a25329 1976 return false
15d4ee04 1977 }
a96aed15 1978
d7a25329
C
1979 getBaseUrls () {
1980 if (this.isOwned()) {
1981 return {
1982 baseUrlHttp: WEBSERVER.URL,
1983 baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
1984 }
c48e82b5
C
1985 }
1986
d7a25329
C
1987 return {
1988 baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
1989 baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
1990 }
c48e82b5
C
1991 }
1992
09209296
C
1993 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1994 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1995 }
1996
453e83ea 1997 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1998 return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
3fd3ab2d 1999 }
e4f97bab 2000
453e83ea 2001 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 2002 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
02756fbd
C
2003 }
2004
453e83ea 2005 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 2006 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
3fd3ab2d 2007 }
a96aed15 2008
8319d6ae
RK
2009 getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2010 const path = '/api/v1/videos/'
7b81edc8
C
2011
2012 return this.isOwned()
8319d6ae
RK
2013 ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
2014 : videoFile.metadataUrl
2015 }
2016
453e83ea 2017 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 2018 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
b9fffa29
C
2019 }
2020
453e83ea 2021 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 2022 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
02756fbd 2023 }
09209296 2024
453e83ea 2025 getBandwidthBits (videoFile: MVideoFile) {
09209296
C
2026 return Math.ceil((videoFile.size * 8) / this.duration)
2027 }
a96aed15 2028}