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.ts194
1 files changed, 100 insertions, 94 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 3e2b4ce64..514edfd9c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -5,10 +5,9 @@ import * as parseTorrent from 'parse-torrent'
5import { join } from 'path' 5import { join } from 'path'
6import * as Sequelize from 'sequelize' 6import * as Sequelize from 'sequelize'
7import { 7import {
8 AfterDestroy, AllowNull, BelongsTo, BelongsToMany, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, IFindOptions, Is, 8 AfterDestroy, AllowNull, BeforeDestroy, BelongsTo, BelongsToMany, Column, CreatedAt, DataType, Default, ForeignKey, HasMany,
9 IsInt, IsUUID, Min, Model, Scopes, Table, UpdatedAt 9 IFindOptions, Is, IsInt, IsUUID, Min, Model, Scopes, Table, UpdatedAt
10} from 'sequelize-typescript' 10} from 'sequelize-typescript'
11import { IIncludeOptions } from 'sequelize-typescript/lib/interfaces/IIncludeOptions'
12import { VideoPrivacy, VideoResolution } from '../../../shared' 11import { VideoPrivacy, VideoResolution } from '../../../shared'
13import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 12import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
14import { Video, VideoDetails } from '../../../shared/models/videos' 13import { Video, VideoDetails } from '../../../shared/models/videos'
@@ -22,6 +21,7 @@ import {
22} from '../../helpers/custom-validators/videos' 21} from '../../helpers/custom-validators/videos'
23import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' 22import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils'
24import { logger } from '../../helpers/logger' 23import { logger } from '../../helpers/logger'
24import { getServerActor } from '../../helpers/utils'
25import { 25import {
26 API_VERSION, CONFIG, CONSTRAINTS_FIELDS, PREVIEWS_SIZE, REMOTE_SCHEME, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_CATEGORIES, 26 API_VERSION, CONFIG, CONSTRAINTS_FIELDS, PREVIEWS_SIZE, REMOTE_SCHEME, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_CATEGORIES,
27 VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES 27 VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES
@@ -31,6 +31,7 @@ import { sendDeleteVideo } from '../../lib/activitypub/send'
31import { AccountModel } from '../account/account' 31import { AccountModel } from '../account/account'
32import { AccountVideoRateModel } from '../account/account-video-rate' 32import { AccountVideoRateModel } from '../account/account-video-rate'
33import { ActorModel } from '../activitypub/actor' 33import { ActorModel } from '../activitypub/actor'
34import { ActorFollowModel } from '../activitypub/actor-follow'
34import { ServerModel } from '../server/server' 35import { ServerModel } from '../server/server'
35import { getSort, throwIfNotValid } from '../utils' 36import { getSort, throwIfNotValid } from '../utils'
36import { TagModel } from './tag' 37import { TagModel } from './tag'
@@ -43,7 +44,6 @@ import { VideoTagModel } from './video-tag'
43 44
44enum ScopeNames { 45enum ScopeNames {
45 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 46 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
46 WITH_ACCOUNT_API = 'WITH_ACCOUNT_API',
47 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', 47 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
48 WITH_TAGS = 'WITH_TAGS', 48 WITH_TAGS = 'WITH_TAGS',
49 WITH_FILES = 'WITH_FILES', 49 WITH_FILES = 'WITH_FILES',
@@ -53,34 +53,60 @@ enum ScopeNames {
53} 53}
54 54
55@Scopes({ 55@Scopes({
56 [ScopeNames.AVAILABLE_FOR_LIST]: { 56 [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number) => ({
57 subQuery: false,
57 where: { 58 where: {
58 id: { 59 id: {
59 [Sequelize.Op.notIn]: Sequelize.literal( 60 [Sequelize.Op.notIn]: Sequelize.literal(
60 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' 61 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
61 ) 62 )
62 }, 63 },
63 privacy: VideoPrivacy.PUBLIC 64 privacy: VideoPrivacy.PUBLIC,
64 } 65 [Sequelize.Op.or]: [
65 }, 66 {
66 [ScopeNames.WITH_ACCOUNT_API]: { 67 '$VideoChannel.Account.Actor.serverId$': null
68 },
69 {
70 '$VideoChannel.Account.Actor.followers.actorId$': actorId
71 },
72 {
73 id: {
74 [ Sequelize.Op.in ]: Sequelize.literal(
75 '(' +
76 'SELECT "videoShare"."videoId" FROM "videoShare" ' +
77 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
78 'WHERE "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) +
79 ')'
80 )
81 }
82 }
83 ]
84 },
67 include: [ 85 include: [
68 { 86 {
69 model: () => VideoChannelModel.unscoped(), 87 attributes: [ 'name', 'description' ],
88 model: VideoChannelModel.unscoped(),
70 required: true, 89 required: true,
71 include: [ 90 include: [
72 { 91 {
73 attributes: [ 'name' ], 92 attributes: [ 'name' ],
74 model: () => AccountModel.unscoped(), 93 model: AccountModel.unscoped(),
75 required: true, 94 required: true,
76 include: [ 95 include: [
77 { 96 {
78 attributes: [ 'serverId' ], 97 attributes: [ 'serverId' ],
79 model: () => ActorModel.unscoped(), 98 model: ActorModel.unscoped(),
80 required: true, 99 required: true,
81 include: [ 100 include: [
82 { 101 {
83 model: () => ServerModel.unscoped(), 102 attributes: [ 'host' ],
103 model: ServerModel.unscoped(),
104 required: false
105 },
106 {
107 attributes: [ ],
108 model: ActorFollowModel.unscoped(),
109 as: 'followers',
84 required: false 110 required: false
85 } 111 }
86 ] 112 ]
@@ -90,7 +116,7 @@ enum ScopeNames {
90 ] 116 ]
91 } 117 }
92 ] 118 ]
93 }, 119 }),
94 [ScopeNames.WITH_ACCOUNT_DETAILS]: { 120 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
95 include: [ 121 include: [
96 { 122 {
@@ -347,23 +373,46 @@ export class VideoModel extends Model<VideoModel> {
347 name: 'videoId', 373 name: 'videoId',
348 allowNull: false 374 allowNull: false
349 }, 375 },
350 onDelete: 'cascade' 376 onDelete: 'cascade',
377 hooks: true
351 }) 378 })
352 VideoComments: VideoCommentModel[] 379 VideoComments: VideoCommentModel[]
353 380
381 @BeforeDestroy
382 static async sendDelete (instance: VideoModel, options) {
383 if (instance.isOwned()) {
384 if (!instance.VideoChannel) {
385 instance.VideoChannel = await instance.$get('VideoChannel', {
386 include: [
387 {
388 model: AccountModel,
389 include: [ ActorModel ]
390 }
391 ],
392 transaction: options.transaction
393 }) as VideoChannelModel
394 }
395
396 logger.debug('Sending delete of video %s.', instance.url)
397
398 return sendDeleteVideo(instance, options.transaction)
399 }
400
401 return undefined
402 }
403
354 @AfterDestroy 404 @AfterDestroy
355 static removeFilesAndSendDelete (instance: VideoModel) { 405 static async removeFilesAndSendDelete (instance: VideoModel) {
356 const tasks = [] 406 const tasks: Promise<any>[] = []
357 407
358 tasks.push( 408 tasks.push(instance.removeThumbnail())
359 instance.removeThumbnail()
360 )
361 409
362 if (instance.isOwned()) { 410 if (instance.isOwned()) {
363 tasks.push( 411 if (!Array.isArray(instance.VideoFiles)) {
364 instance.removePreview(), 412 instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
365 sendDeleteVideo(instance, undefined) 413 }
366 ) 414
415 tasks.push(instance.removePreview())
367 416
368 // Remove physical files and torrents 417 // Remove physical files and torrents
369 instance.VideoFiles.forEach(file => { 418 instance.VideoFiles.forEach(file => {
@@ -500,14 +549,16 @@ export class VideoModel extends Model<VideoModel> {
500 }) 549 })
501 } 550 }
502 551
503 static listForApi (start: number, count: number, sort: string) { 552 static async listForApi (start: number, count: number, sort: string) {
504 const query = { 553 const query = {
505 offset: start, 554 offset: start,
506 limit: count, 555 limit: count,
507 order: [ getSort(sort) ] 556 order: [ getSort(sort) ]
508 } 557 }
509 558
510 return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST, ScopeNames.WITH_ACCOUNT_API ]) 559 const serverActor = await getServerActor()
560
561 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] })
511 .findAndCountAll(query) 562 .findAndCountAll(query)
512 .then(({ rows, count }) => { 563 .then(({ rows, count }) => {
513 return { 564 return {
@@ -517,6 +568,29 @@ export class VideoModel extends Model<VideoModel> {
517 }) 568 })
518 } 569 }
519 570
571 static async searchAndPopulateAccountAndServerAndTags (value: string, start: number, count: number, sort: string) {
572 const query: IFindOptions<VideoModel> = {
573 offset: start,
574 limit: count,
575 order: [ getSort(sort) ],
576 where: {
577 name: {
578 [Sequelize.Op.iLike]: '%' + value + '%'
579 }
580 }
581 }
582
583 const serverActor = await getServerActor()
584
585 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] })
586 .findAndCountAll(query).then(({ rows, count }) => {
587 return {
588 data: rows,
589 total: count
590 }
591 })
592 }
593
520 static load (id: number) { 594 static load (id: number) {
521 return VideoModel.findById(id) 595 return VideoModel.findById(id)
522 } 596 }
@@ -603,74 +677,6 @@ export class VideoModel extends Model<VideoModel> {
603 .findOne(options) 677 .findOne(options)
604 } 678 }
605 679
606 static searchAndPopulateAccountAndServerAndTags (value: string, start: number, count: number, sort: string) {
607 const serverInclude: IIncludeOptions = {
608 model: ServerModel,
609 required: false
610 }
611
612 const accountInclude: IIncludeOptions = {
613 model: AccountModel,
614 include: [
615 {
616 model: ActorModel,
617 required: true,
618 include: [ serverInclude ]
619 }
620 ]
621 }
622
623 const videoChannelInclude: IIncludeOptions = {
624 model: VideoChannelModel,
625 include: [ accountInclude ],
626 required: true
627 }
628
629 const tagInclude: IIncludeOptions = {
630 model: TagModel
631 }
632
633 const query: IFindOptions<VideoModel> = {
634 distinct: true, // Because we have tags
635 offset: start,
636 limit: count,
637 order: [ getSort(sort) ],
638 where: {}
639 }
640
641 // TODO: search on tags too
642 // const escapedValue = Video['sequelize'].escape('%' + value + '%')
643 // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
644 // `(SELECT "VideoTags"."videoId"
645 // FROM "Tags"
646 // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
647 // WHERE name ILIKE ${escapedValue}
648 // )`
649 // )
650
651 // TODO: search on account too
652 // accountInclude.where = {
653 // name: {
654 // [Sequelize.Op.iLike]: '%' + value + '%'
655 // }
656 // }
657 query.where['name'] = {
658 [Sequelize.Op.iLike]: '%' + value + '%'
659 }
660
661 query.include = [
662 videoChannelInclude, tagInclude
663 ]
664
665 return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST ])
666 .findAndCountAll(query).then(({ rows, count }) => {
667 return {
668 data: rows,
669 total: count
670 }
671 })
672 }
673
674 getOriginalFile () { 680 getOriginalFile () {
675 if (Array.isArray(this.VideoFiles) === false) return undefined 681 if (Array.isArray(this.VideoFiles) === false) return undefined
676 682