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