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