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