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