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