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