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