]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video.ts
Add max lives limit
[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
a056ca48
C
1145 static countLocalLives () {
1146 const options = {
1147 where: {
1148 remote: false,
1149 isLive: true
1150 }
1151 }
1152
1153 return VideoModel.count(options)
1154 }
1155
1156 static countLivesOfAccount (accountId: number) {
1157 const options = {
1158 where: {
1159 remote: false,
1160 isLive: true
1161 },
1162 include: [
1163 {
1164 required: true,
1165 model: VideoChannelModel.unscoped(),
1166 where: {
1167 accountId
1168 }
1169 }
1170 ]
1171 }
1172
1173 return VideoModel.count(options)
1174 }
1175
453e83ea 1176 static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
418d092a 1177 const where = buildWhereIdOrUUID(id)
627621c1
C
1178 const options = {
1179 where,
1180 transaction: t
3fd3ab2d 1181 }
d8755eed 1182
e8bafea3 1183 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
3fd3ab2d 1184 }
d8755eed 1185
d636ab58
C
1186 static loadWithBlacklist (id: number | string, t?: Transaction): Bluebird<MVideoThumbnailBlacklist> {
1187 const where = buildWhereIdOrUUID(id)
1188 const options = {
1189 where,
1190 transaction: t
1191 }
1192
1193 return VideoModel.scope([
1194 ScopeNames.WITH_THUMBNAILS,
1195 ScopeNames.WITH_BLACKLISTED
1196 ]).findOne(options)
1197 }
1198
7eba5e1f
C
1199 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1200 const fun = () => {
943e5193
C
1201 const query = {
1202 where: buildWhereIdOrUUID(id),
7eba5e1f
C
1203 transaction: t
1204 }
1205
943e5193 1206 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
7eba5e1f
C
1207 }
1208
1209 return ModelCache.Instance.doCache({
943e5193 1210 cacheType: 'load-video-immutable-id',
7eba5e1f
C
1211 key: '' + id,
1212 deleteKey: 'video',
1213 fun
1214 })
1215 }
1216
453e83ea 1217 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
418d092a 1218 const where = buildWhereIdOrUUID(id)
09209296
C
1219 const options = {
1220 where,
1221 transaction: t
1222 }
1223
e8bafea3
C
1224 return VideoModel.scope([
1225 ScopeNames.WITH_BLACKLISTED,
1226 ScopeNames.WITH_USER_ID,
1227 ScopeNames.WITH_THUMBNAILS
1228 ]).findOne(options)
09209296
C
1229 }
1230
453e83ea 1231 static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
418d092a 1232 const where = buildWhereIdOrUUID(id)
627621c1 1233
3fd3ab2d 1234 const options = {
627621c1
C
1235 attributes: [ 'id' ],
1236 where,
1237 transaction: t
3fd3ab2d 1238 }
72c7248b 1239
e8bafea3 1240 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
627621c1
C
1241 }
1242
453e83ea 1243 static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
e2600d8b
C
1244 const where = buildWhereIdOrUUID(id)
1245
1246 const query = {
1247 where,
1248 transaction: t,
1249 logging
1250 }
1251
e8bafea3 1252 return VideoModel.scope([
d7a25329 1253 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
1254 ScopeNames.WITH_STREAMING_PLAYLISTS,
1255 ScopeNames.WITH_THUMBNAILS
e2600d8b 1256 ]).findOne(query)
3fd3ab2d 1257 }
72c7248b 1258
453e83ea 1259 static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
8fa5653a
C
1260 const options = {
1261 where: {
1262 uuid
1263 }
1264 }
1265
e8bafea3 1266 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
8fa5653a
C
1267 }
1268
453e83ea 1269 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
1735c825 1270 const query: FindOptions = {
627621c1
C
1271 where: {
1272 url
4157cdb1
C
1273 },
1274 transaction
627621c1
C
1275 }
1276
e8bafea3 1277 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
4157cdb1
C
1278 }
1279
943e5193
C
1280 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1281 const fun = () => {
1282 const query: FindOptions = {
1283 where: {
1284 url
1285 },
1286 transaction
1287 }
1288
1289 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1290 }
1291
1292 return ModelCache.Instance.doCache({
1293 cacheType: 'load-video-immutable-url',
1294 key: url,
1295 deleteKey: 'video',
1296 fun
1297 })
1298 }
1299
0283eaac 1300 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1735c825 1301 const query: FindOptions = {
4157cdb1
C
1302 where: {
1303 url
1304 },
1305 transaction
1306 }
627621c1 1307
09209296
C
1308 return VideoModel.scope([
1309 ScopeNames.WITH_ACCOUNT_DETAILS,
d7a25329 1310 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3 1311 ScopeNames.WITH_STREAMING_PLAYLISTS,
453e83ea
C
1312 ScopeNames.WITH_THUMBNAILS,
1313 ScopeNames.WITH_BLACKLISTED
09209296 1314 ]).findOne(query)
627621c1
C
1315 }
1316
453e83ea 1317 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
418d092a 1318 const where = buildWhereIdOrUUID(id)
627621c1 1319
3fd3ab2d 1320 const options = {
3acc5084 1321 order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
627621c1 1322 where,
2186386c 1323 transaction: t
3fd3ab2d 1324 }
fd45e8f4 1325
3acc5084 1326 const scopes: (string | ScopeOptions)[] = [
6e46de09
C
1327 ScopeNames.WITH_TAGS,
1328 ScopeNames.WITH_BLACKLISTED,
09209296
C
1329 ScopeNames.WITH_ACCOUNT_DETAILS,
1330 ScopeNames.WITH_SCHEDULED_UPDATE,
d7a25329 1331 ScopeNames.WITH_WEBTORRENT_FILES,
e8bafea3
C
1332 ScopeNames.WITH_STREAMING_PLAYLISTS,
1333 ScopeNames.WITH_THUMBNAILS
09209296
C
1334 ]
1335
1336 if (userId) {
3acc5084 1337 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
09209296
C
1338 }
1339
1340 return VideoModel
1341 .scope(scopes)
1342 .findOne(options)
1343 }
1344
89cd1275 1345 static loadForGetAPI (parameters: {
a1587156
C
1346 id: number | string
1347 t?: Transaction
89cd1275 1348 userId?: number
453e83ea 1349 }): Bluebird<MVideoDetails> {
89cd1275 1350 const { id, t, userId } = parameters
418d092a 1351 const where = buildWhereIdOrUUID(id)
09209296
C
1352
1353 const options = {
1735c825 1354 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
09209296
C
1355 where,
1356 transaction: t
1357 }
1358
3acc5084 1359 const scopes: (string | ScopeOptions)[] = [
09209296
C
1360 ScopeNames.WITH_TAGS,
1361 ScopeNames.WITH_BLACKLISTED,
6e46de09 1362 ScopeNames.WITH_ACCOUNT_DETAILS,
09209296 1363 ScopeNames.WITH_SCHEDULED_UPDATE,
e8bafea3 1364 ScopeNames.WITH_THUMBNAILS,
d7a25329 1365 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
3acc5084 1366 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
6e46de09
C
1367 ]
1368
1369 if (userId) {
3acc5084 1370 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
6e46de09
C
1371 }
1372
d48ff09d 1373 return VideoModel
6e46de09 1374 .scope(scopes)
da854ddd
C
1375 .findOne(options)
1376 }
1377
09cababd
C
1378 static async getStats () {
1379 const totalLocalVideos = await VideoModel.count({
1380 where: {
1381 remote: false
1382 }
1383 })
09cababd
C
1384
1385 let totalLocalVideoViews = await VideoModel.sum('views', {
1386 where: {
1387 remote: false
1388 }
1389 })
baab47ca 1390
09cababd
C
1391 // Sequelize could return null...
1392 if (!totalLocalVideoViews) totalLocalVideoViews = 0
1393
baab47ca
C
1394 const { total: totalVideos } = await VideoModel.listForApi({
1395 start: 0,
1396 count: 0,
1397 sort: '-publishedAt',
1398 nsfw: buildNSFWFilter(),
1399 includeLocalVideos: true,
1400 withFiles: false
1401 })
1402
09cababd
C
1403 return {
1404 totalLocalVideos,
1405 totalLocalVideoViews,
1406 totalVideos
1407 }
1408 }
1409
6b616860
C
1410 static incrementViews (id: number, views: number) {
1411 return VideoModel.increment('views', {
1412 by: views,
1413 where: {
1414 id
1415 }
1416 })
1417 }
1418
8d427346
C
1419 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1420 // Instances only share videos
1421 const query = 'SELECT 1 FROM "videoShare" ' +
a1587156 1422 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
f046e2fa 1423 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "videoShare"."videoId" = $videoId ' +
a1587156 1424 'LIMIT 1'
8d427346
C
1425
1426 const options = {
d5d9b6d7 1427 type: QueryTypes.SELECT as QueryTypes.SELECT,
8d427346
C
1428 bind: { followerActorId, videoId },
1429 raw: true
1430 }
1431
1432 return VideoModel.sequelize.query(query, options)
1433 .then(results => results.length === 1)
1434 }
1435
453e83ea 1436 static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
7d14d4d2
C
1437 const options = {
1438 where: {
1439 channelId: videoChannel.id
1440 },
1441 transaction: t
1442 }
1443
1444 return VideoModel.update({ support: videoChannel.support }, options)
1445 }
1446
453e83ea 1447 static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
7d14d4d2
C
1448 const query = {
1449 attributes: [ 'id' ],
1450 where: {
1451 channelId: videoChannel.id
1452 }
1453 }
1454
1455 return VideoModel.findAll(query)
a1587156 1456 .then(videos => videos.map(v => v.id))
7d14d4d2
C
1457 }
1458
2d3741d6 1459 // threshold corresponds to how many video the field should have to be returned
7348b1fd 1460 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
65b21c96 1461 const serverActor = await getServerActor()
4e74e803 1462 const followerActorId = serverActor.id
7348b1fd 1463
5f3e2425
C
1464 const queryOptions: BuildVideosQueryOptions = {
1465 attributes: [ `"${field}"` ],
1466 group: `GROUP BY "${field}"`,
1467 having: `HAVING COUNT("${field}") >= ${threshold}`,
1468 start: 0,
1469 sort: 'random',
1470 count,
65b21c96 1471 serverAccountId: serverActor.Account.id,
4e74e803 1472 followerActorId,
5f3e2425 1473 includeLocalVideos: true
7348b1fd
C
1474 }
1475
5f3e2425 1476 const { query, replacements } = buildListQuery(VideoModel, queryOptions)
2d3741d6 1477
5f3e2425
C
1478 return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
1479 .then(rows => rows.map(r => r[field]))
2d3741d6
C
1480 }
1481
b36f41ca
C
1482 static buildTrendingQuery (trendingDays: number) {
1483 return {
1484 attributes: [],
1485 subQuery: false,
1486 model: VideoViewModel,
1487 required: false,
1488 where: {
1489 startDate: {
a1587156 1490 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
b36f41ca
C
1491 }
1492 }
1493 }
1494 }
1495
6e46de09 1496 private static async getAvailableForApi (
5f3e2425 1497 options: BuildVideosQueryOptions,
6e46de09 1498 countVideos = true
b84d4c80 1499 ): Promise<ResultList<VideoModel>> {
6b842050
C
1500 function getCount () {
1501 if (countVideos !== true) return Promise.resolve(undefined)
8ea6f49a 1502
6b842050
C
1503 const countOptions = Object.assign({}, options, { isCount: true })
1504 const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
3caf77d3 1505
6b842050
C
1506 return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
1507 .then(rows => rows.length !== 0 ? rows[0].total : 0)
1508 }
1509
1510 function getModels () {
baab47ca
C
1511 if (options.count === 0) return Promise.resolve([])
1512
6b842050
C
1513 const { query, replacements, order } = buildListQuery(VideoModel, options)
1514 const queryModels = wrapForAPIResults(query, replacements, options, order)
1515
1516 return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
1517 .then(rows => VideoModel.buildAPIResult(rows))
1518 }
1519
1520 const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
afd2cba5 1521
ddc07312
C
1522 return {
1523 data: rows,
1524 total: count
1525 }
1526 }
1527
6b842050 1528 private static buildAPIResult (rows: any[]) {
97816649
C
1529 const videosMemo: { [ id: number ]: VideoModel } = {}
1530 const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {}
6b842050
C
1531
1532 const thumbnailsDone = new Set<number>()
1533 const historyDone = new Set<number>()
1534 const videoFilesDone = new Set<number>()
1535
1536 const videos: VideoModel[] = []
1537
1538 const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ]
1539 const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
1540 const serverKeys = [ 'id', 'host' ]
1541 const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ]
97816649 1542 const videoStreamingPlaylistKeys = [ 'id' ]
6b842050
C
1543 const videoKeys = [
1544 'id',
1545 'uuid',
1546 'name',
1547 'category',
1548 'licence',
1549 'language',
1550 'privacy',
1551 'nsfw',
1552 'description',
1553 'support',
1554 'duration',
1555 'views',
1556 'likes',
1557 'dislikes',
1558 'remote',
1559 'url',
1560 'commentsEnabled',
1561 'downloadEnabled',
1562 'waitTranscoding',
1563 'state',
1564 'publishedAt',
1565 'originallyPublishedAt',
1566 'channelId',
1567 'createdAt',
1568 'updatedAt'
1569 ]
afd2cba5 1570
6b842050
C
1571 function buildActor (rowActor: any) {
1572 const avatarModel = rowActor.Avatar.id !== null
1573 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
1574 : null
df0b219d 1575
6b842050
C
1576 const serverModel = rowActor.Server.id !== null
1577 ? new ServerModel(pick(rowActor.Server, serverKeys))
1578 : null
df0b219d 1579
6b842050
C
1580 const actorModel = new ActorModel(pick(rowActor, actorKeys))
1581 actorModel.Avatar = avatarModel
1582 actorModel.Server = serverModel
1583
1584 return actorModel
df0b219d
C
1585 }
1586
6b842050 1587 for (const row of rows) {
97816649 1588 if (!videosMemo[row.id]) {
6b842050
C
1589 // Build Channel
1590 const channel = row.VideoChannel
1591 const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
1592 channelModel.Actor = buildActor(channel.Actor)
1593
1594 const account = row.VideoChannel.Account
1595 const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
1596 accountModel.Actor = buildActor(account.Actor)
1597
1598 channelModel.Account = accountModel
1599
1600 const videoModel = new VideoModel(pick(row, videoKeys))
1601 videoModel.VideoChannel = channelModel
1602
1603 videoModel.UserVideoHistories = []
1604 videoModel.Thumbnails = []
1605 videoModel.VideoFiles = []
97816649 1606 videoModel.VideoStreamingPlaylists = []
6b842050 1607
97816649 1608 videosMemo[row.id] = videoModel
6b842050
C
1609 // Don't take object value to have a sorted array
1610 videos.push(videoModel)
1611 }
1612
97816649 1613 const videoModel = videosMemo[row.id]
6b842050
C
1614
1615 if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
1616 const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
1617 videoModel.UserVideoHistories.push(historyModel)
1618
1619 historyDone.add(row.userVideoHistory.id)
1620 }
1621
1622 if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
1623 const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]))
1624 videoModel.Thumbnails.push(thumbnailModel)
1625
1626 thumbnailsDone.add(row.Thumbnails.id)
1627 }
1628
1629 if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
1630 const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
1631 videoModel.VideoFiles.push(videoFileModel)
1632
1633 videoFilesDone.add(row.VideoFiles.id)
1634 }
97816649
C
1635
1636 if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
1637 const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
1638 videoModel.VideoFiles.push(videoFileModel)
1639
1640 videoFilesDone.add(row.VideoFiles.id)
1641 }
1642
1643 if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) {
1644 const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys))
1645 streamingPlaylist.VideoFiles = []
1646
1647 videoModel.VideoStreamingPlaylists.push(streamingPlaylist)
1648
1649 videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist
1650 }
1651
1652 if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) {
1653 const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]
1654
1655 const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys))
1656 streamingPlaylist.VideoFiles.push(videoFileModel)
1657
1658 videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id)
1659 }
6b842050 1660 }
df0b219d 1661
6b842050 1662 return videos
afd2cba5 1663 }
066e94c5 1664
098eb377 1665 static getCategoryLabel (id: number) {
a1587156 1666 return VIDEO_CATEGORIES[id] || 'Misc'
ae5a3dd6
C
1667 }
1668
098eb377 1669 static getLicenceLabel (id: number) {
a1587156 1670 return VIDEO_LICENCES[id] || 'Unknown'
ae5a3dd6
C
1671 }
1672
098eb377 1673 static getLanguageLabel (id: string) {
a1587156 1674 return VIDEO_LANGUAGES[id] || 'Unknown'
ae5a3dd6
C
1675 }
1676
098eb377 1677 static getPrivacyLabel (id: number) {
a1587156 1678 return VIDEO_PRIVACIES[id] || 'Unknown'
2186386c 1679 }
2243730c 1680
098eb377 1681 static getStateLabel (id: number) {
a1587156 1682 return VIDEO_STATES[id] || 'Unknown'
2243730c
C
1683 }
1684
5b77537c
C
1685 isBlacklisted () {
1686 return !!this.VideoBlacklist
1687 }
1688
bfbd9128 1689 isBlocked () {
faa9d434 1690 return this.VideoChannel.Account.Actor.Server?.isBlocked() || this.VideoChannel.Account.isBlocked()
bfbd9128
C
1691 }
1692
a1587156 1693 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
d7a25329 1694 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
92e0f42e 1695 const file = fun(this.VideoFiles, file => file.resolution)
d7a25329
C
1696
1697 return Object.assign(file, { Video: this })
1698 }
1699
1700 // No webtorrent files, try with streaming playlist files
1701 if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
1702 const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
1703
92e0f42e 1704 const file = fun(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
d7a25329
C
1705 return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
1706 }
aaf61f38 1707
d7a25329 1708 return undefined
e4f97bab 1709 }
aaf61f38 1710
a1587156 1711 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1712 return this.getQualityFileBy(maxBy)
1713 }
1714
a1587156 1715 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
92e0f42e
C
1716 return this.getQualityFileBy(minBy)
1717 }
1718
a1587156 1719 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
29d4e137
C
1720 if (Array.isArray(this.VideoFiles) === false) return undefined
1721
d7a25329
C
1722 const file = this.VideoFiles.find(f => f.resolution === resolution)
1723 if (!file) return undefined
1724
1725 return Object.assign(file, { Video: this })
29d4e137
C
1726 }
1727
453e83ea 1728 async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
3acc5084
C
1729 thumbnail.videoId = this.id
1730
1731 const savedThumbnail = await thumbnail.save({ transaction })
1732
e8bafea3
C
1733 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1734
1735 // Already have this thumbnail, skip
3acc5084 1736 if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
e8bafea3 1737
3acc5084 1738 this.Thumbnails.push(savedThumbnail)
e8bafea3
C
1739 }
1740
e8bafea3
C
1741 generateThumbnailName () {
1742 return this.uuid + '.jpg'
7b1f49de
C
1743 }
1744
3acc5084 1745 getMiniature () {
e8bafea3
C
1746 if (Array.isArray(this.Thumbnails) === false) return undefined
1747
3acc5084 1748 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
e8bafea3
C
1749 }
1750
1751 generatePreviewName () {
1752 return this.uuid + '.jpg'
1753 }
1754
6872996d
C
1755 hasPreview () {
1756 return !!this.getPreview()
1757 }
1758
e8bafea3
C
1759 getPreview () {
1760 if (Array.isArray(this.Thumbnails) === false) return undefined
1761
1762 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
3fd3ab2d 1763 }
7b1f49de 1764
3fd3ab2d
C
1765 isOwned () {
1766 return this.remote === false
9567011b
C
1767 }
1768
cef534ed
C
1769 getWatchStaticPath () {
1770 return '/videos/watch/' + this.uuid
1771 }
1772
40e87e9e 1773 getEmbedStaticPath () {
3fd3ab2d
C
1774 return '/videos/embed/' + this.uuid
1775 }
e4f97bab 1776
3acc5084
C
1777 getMiniatureStaticPath () {
1778 const thumbnail = this.getMiniature()
e8bafea3
C
1779 if (!thumbnail) return null
1780
1781 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
e4f97bab 1782 }
227d02fe 1783
40e87e9e 1784 getPreviewStaticPath () {
e8bafea3
C
1785 const preview = this.getPreview()
1786 if (!preview) return null
1787
1788 // We use a local cache, so specify our cache endpoint instead of potential remote URL
557b13ae 1789 return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
3fd3ab2d 1790 }
40298b02 1791
b5fecbf4 1792 toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
098eb377 1793 return videoModelToFormattedJSON(this, options)
14d3270f 1794 }
14d3270f 1795
b5fecbf4 1796 toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
098eb377 1797 return videoModelToFormattedDetailsJSON(this)
244e76a5
RK
1798 }
1799
1800 getFormattedVideoFilesJSON (): VideoFile[] {
d7a25329 1801 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
97816649
C
1802 let files: MVideoFileRedundanciesOpt[] = []
1803
97816649
C
1804 if (Array.isArray(this.VideoFiles)) {
1805 files = files.concat(this.VideoFiles)
1806 }
1807
1808 for (const p of (this.VideoStreamingPlaylists || [])) {
1809 files = files.concat(p.VideoFiles || [])
1810 }
1811
97816649 1812 return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, files)
3fd3ab2d 1813 }
e4f97bab 1814
de6310b2 1815 toActivityPubObject (this: MVideoAP): VideoObject {
098eb377 1816 return videoModelToActivityPubObject(this)
3fd3ab2d
C
1817 }
1818
1819 getTruncatedDescription () {
1820 if (!this.description) return null
93e1258c 1821
bffbebbe 1822 const maxLength = CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
687c6180 1823 return peertubeTruncate(this.description, { length: maxLength })
93e1258c
C
1824 }
1825
d7a25329
C
1826 getMaxQualityResolution () {
1827 const file = this.getMaxQualityFile()
1828 const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
1829 const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
0d0e8dd0 1830
056aa7f2 1831 return getVideoFileResolution(originalFilePath)
3fd3ab2d 1832 }
0d0e8dd0 1833
96f29c0f 1834 getDescriptionAPIPath () {
3fd3ab2d 1835 return `/api/${API_VERSION}/videos/${this.uuid}/description`
feb4bdfd
C
1836 }
1837
d7a25329 1838 getHLSPlaylist (): MStreamingPlaylistFilesVideo {
e2600d8b
C
1839 if (!this.VideoStreamingPlaylists) return undefined
1840
d7a25329
C
1841 const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
1842 playlist.Video = this
1843
1844 return playlist
e2600d8b
C
1845 }
1846
d7a25329
C
1847 setHLSPlaylist (playlist: MStreamingPlaylist) {
1848 const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
b9fffa29 1849
d7a25329
C
1850 if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
1851 this.VideoStreamingPlaylists = toAdd
1852 return
1853 }
1854
1855 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
a1587156
C
1856 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1857 .concat(toAdd)
d7a25329
C
1858 }
1859
1860 removeFile (videoFile: MVideoFile, isRedundancy = false) {
1861 const filePath = getVideoFilePath(this, videoFile, isRedundancy)
62689b94 1862 return remove(filePath)
ed31c059 1863 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
feb4bdfd
C
1864 }
1865
453e83ea 1866 removeTorrent (videoFile: MVideoFile) {
d7a25329 1867 const torrentPath = getTorrentFilePath(this, videoFile)
62689b94 1868 return remove(torrentPath)
ed31c059 1869 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
aaf61f38
C
1870 }
1871
ffc65cbd 1872 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
66fb2aa3 1873 const directoryPath = getHLSDirectory(this, isRedundancy)
09209296 1874
ffc65cbd
C
1875 await remove(directoryPath)
1876
1877 if (isRedundancy !== true) {
a1587156 1878 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
ffc65cbd
C
1879 streamingPlaylistWithFiles.Video = this
1880
1881 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
1882 streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
1883 }
1884
1885 // Remove physical files and torrents
1886 await Promise.all(
1887 streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file))
1888 )
1889 }
09209296
C
1890 }
1891
1297eb5d
C
1892 isOutdated () {
1893 if (this.isOwned()) return false
1894
9f79ade6 1895 return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
1297eb5d
C
1896 }
1897
22a73cb8 1898 hasPrivacyForFederation () {
3092e9bb 1899 return isPrivacyForFederation(this.privacy)
22a73cb8
C
1900 }
1901
1902 isNewVideo (newPrivacy: VideoPrivacy) {
3092e9bb 1903 return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true
22a73cb8
C
1904 }
1905
04b8c3fb
C
1906 setAsRefreshed () {
1907 this.changed('updatedAt', true)
1908
1909 return this.save()
1910 }
1911
22a73cb8
C
1912 requiresAuth () {
1913 return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
1914 }
1915
1916 setPrivacy (newPrivacy: VideoPrivacy) {
1917 if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) {
1918 this.publishedAt = new Date()
1919 }
1920
1921 this.privacy = newPrivacy
1922 }
1923
1924 isConfidential () {
1925 return this.privacy === VideoPrivacy.PRIVATE ||
1926 this.privacy === VideoPrivacy.UNLISTED ||
1927 this.privacy === VideoPrivacy.INTERNAL
1928 }
1929
d7a25329
C
1930 async publishIfNeededAndSave (t: Transaction) {
1931 if (this.state !== VideoState.PUBLISHED) {
1932 this.state = VideoState.PUBLISHED
1933 this.publishedAt = new Date()
1934 await this.save({ transaction: t })
7920c273 1935
d7a25329 1936 return true
6fcd19ba 1937 }
aaf61f38 1938
d7a25329 1939 return false
15d4ee04 1940 }
a96aed15 1941
d7a25329
C
1942 getBaseUrls () {
1943 if (this.isOwned()) {
1944 return {
1945 baseUrlHttp: WEBSERVER.URL,
1946 baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
1947 }
c48e82b5
C
1948 }
1949
d7a25329
C
1950 return {
1951 baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
1952 baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
1953 }
c48e82b5
C
1954 }
1955
09209296
C
1956 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1957 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1958 }
1959
453e83ea 1960 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1961 return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
3fd3ab2d 1962 }
e4f97bab 1963
453e83ea 1964 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1965 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
02756fbd
C
1966 }
1967
453e83ea 1968 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1969 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
3fd3ab2d 1970 }
a96aed15 1971
8319d6ae
RK
1972 getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1973 const path = '/api/v1/videos/'
7b81edc8
C
1974
1975 return this.isOwned()
8319d6ae
RK
1976 ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
1977 : videoFile.metadataUrl
1978 }
1979
453e83ea 1980 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1981 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
b9fffa29
C
1982 }
1983
453e83ea 1984 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
d7a25329 1985 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
02756fbd 1986 }
09209296 1987
453e83ea 1988 getBandwidthBits (videoFile: MVideoFile) {
09209296
C
1989 return Math.ceil((videoFile.size * 8) / this.duration)
1990 }
a96aed15 1991}