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