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