diff options
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r-- | server/models/video/video.ts | 194 |
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' | |||
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import * as Sequelize from 'sequelize' | 6 | import * as Sequelize from 'sequelize' |
7 | import { | 7 | import { |
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' |
11 | import { IIncludeOptions } from 'sequelize-typescript/lib/interfaces/IIncludeOptions' | ||
12 | import { VideoPrivacy, VideoResolution } from '../../../shared' | 11 | import { VideoPrivacy, VideoResolution } from '../../../shared' |
13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 12 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
14 | import { Video, VideoDetails } from '../../../shared/models/videos' | 13 | import { Video, VideoDetails } from '../../../shared/models/videos' |
@@ -22,6 +21,7 @@ import { | |||
22 | } from '../../helpers/custom-validators/videos' | 21 | } from '../../helpers/custom-validators/videos' |
23 | import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' | 22 | import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' |
24 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
24 | import { getServerActor } from '../../helpers/utils' | ||
25 | import { | 25 | import { |
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' | |||
31 | import { AccountModel } from '../account/account' | 31 | import { AccountModel } from '../account/account' |
32 | import { AccountVideoRateModel } from '../account/account-video-rate' | 32 | import { AccountVideoRateModel } from '../account/account-video-rate' |
33 | import { ActorModel } from '../activitypub/actor' | 33 | import { ActorModel } from '../activitypub/actor' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | ||
34 | import { ServerModel } from '../server/server' | 35 | import { ServerModel } from '../server/server' |
35 | import { getSort, throwIfNotValid } from '../utils' | 36 | import { getSort, throwIfNotValid } from '../utils' |
36 | import { TagModel } from './tag' | 37 | import { TagModel } from './tag' |
@@ -43,7 +44,6 @@ import { VideoTagModel } from './video-tag' | |||
43 | 44 | ||
44 | enum ScopeNames { | 45 | enum 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 | ||