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