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