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