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