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