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