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