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