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