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