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