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