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