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