aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r--server/models/video/video.ts372
1 files changed, 204 insertions, 168 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index eacffe186..bd4ca63ea 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,18 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import { join } from 'path' 3import { join } from 'path'
4import { 4import { CountOptions, FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
5 CountOptions,
6 FindOptions,
7 IncludeOptions,
8 ModelIndexesOptions,
9 Op,
10 QueryTypes,
11 ScopeOptions,
12 Sequelize,
13 Transaction,
14 WhereOptions
15} from 'sequelize'
16import { 5import {
17 AllowNull, 6 AllowNull,
18 BeforeDestroy, 7 BeforeDestroy,
@@ -131,87 +120,19 @@ import {
131 MVideoFormattableDetails, 120 MVideoFormattableDetails,
132 MVideoForUser, 121 MVideoForUser,
133 MVideoFullLight, 122 MVideoFullLight,
134 MVideoIdThumbnail, 123 MVideoIdThumbnail, MVideoImmutable,
135 MVideoThumbnail, 124 MVideoThumbnail,
136 MVideoThumbnailBlacklist, 125 MVideoThumbnailBlacklist,
137 MVideoWithAllFiles, 126 MVideoWithAllFiles,
138 MVideoWithFile, 127 MVideoWithFile,
139 MVideoWithRights, 128 MVideoWithRights
140 MStreamingPlaylistFiles
141} from '../../typings/models' 129} from '../../typings/models'
142import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file' 130import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
143import { MThumbnail } from '../../typings/models/video/thumbnail' 131import { MThumbnail } from '../../typings/models/video/thumbnail'
144import { VideoFile } from '@shared/models/videos/video-file.model' 132import { VideoFile } from '@shared/models/videos/video-file.model'
145import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 133import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
146import validator from 'validator' 134import validator from 'validator'
147 135import { ModelCache } from '@server/models/model-cache'
148// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
149const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
150 buildTrigramSearchIndex('video_name_trigram', 'name'),
151
152 { fields: [ 'createdAt' ] },
153 {
154 fields: [
155 { name: 'publishedAt', order: 'DESC' },
156 { name: 'id', order: 'ASC' }
157 ]
158 },
159 { fields: [ 'duration' ] },
160 { fields: [ 'views' ] },
161 { fields: [ 'channelId' ] },
162 {
163 fields: [ 'originallyPublishedAt' ],
164 where: {
165 originallyPublishedAt: {
166 [Op.ne]: null
167 }
168 }
169 },
170 {
171 fields: [ 'category' ], // We don't care videos with an unknown category
172 where: {
173 category: {
174 [Op.ne]: null
175 }
176 }
177 },
178 {
179 fields: [ 'licence' ], // We don't care videos with an unknown licence
180 where: {
181 licence: {
182 [Op.ne]: null
183 }
184 }
185 },
186 {
187 fields: [ 'language' ], // We don't care videos with an unknown language
188 where: {
189 language: {
190 [Op.ne]: null
191 }
192 }
193 },
194 {
195 fields: [ 'nsfw' ], // Most of the videos are not NSFW
196 where: {
197 nsfw: true
198 }
199 },
200 {
201 fields: [ 'remote' ], // Only index local videos
202 where: {
203 remote: false
204 }
205 },
206 {
207 fields: [ 'uuid' ],
208 unique: true
209 },
210 {
211 fields: [ 'url' ],
212 unique: true
213 }
214]
215 136
216export enum ScopeNames { 137export enum ScopeNames {
217 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 138 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -224,6 +145,7 @@ export enum ScopeNames {
224 WITH_USER_HISTORY = 'WITH_USER_HISTORY', 145 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
225 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 146 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
226 WITH_USER_ID = 'WITH_USER_ID', 147 WITH_USER_ID = 'WITH_USER_ID',
148 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
227 WITH_THUMBNAILS = 'WITH_THUMBNAILS' 149 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
228} 150}
229 151
@@ -267,7 +189,10 @@ export type AvailableForListIDsOptions = {
267} 189}
268 190
269@Scopes(() => ({ 191@Scopes(() => ({
270 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 192 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
193 attributes: [ 'id', 'url', 'uuid', 'remote' ]
194 },
195 [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
271 const query: FindOptions = { 196 const query: FindOptions = {
272 include: [ 197 include: [
273 { 198 {
@@ -292,7 +217,7 @@ export type AvailableForListIDsOptions = {
292 if (options.ids) { 217 if (options.ids) {
293 query.where = { 218 query.where = {
294 id: { 219 id: {
295 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken 220 [Op.in]: options.ids
296 } 221 }
297 } 222 }
298 } 223 }
@@ -316,7 +241,7 @@ export type AvailableForListIDsOptions = {
316 241
317 return query 242 return query
318 }, 243 },
319 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { 244 [ScopeNames.AVAILABLE_FOR_LIST_IDS]: (options: AvailableForListIDsOptions) => {
320 const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : [] 245 const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : []
321 246
322 const query: FindOptions = { 247 const query: FindOptions = {
@@ -327,11 +252,11 @@ export type AvailableForListIDsOptions = {
327 const attributesType = options.attributesType || 'id' 252 const attributesType = options.attributesType || 'id'
328 253
329 if (attributesType === 'id') query.attributes = [ 'id' ] 254 if (attributesType === 'id') query.attributes = [ 'id' ]
330 else if (attributesType === 'none') query.attributes = [ ] 255 else if (attributesType === 'none') query.attributes = []
331 256
332 whereAnd.push({ 257 whereAnd.push({
333 id: { 258 id: {
334 [ Op.notIn ]: Sequelize.literal( 259 [Op.notIn]: Sequelize.literal(
335 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' 260 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
336 ) 261 )
337 } 262 }
@@ -340,7 +265,7 @@ export type AvailableForListIDsOptions = {
340 if (options.serverAccountId) { 265 if (options.serverAccountId) {
341 whereAnd.push({ 266 whereAnd.push({
342 channelId: { 267 channelId: {
343 [ Op.notIn ]: Sequelize.literal( 268 [Op.notIn]: Sequelize.literal(
344 '(' + 269 '(' +
345 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + 270 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
346 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + 271 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
@@ -353,15 +278,14 @@ export type AvailableForListIDsOptions = {
353 278
354 // Only list public/published videos 279 // Only list public/published videos
355 if (!options.filter || options.filter !== 'all-local') { 280 if (!options.filter || options.filter !== 'all-local') {
356
357 const publishWhere = { 281 const publishWhere = {
358 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 282 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
359 [ Op.or ]: [ 283 [Op.or]: [
360 { 284 {
361 state: VideoState.PUBLISHED 285 state: VideoState.PUBLISHED
362 }, 286 },
363 { 287 {
364 [ Op.and ]: { 288 [Op.and]: {
365 state: VideoState.TO_TRANSCODE, 289 state: VideoState.TO_TRANSCODE,
366 waitTranscoding: false 290 waitTranscoding: false
367 } 291 }
@@ -448,7 +372,7 @@ export type AvailableForListIDsOptions = {
448 [Op.or]: [ 372 [Op.or]: [
449 { 373 {
450 id: { 374 id: {
451 [ Op.in ]: Sequelize.literal( 375 [Op.in]: Sequelize.literal(
452 '(' + 376 '(' +
453 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 377 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
454 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 378 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
@@ -459,7 +383,7 @@ export type AvailableForListIDsOptions = {
459 }, 383 },
460 { 384 {
461 id: { 385 id: {
462 [ Op.in ]: Sequelize.literal( 386 [Op.in]: Sequelize.literal(
463 '(' + 387 '(' +
464 'SELECT "video"."id" AS "id" FROM "video" ' + 388 'SELECT "video"."id" AS "id" FROM "video" ' +
465 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 389 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
@@ -479,7 +403,7 @@ export type AvailableForListIDsOptions = {
479 if (options.withFiles === true) { 403 if (options.withFiles === true) {
480 whereAnd.push({ 404 whereAnd.push({
481 id: { 405 id: {
482 [ Op.in ]: Sequelize.literal( 406 [Op.in]: Sequelize.literal(
483 '(SELECT "videoId" FROM "videoFile")' 407 '(SELECT "videoId" FROM "videoFile")'
484 ) 408 )
485 } 409 }
@@ -493,7 +417,7 @@ export type AvailableForListIDsOptions = {
493 417
494 whereAnd.push({ 418 whereAnd.push({
495 id: { 419 id: {
496 [ Op.in ]: Sequelize.literal( 420 [Op.in]: Sequelize.literal(
497 '(' + 421 '(' +
498 'SELECT "videoId" FROM "videoTag" ' + 422 'SELECT "videoId" FROM "videoTag" ' +
499 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 423 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -509,7 +433,7 @@ export type AvailableForListIDsOptions = {
509 433
510 whereAnd.push({ 434 whereAnd.push({
511 id: { 435 id: {
512 [ Op.in ]: Sequelize.literal( 436 [Op.in]: Sequelize.literal(
513 '(' + 437 '(' +
514 'SELECT "videoId" FROM "videoTag" ' + 438 'SELECT "videoId" FROM "videoTag" ' +
515 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 439 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -529,7 +453,7 @@ export type AvailableForListIDsOptions = {
529 if (options.categoryOneOf) { 453 if (options.categoryOneOf) {
530 whereAnd.push({ 454 whereAnd.push({
531 category: { 455 category: {
532 [ Op.or ]: options.categoryOneOf 456 [Op.or]: options.categoryOneOf
533 } 457 }
534 }) 458 })
535 } 459 }
@@ -537,7 +461,7 @@ export type AvailableForListIDsOptions = {
537 if (options.licenceOneOf) { 461 if (options.licenceOneOf) {
538 whereAnd.push({ 462 whereAnd.push({
539 licence: { 463 licence: {
540 [ Op.or ]: options.licenceOneOf 464 [Op.or]: options.licenceOneOf
541 } 465 }
542 }) 466 })
543 } 467 }
@@ -552,12 +476,12 @@ export type AvailableForListIDsOptions = {
552 [Op.or]: [ 476 [Op.or]: [
553 { 477 {
554 language: { 478 language: {
555 [ Op.or ]: videoLanguages 479 [Op.or]: videoLanguages
556 } 480 }
557 }, 481 },
558 { 482 {
559 id: { 483 id: {
560 [ Op.in ]: Sequelize.literal( 484 [Op.in]: Sequelize.literal(
561 '(' + 485 '(' +
562 'SELECT "videoId" FROM "videoCaption" ' + 486 'SELECT "videoId" FROM "videoCaption" ' +
563 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' + 487 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' +
@@ -591,12 +515,12 @@ export type AvailableForListIDsOptions = {
591 } 515 }
592 516
593 query.where = { 517 query.where = {
594 [ Op.and ]: whereAnd 518 [Op.and]: whereAnd
595 } 519 }
596 520
597 return query 521 return query
598 }, 522 },
599 [ ScopeNames.WITH_THUMBNAILS ]: { 523 [ScopeNames.WITH_THUMBNAILS]: {
600 include: [ 524 include: [
601 { 525 {
602 model: ThumbnailModel, 526 model: ThumbnailModel,
@@ -604,7 +528,7 @@ export type AvailableForListIDsOptions = {
604 } 528 }
605 ] 529 ]
606 }, 530 },
607 [ ScopeNames.WITH_USER_ID ]: { 531 [ScopeNames.WITH_USER_ID]: {
608 include: [ 532 include: [
609 { 533 {
610 attributes: [ 'accountId' ], 534 attributes: [ 'accountId' ],
@@ -620,7 +544,7 @@ export type AvailableForListIDsOptions = {
620 } 544 }
621 ] 545 ]
622 }, 546 },
623 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 547 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
624 include: [ 548 include: [
625 { 549 {
626 model: VideoChannelModel.unscoped(), 550 model: VideoChannelModel.unscoped(),
@@ -672,10 +596,10 @@ export type AvailableForListIDsOptions = {
672 } 596 }
673 ] 597 ]
674 }, 598 },
675 [ ScopeNames.WITH_TAGS ]: { 599 [ScopeNames.WITH_TAGS]: {
676 include: [ TagModel ] 600 include: [ TagModel ]
677 }, 601 },
678 [ ScopeNames.WITH_BLACKLISTED ]: { 602 [ScopeNames.WITH_BLACKLISTED]: {
679 include: [ 603 include: [
680 { 604 {
681 attributes: [ 'id', 'reason', 'unfederated' ], 605 attributes: [ 'id', 'reason', 'unfederated' ],
@@ -684,7 +608,7 @@ export type AvailableForListIDsOptions = {
684 } 608 }
685 ] 609 ]
686 }, 610 },
687 [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => { 611 [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => {
688 let subInclude: any[] = [] 612 let subInclude: any[] = []
689 613
690 if (withRedundancies === true) { 614 if (withRedundancies === true) {
@@ -708,7 +632,7 @@ export type AvailableForListIDsOptions = {
708 ] 632 ]
709 } 633 }
710 }, 634 },
711 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { 635 [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
712 const subInclude: IncludeOptions[] = [ 636 const subInclude: IncludeOptions[] = [
713 { 637 {
714 model: VideoFileModel.unscoped(), 638 model: VideoFileModel.unscoped(),
@@ -735,7 +659,7 @@ export type AvailableForListIDsOptions = {
735 ] 659 ]
736 } 660 }
737 }, 661 },
738 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 662 [ScopeNames.WITH_SCHEDULED_UPDATE]: {
739 include: [ 663 include: [
740 { 664 {
741 model: ScheduleVideoUpdateModel.unscoped(), 665 model: ScheduleVideoUpdateModel.unscoped(),
@@ -743,7 +667,7 @@ export type AvailableForListIDsOptions = {
743 } 667 }
744 ] 668 ]
745 }, 669 },
746 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => { 670 [ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
747 return { 671 return {
748 include: [ 672 include: [
749 { 673 {
@@ -760,7 +684,72 @@ export type AvailableForListIDsOptions = {
760})) 684}))
761@Table({ 685@Table({
762 tableName: 'video', 686 tableName: 'video',
763 indexes 687 indexes: [
688 buildTrigramSearchIndex('video_name_trigram', 'name'),
689
690 { fields: [ 'createdAt' ] },
691 {
692 fields: [
693 { name: 'publishedAt', order: 'DESC' },
694 { name: 'id', order: 'ASC' }
695 ]
696 },
697 { fields: [ 'duration' ] },
698 { fields: [ 'views' ] },
699 { fields: [ 'channelId' ] },
700 {
701 fields: [ 'originallyPublishedAt' ],
702 where: {
703 originallyPublishedAt: {
704 [Op.ne]: null
705 }
706 }
707 },
708 {
709 fields: [ 'category' ], // We don't care videos with an unknown category
710 where: {
711 category: {
712 [Op.ne]: null
713 }
714 }
715 },
716 {
717 fields: [ 'licence' ], // We don't care videos with an unknown licence
718 where: {
719 licence: {
720 [Op.ne]: null
721 }
722 }
723 },
724 {
725 fields: [ 'language' ], // We don't care videos with an unknown language
726 where: {
727 language: {
728 [Op.ne]: null
729 }
730 }
731 },
732 {
733 fields: [ 'nsfw' ], // Most of the videos are not NSFW
734 where: {
735 nsfw: true
736 }
737 },
738 {
739 fields: [ 'remote' ], // Only index local videos
740 where: {
741 remote: false
742 }
743 },
744 {
745 fields: [ 'uuid' ],
746 unique: true
747 },
748 {
749 fields: [ 'url' ],
750 unique: true
751 }
752 ]
764}) 753})
765export class VideoModel extends Model<VideoModel> { 754export class VideoModel extends Model<VideoModel> {
766 755
@@ -1031,7 +1020,7 @@ export class VideoModel extends Model<VideoModel> {
1031 }, 1020 },
1032 onDelete: 'cascade', 1021 onDelete: 'cascade',
1033 hooks: true, 1022 hooks: true,
1034 [ 'separate' as any ]: true 1023 ['separate' as any]: true
1035 }) 1024 })
1036 VideoCaptions: VideoCaptionModel[] 1025 VideoCaptions: VideoCaptionModel[]
1037 1026
@@ -1090,6 +1079,11 @@ export class VideoModel extends Model<VideoModel> {
1090 return undefined 1079 return undefined
1091 } 1080 }
1092 1081
1082 @BeforeDestroy
1083 static invalidateCache (instance: VideoModel) {
1084 ModelCache.Instance.invalidateCache('video', instance.id)
1085 }
1086
1093 static listLocal (): Bluebird<MVideoWithAllFiles[]> { 1087 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
1094 const query = { 1088 const query = {
1095 where: { 1089 where: {
@@ -1127,16 +1121,16 @@ export class VideoModel extends Model<VideoModel> {
1127 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings 1121 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
1128 where: { 1122 where: {
1129 id: { 1123 id: {
1130 [ Op.in ]: Sequelize.literal('(' + rawQuery + ')') 1124 [Op.in]: Sequelize.literal('(' + rawQuery + ')')
1131 }, 1125 },
1132 [ Op.or ]: [ 1126 [Op.or]: [
1133 { privacy: VideoPrivacy.PUBLIC }, 1127 { privacy: VideoPrivacy.PUBLIC },
1134 { privacy: VideoPrivacy.UNLISTED } 1128 { privacy: VideoPrivacy.UNLISTED }
1135 ] 1129 ]
1136 }, 1130 },
1137 include: [ 1131 include: [
1138 { 1132 {
1139 attributes: [ 'language' ], 1133 attributes: [ 'language', 'fileUrl' ],
1140 model: VideoCaptionModel.unscoped(), 1134 model: VideoCaptionModel.unscoped(),
1141 required: false 1135 required: false
1142 }, 1136 },
@@ -1146,10 +1140,10 @@ export class VideoModel extends Model<VideoModel> {
1146 required: false, 1140 required: false,
1147 // We only want videos shared by this actor 1141 // We only want videos shared by this actor
1148 where: { 1142 where: {
1149 [ Op.and ]: [ 1143 [Op.and]: [
1150 { 1144 {
1151 id: { 1145 id: {
1152 [ Op.not ]: null 1146 [Op.not]: null
1153 } 1147 }
1154 }, 1148 },
1155 { 1149 {
@@ -1199,8 +1193,8 @@ export class VideoModel extends Model<VideoModel> {
1199 // totals: totalVideos + totalVideoShares 1193 // totals: totalVideos + totalVideoShares
1200 let totalVideos = 0 1194 let totalVideos = 0
1201 let totalVideoShares = 0 1195 let totalVideoShares = 0
1202 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10) 1196 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
1203 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10) 1197 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
1204 1198
1205 const total = totalVideos + totalVideoShares 1199 const total = totalVideos + totalVideoShares
1206 return { 1200 return {
@@ -1243,7 +1237,7 @@ export class VideoModel extends Model<VideoModel> {
1243 baseQuery = Object.assign(baseQuery, { 1237 baseQuery = Object.assign(baseQuery, {
1244 where: { 1238 where: {
1245 name: { 1239 name: {
1246 [ Op.iLike ]: '%' + search + '%' 1240 [Op.iLike]: '%' + search + '%'
1247 } 1241 }
1248 } 1242 }
1249 }) 1243 })
@@ -1273,25 +1267,25 @@ export class VideoModel extends Model<VideoModel> {
1273 } 1267 }
1274 1268
1275 static async listForApi (options: { 1269 static async listForApi (options: {
1276 start: number, 1270 start: number
1277 count: number, 1271 count: number
1278 sort: string, 1272 sort: string
1279 nsfw: boolean, 1273 nsfw: boolean
1280 includeLocalVideos: boolean, 1274 includeLocalVideos: boolean
1281 withFiles: boolean, 1275 withFiles: boolean
1282 categoryOneOf?: number[], 1276 categoryOneOf?: number[]
1283 licenceOneOf?: number[], 1277 licenceOneOf?: number[]
1284 languageOneOf?: string[], 1278 languageOneOf?: string[]
1285 tagsOneOf?: string[], 1279 tagsOneOf?: string[]
1286 tagsAllOf?: string[], 1280 tagsAllOf?: string[]
1287 filter?: VideoFilter, 1281 filter?: VideoFilter
1288 accountId?: number, 1282 accountId?: number
1289 videoChannelId?: number, 1283 videoChannelId?: number
1290 followerActorId?: number 1284 followerActorId?: number
1291 videoPlaylistId?: number, 1285 videoPlaylistId?: number
1292 trendingDays?: number, 1286 trendingDays?: number
1293 user?: MUserAccountId, 1287 user?: MUserAccountId
1294 historyOfUser?: MUserId, 1288 historyOfUser?: MUserId
1295 countVideos?: boolean 1289 countVideos?: boolean
1296 }) { 1290 }) {
1297 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1291 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
@@ -1357,7 +1351,7 @@ export class VideoModel extends Model<VideoModel> {
1357 tagsAllOf?: string[] 1351 tagsAllOf?: string[]
1358 durationMin?: number // seconds 1352 durationMin?: number // seconds
1359 durationMax?: number // seconds 1353 durationMax?: number // seconds
1360 user?: MUserAccountId, 1354 user?: MUserAccountId
1361 filter?: VideoFilter 1355 filter?: VideoFilter
1362 }) { 1356 }) {
1363 const whereAnd = [] 1357 const whereAnd = []
@@ -1365,8 +1359,8 @@ export class VideoModel extends Model<VideoModel> {
1365 if (options.startDate || options.endDate) { 1359 if (options.startDate || options.endDate) {
1366 const publishedAtRange = {} 1360 const publishedAtRange = {}
1367 1361
1368 if (options.startDate) publishedAtRange[ Op.gte ] = options.startDate 1362 if (options.startDate) publishedAtRange[Op.gte] = options.startDate
1369 if (options.endDate) publishedAtRange[ Op.lte ] = options.endDate 1363 if (options.endDate) publishedAtRange[Op.lte] = options.endDate
1370 1364
1371 whereAnd.push({ publishedAt: publishedAtRange }) 1365 whereAnd.push({ publishedAt: publishedAtRange })
1372 } 1366 }
@@ -1374,8 +1368,8 @@ export class VideoModel extends Model<VideoModel> {
1374 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) { 1368 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) {
1375 const originallyPublishedAtRange = {} 1369 const originallyPublishedAtRange = {}
1376 1370
1377 if (options.originallyPublishedStartDate) originallyPublishedAtRange[ Op.gte ] = options.originallyPublishedStartDate 1371 if (options.originallyPublishedStartDate) originallyPublishedAtRange[Op.gte] = options.originallyPublishedStartDate
1378 if (options.originallyPublishedEndDate) originallyPublishedAtRange[ Op.lte ] = options.originallyPublishedEndDate 1372 if (options.originallyPublishedEndDate) originallyPublishedAtRange[Op.lte] = options.originallyPublishedEndDate
1379 1373
1380 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange }) 1374 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange })
1381 } 1375 }
@@ -1383,8 +1377,8 @@ export class VideoModel extends Model<VideoModel> {
1383 if (options.durationMin || options.durationMax) { 1377 if (options.durationMin || options.durationMax) {
1384 const durationRange = {} 1378 const durationRange = {}
1385 1379
1386 if (options.durationMin) durationRange[ Op.gte ] = options.durationMin 1380 if (options.durationMin) durationRange[Op.gte] = options.durationMin
1387 if (options.durationMax) durationRange[ Op.lte ] = options.durationMax 1381 if (options.durationMax) durationRange[Op.lte] = options.durationMax
1388 1382
1389 whereAnd.push({ duration: durationRange }) 1383 whereAnd.push({ duration: durationRange })
1390 } 1384 }
@@ -1395,7 +1389,7 @@ export class VideoModel extends Model<VideoModel> {
1395 if (options.search) { 1389 if (options.search) {
1396 const trigramSearch = { 1390 const trigramSearch = {
1397 id: { 1391 id: {
1398 [ Op.in ]: Sequelize.literal( 1392 [Op.in]: Sequelize.literal(
1399 '(' + 1393 '(' +
1400 'SELECT "video"."id" FROM "video" ' + 1394 'SELECT "video"."id" FROM "video" ' +
1401 'WHERE ' + 1395 'WHERE ' +
@@ -1484,6 +1478,24 @@ export class VideoModel extends Model<VideoModel> {
1484 ]).findOne(options) 1478 ]).findOne(options)
1485 } 1479 }
1486 1480
1481 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1482 const fun = () => {
1483 const query = {
1484 where: buildWhereIdOrUUID(id),
1485 transaction: t
1486 }
1487
1488 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1489 }
1490
1491 return ModelCache.Instance.doCache({
1492 cacheType: 'load-video-immutable-id',
1493 key: '' + id,
1494 deleteKey: 'video',
1495 fun
1496 })
1497 }
1498
1487 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { 1499 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
1488 const where = buildWhereIdOrUUID(id) 1500 const where = buildWhereIdOrUUID(id)
1489 const options = { 1501 const options = {
@@ -1547,6 +1559,26 @@ export class VideoModel extends Model<VideoModel> {
1547 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) 1559 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1548 } 1560 }
1549 1561
1562 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1563 const fun = () => {
1564 const query: FindOptions = {
1565 where: {
1566 url
1567 },
1568 transaction
1569 }
1570
1571 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1572 }
1573
1574 return ModelCache.Instance.doCache({
1575 cacheType: 'load-video-immutable-url',
1576 key: url,
1577 deleteKey: 'video',
1578 fun
1579 })
1580 }
1581
1550 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> { 1582 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1551 const query: FindOptions = { 1583 const query: FindOptions = {
1552 where: { 1584 where: {
@@ -1593,8 +1625,8 @@ export class VideoModel extends Model<VideoModel> {
1593 } 1625 }
1594 1626
1595 static loadForGetAPI (parameters: { 1627 static loadForGetAPI (parameters: {
1596 id: number | string, 1628 id: number | string
1597 t?: Transaction, 1629 t?: Transaction
1598 userId?: number 1630 userId?: number
1599 }): Bluebird<MVideoDetails> { 1631 }): Bluebird<MVideoDetails> {
1600 const { id, t, userId } = parameters 1632 const { id, t, userId } = parameters
@@ -1660,9 +1692,9 @@ export class VideoModel extends Model<VideoModel> {
1660 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) { 1692 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1661 // Instances only share videos 1693 // Instances only share videos
1662 const query = 'SELECT 1 FROM "videoShare" ' + 1694 const query = 'SELECT 1 FROM "videoShare" ' +
1663 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 1695 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1664 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' + 1696 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1665 'LIMIT 1' 1697 'LIMIT 1'
1666 1698
1667 const options = { 1699 const options = {
1668 type: QueryTypes.SELECT as QueryTypes.SELECT, 1700 type: QueryTypes.SELECT as QueryTypes.SELECT,
@@ -1694,7 +1726,7 @@ export class VideoModel extends Model<VideoModel> {
1694 } 1726 }
1695 1727
1696 return VideoModel.findAll(query) 1728 return VideoModel.findAll(query)
1697 .then(videos => videos.map(v => v.id)) 1729 .then(videos => videos.map(v => v.id))
1698 } 1730 }
1699 1731
1700 // threshold corresponds to how many video the field should have to be returned 1732 // threshold corresponds to how many video the field should have to be returned
@@ -1714,14 +1746,14 @@ export class VideoModel extends Model<VideoModel> {
1714 limit: count, 1746 limit: count,
1715 group: field, 1747 group: field,
1716 having: Sequelize.where( 1748 having: Sequelize.where(
1717 Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold } 1749 Sequelize.fn('COUNT', Sequelize.col(field)), { [Op.gte]: threshold }
1718 ), 1750 ),
1719 order: [ (this.sequelize as any).random() ] 1751 order: [ (this.sequelize as any).random() ]
1720 } 1752 }
1721 1753
1722 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) 1754 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] })
1723 .findAll(query) 1755 .findAll(query)
1724 .then(rows => rows.map(r => r[ field ])) 1756 .then(rows => rows.map(r => r[field]))
1725 } 1757 }
1726 1758
1727 static buildTrendingQuery (trendingDays: number) { 1759 static buildTrendingQuery (trendingDays: number) {
@@ -1732,7 +1764,7 @@ export class VideoModel extends Model<VideoModel> {
1732 required: false, 1764 required: false,
1733 where: { 1765 where: {
1734 startDate: { 1766 startDate: {
1735 [ Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) 1767 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
1736 } 1768 }
1737 } 1769 }
1738 } 1770 }
@@ -1815,23 +1847,23 @@ export class VideoModel extends Model<VideoModel> {
1815 } 1847 }
1816 1848
1817 static getCategoryLabel (id: number) { 1849 static getCategoryLabel (id: number) {
1818 return VIDEO_CATEGORIES[ id ] || 'Misc' 1850 return VIDEO_CATEGORIES[id] || 'Misc'
1819 } 1851 }
1820 1852
1821 static getLicenceLabel (id: number) { 1853 static getLicenceLabel (id: number) {
1822 return VIDEO_LICENCES[ id ] || 'Unknown' 1854 return VIDEO_LICENCES[id] || 'Unknown'
1823 } 1855 }
1824 1856
1825 static getLanguageLabel (id: string) { 1857 static getLanguageLabel (id: string) {
1826 return VIDEO_LANGUAGES[ id ] || 'Unknown' 1858 return VIDEO_LANGUAGES[id] || 'Unknown'
1827 } 1859 }
1828 1860
1829 static getPrivacyLabel (id: number) { 1861 static getPrivacyLabel (id: number) {
1830 return VIDEO_PRIVACIES[ id ] || 'Unknown' 1862 return VIDEO_PRIVACIES[id] || 'Unknown'
1831 } 1863 }
1832 1864
1833 static getStateLabel (id: number) { 1865 static getStateLabel (id: number) {
1834 return VIDEO_STATES[ id ] || 'Unknown' 1866 return VIDEO_STATES[id] || 'Unknown'
1835 } 1867 }
1836 1868
1837 isBlacklisted () { 1869 isBlacklisted () {
@@ -1843,7 +1875,7 @@ export class VideoModel extends Model<VideoModel> {
1843 this.VideoChannel.Account.isBlocked() 1875 this.VideoChannel.Account.isBlocked()
1844 } 1876 }
1845 1877
1846 getQualityFileBy <T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) { 1878 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
1847 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { 1879 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
1848 const file = fun(this.VideoFiles, file => file.resolution) 1880 const file = fun(this.VideoFiles, file => file.resolution)
1849 1881
@@ -1861,15 +1893,15 @@ export class VideoModel extends Model<VideoModel> {
1861 return undefined 1893 return undefined
1862 } 1894 }
1863 1895
1864 getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1896 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1865 return this.getQualityFileBy(maxBy) 1897 return this.getQualityFileBy(maxBy)
1866 } 1898 }
1867 1899
1868 getMinQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1900 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1869 return this.getQualityFileBy(minBy) 1901 return this.getQualityFileBy(minBy)
1870 } 1902 }
1871 1903
1872 getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { 1904 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
1873 if (Array.isArray(this.VideoFiles) === false) return undefined 1905 if (Array.isArray(this.VideoFiles) === false) return undefined
1874 1906
1875 const file = this.VideoFiles.find(f => f.resolution === resolution) 1907 const file = this.VideoFiles.find(f => f.resolution === resolution)
@@ -1905,6 +1937,10 @@ export class VideoModel extends Model<VideoModel> {
1905 return this.uuid + '.jpg' 1937 return this.uuid + '.jpg'
1906 } 1938 }
1907 1939
1940 hasPreview () {
1941 return !!this.getPreview()
1942 }
1943
1908 getPreview () { 1944 getPreview () {
1909 if (Array.isArray(this.Thumbnails) === false) return undefined 1945 if (Array.isArray(this.Thumbnails) === false) return undefined
1910 1946
@@ -1992,8 +2028,8 @@ export class VideoModel extends Model<VideoModel> {
1992 } 2028 }
1993 2029
1994 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists 2030 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
1995 .filter(s => s.type !== VideoStreamingPlaylistType.HLS) 2031 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1996 .concat(toAdd) 2032 .concat(toAdd)
1997 } 2033 }
1998 2034
1999 removeFile (videoFile: MVideoFile, isRedundancy = false) { 2035 removeFile (videoFile: MVideoFile, isRedundancy = false) {
@@ -2014,7 +2050,7 @@ export class VideoModel extends Model<VideoModel> {
2014 await remove(directoryPath) 2050 await remove(directoryPath)
2015 2051
2016 if (isRedundancy !== true) { 2052 if (isRedundancy !== true) {
2017 let streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo 2053 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
2018 streamingPlaylistWithFiles.Video = this 2054 streamingPlaylistWithFiles.Video = this
2019 2055
2020 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) { 2056 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {