diff options
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/sql/video/shared/abstract-video-query-builder.ts | 7 | ||||
-rw-r--r-- | server/models/video/sql/video/shared/video-file-query-builder.ts | 30 | ||||
-rw-r--r-- | server/models/video/sql/video/video-model-get-query-builder.ts | 15 | ||||
-rw-r--r-- | server/models/video/sql/video/videos-model-list-query-builder.ts | 41 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 13 | ||||
-rw-r--r-- | server/models/video/video-live-session.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-source.ts | 55 | ||||
-rw-r--r-- | server/models/video/video.ts | 10 |
8 files changed, 148 insertions, 25 deletions
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index b79d20ade..3c74b0ea6 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -313,7 +313,12 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery { | |||
313 | return result | 313 | return result |
314 | } | 314 | } |
315 | 315 | ||
316 | protected whereId (options: { id?: string | number, url?: string }) { | 316 | protected whereId (options: { ids?: number[], id?: string | number, url?: string }) { |
317 | if (options.ids) { | ||
318 | this.where = `WHERE "video"."id" IN (${createSafeIn(this.sequelize, options.ids)})` | ||
319 | return | ||
320 | } | ||
321 | |||
317 | if (options.url) { | 322 | if (options.url) { |
318 | this.where = 'WHERE "video"."url" = :videoUrl' | 323 | this.where = 'WHERE "video"."url" = :videoUrl' |
319 | this.replacements.videoUrl = options.url | 324 | this.replacements.videoUrl = options.url |
diff --git a/server/models/video/sql/video/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts index 50c12f627..cc53a4860 100644 --- a/server/models/video/sql/video/shared/video-file-query-builder.ts +++ b/server/models/video/sql/video/shared/video-file-query-builder.ts | |||
@@ -1,7 +1,17 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize, Transaction } from 'sequelize' |
2 | import { BuildVideoGetQueryOptions } from '../video-model-get-query-builder' | ||
3 | import { AbstractVideoQueryBuilder } from './abstract-video-query-builder' | 2 | import { AbstractVideoQueryBuilder } from './abstract-video-query-builder' |
4 | 3 | ||
4 | export type FileQueryOptions = { | ||
5 | id?: string | number | ||
6 | url?: string | ||
7 | |||
8 | includeRedundancy: boolean | ||
9 | |||
10 | transaction?: Transaction | ||
11 | |||
12 | logging?: boolean | ||
13 | } | ||
14 | |||
5 | /** | 15 | /** |
6 | * | 16 | * |
7 | * Fetch files (webtorrent and streaming playlist) according to a video | 17 | * Fetch files (webtorrent and streaming playlist) according to a video |
@@ -15,26 +25,26 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
15 | super(sequelize, 'get') | 25 | super(sequelize, 'get') |
16 | } | 26 | } |
17 | 27 | ||
18 | queryWebTorrentVideos (options: BuildVideoGetQueryOptions) { | 28 | queryWebTorrentVideos (options: FileQueryOptions) { |
19 | this.buildWebtorrentFilesQuery(options) | 29 | this.buildWebtorrentFilesQuery(options) |
20 | 30 | ||
21 | return this.runQuery(options) | 31 | return this.runQuery(options) |
22 | } | 32 | } |
23 | 33 | ||
24 | queryStreamingPlaylistVideos (options: BuildVideoGetQueryOptions) { | 34 | queryStreamingPlaylistVideos (options: FileQueryOptions) { |
25 | this.buildVideoStreamingPlaylistFilesQuery(options) | 35 | this.buildVideoStreamingPlaylistFilesQuery(options) |
26 | 36 | ||
27 | return this.runQuery(options) | 37 | return this.runQuery(options) |
28 | } | 38 | } |
29 | 39 | ||
30 | private buildWebtorrentFilesQuery (options: BuildVideoGetQueryOptions) { | 40 | private buildWebtorrentFilesQuery (options: FileQueryOptions) { |
31 | this.attributes = { | 41 | this.attributes = { |
32 | '"video"."id"': '' | 42 | '"video"."id"': '' |
33 | } | 43 | } |
34 | 44 | ||
35 | this.includeWebtorrentFiles() | 45 | this.includeWebtorrentFiles() |
36 | 46 | ||
37 | if (this.shouldIncludeRedundancies(options)) { | 47 | if (options.includeRedundancy) { |
38 | this.includeWebTorrentRedundancies() | 48 | this.includeWebTorrentRedundancies() |
39 | } | 49 | } |
40 | 50 | ||
@@ -43,14 +53,14 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
43 | this.query = this.buildQuery() | 53 | this.query = this.buildQuery() |
44 | } | 54 | } |
45 | 55 | ||
46 | private buildVideoStreamingPlaylistFilesQuery (options: BuildVideoGetQueryOptions) { | 56 | private buildVideoStreamingPlaylistFilesQuery (options: FileQueryOptions) { |
47 | this.attributes = { | 57 | this.attributes = { |
48 | '"video"."id"': '' | 58 | '"video"."id"': '' |
49 | } | 59 | } |
50 | 60 | ||
51 | this.includeStreamingPlaylistFiles() | 61 | this.includeStreamingPlaylistFiles() |
52 | 62 | ||
53 | if (this.shouldIncludeRedundancies(options)) { | 63 | if (options.includeRedundancy) { |
54 | this.includeStreamingPlaylistRedundancies() | 64 | this.includeStreamingPlaylistRedundancies() |
55 | } | 65 | } |
56 | 66 | ||
@@ -62,8 +72,4 @@ export class VideoFileQueryBuilder extends AbstractVideoQueryBuilder { | |||
62 | private buildQuery () { | 72 | private buildQuery () { |
63 | return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}` | 73 | return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}` |
64 | } | 74 | } |
65 | |||
66 | private shouldIncludeRedundancies (options: BuildVideoGetQueryOptions) { | ||
67 | return options.type === 'api' | ||
68 | } | ||
69 | } | 75 | } |
diff --git a/server/models/video/sql/video/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts index b0879c9ac..32e5c4ff7 100644 --- a/server/models/video/sql/video/video-model-get-query-builder.ts +++ b/server/models/video/sql/video/video-model-get-query-builder.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { pick } from 'lodash' | ||
1 | import { Sequelize, Transaction } from 'sequelize' | 2 | import { Sequelize, Transaction } from 'sequelize' |
2 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' | 3 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' |
3 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' | 4 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' |
@@ -50,15 +51,21 @@ export class VideoModelGetQueryBuilder { | |||
50 | } | 51 | } |
51 | 52 | ||
52 | async queryVideo (options: BuildVideoGetQueryOptions) { | 53 | async queryVideo (options: BuildVideoGetQueryOptions) { |
54 | const fileQueryOptions = { | ||
55 | ...pick(options, [ 'id', 'url', 'transaction', 'logging' ]), | ||
56 | |||
57 | includeRedundancy: this.shouldIncludeRedundancies(options) | ||
58 | } | ||
59 | |||
53 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ | 60 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ |
54 | this.videoQueryBuilder.queryVideos(options), | 61 | this.videoQueryBuilder.queryVideos(options), |
55 | 62 | ||
56 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 63 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
57 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options) | 64 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions) |
58 | : Promise.resolve(undefined), | 65 | : Promise.resolve(undefined), |
59 | 66 | ||
60 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) | 67 | VideoModelGetQueryBuilder.videoFilesInclude.has(options.type) |
61 | ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options) | 68 | ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) |
62 | : Promise.resolve(undefined) | 69 | : Promise.resolve(undefined) |
63 | ]) | 70 | ]) |
64 | 71 | ||
@@ -76,6 +83,10 @@ export class VideoModelGetQueryBuilder { | |||
76 | 83 | ||
77 | return videos[0] | 84 | return videos[0] |
78 | } | 85 | } |
86 | |||
87 | private shouldIncludeRedundancies (options: BuildVideoGetQueryOptions) { | ||
88 | return options.type === 'api' | ||
89 | } | ||
79 | } | 90 | } |
80 | 91 | ||
81 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { | 92 | export class VideosModelGetQuerySubBuilder extends AbstractVideoQueryBuilder { |
diff --git a/server/models/video/sql/video/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts index 2a4afc389..4fe6bc321 100644 --- a/server/models/video/sql/video/videos-model-list-query-builder.ts +++ b/server/models/video/sql/video/videos-model-list-query-builder.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { VideoInclude } from '@shared/models' | 1 | import { pick } from 'lodash' |
2 | import { Sequelize } from 'sequelize' | 2 | import { Sequelize } from 'sequelize' |
3 | import { VideoInclude } from '@shared/models' | ||
3 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' | 4 | import { AbstractVideoQueryBuilder } from './shared/abstract-video-query-builder' |
5 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' | ||
4 | import { VideoModelBuilder } from './shared/video-model-builder' | 6 | import { VideoModelBuilder } from './shared/video-model-builder' |
5 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' | 7 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' |
6 | 8 | ||
@@ -16,20 +18,46 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
16 | private innerQuery: string | 18 | private innerQuery: string |
17 | private innerSort: string | 19 | private innerSort: string |
18 | 20 | ||
21 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | ||
22 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | ||
23 | |||
19 | private readonly videoModelBuilder: VideoModelBuilder | 24 | private readonly videoModelBuilder: VideoModelBuilder |
20 | 25 | ||
21 | constructor (protected readonly sequelize: Sequelize) { | 26 | constructor (protected readonly sequelize: Sequelize) { |
22 | super(sequelize, 'list') | 27 | super(sequelize, 'list') |
23 | 28 | ||
24 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) | 29 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) |
30 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
31 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
25 | } | 32 | } |
26 | 33 | ||
27 | queryVideos (options: BuildVideosListQueryOptions) { | 34 | async queryVideos (options: BuildVideosListQueryOptions) { |
28 | this.buildInnerQuery(options) | 35 | this.buildInnerQuery(options) |
29 | this.buildMainQuery(options) | 36 | this.buildMainQuery(options) |
30 | 37 | ||
31 | return this.runQuery() | 38 | const rows = await this.runQuery() |
32 | .then(rows => this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include })) | 39 | |
40 | if (options.include & VideoInclude.FILES) { | ||
41 | const videoIds = Array.from(new Set(rows.map(r => r.id))) | ||
42 | |||
43 | if (videoIds.length !== 0) { | ||
44 | const fileQueryOptions = { | ||
45 | ...pick(options, [ 'transaction', 'logging' ]), | ||
46 | |||
47 | ids: videoIds, | ||
48 | includeRedundancy: false | ||
49 | } | ||
50 | |||
51 | const [ rowsWebTorrentFiles, rowsStreamingPlaylist ] = await Promise.all([ | ||
52 | this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(fileQueryOptions), | ||
53 | this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(fileQueryOptions) | ||
54 | ]) | ||
55 | |||
56 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include, rowsStreamingPlaylist, rowsWebTorrentFiles }) | ||
57 | } | ||
58 | } | ||
59 | |||
60 | return this.videoModelBuilder.buildVideosFromRows({ rows, include: options.include }) | ||
33 | } | 61 | } |
34 | 62 | ||
35 | private buildInnerQuery (options: BuildVideosListQueryOptions) { | 63 | private buildInnerQuery (options: BuildVideosListQueryOptions) { |
@@ -52,11 +80,6 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder { | |||
52 | this.includeAccounts() | 80 | this.includeAccounts() |
53 | this.includeThumbnails() | 81 | this.includeThumbnails() |
54 | 82 | ||
55 | if (options.include & VideoInclude.FILES) { | ||
56 | this.includeWebtorrentFiles() | ||
57 | this.includeStreamingPlaylistFiles() | ||
58 | } | ||
59 | |||
60 | if (options.user) { | 83 | if (options.user) { |
61 | this.includeUserHistory(options.user.id) | 84 | this.includeUserHistory(options.user.id) |
62 | } | 85 | } |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index d6dd1b8bb..91dafbcf1 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -311,6 +311,16 @@ export type SummaryOptions = { | |||
311 | ')' | 311 | ')' |
312 | ), | 312 | ), |
313 | 'viewsPerDay' | 313 | 'viewsPerDay' |
314 | ], | ||
315 | [ | ||
316 | literal( | ||
317 | '(' + | ||
318 | 'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' + | ||
319 | 'FROM "video" ' + | ||
320 | 'WHERE "video"."channelId" = "VideoChannelModel"."id"' + | ||
321 | ')' | ||
322 | ), | ||
323 | 'totalViews' | ||
314 | ] | 324 | ] |
315 | ] | 325 | ] |
316 | } | 326 | } |
@@ -766,6 +776,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
766 | }) | 776 | }) |
767 | } | 777 | } |
768 | 778 | ||
779 | const totalViews = this.get('totalViews') as number | ||
780 | |||
769 | const actor = this.Actor.toFormattedJSON() | 781 | const actor = this.Actor.toFormattedJSON() |
770 | const videoChannel = { | 782 | const videoChannel = { |
771 | id: this.id, | 783 | id: this.id, |
@@ -779,6 +791,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
779 | 791 | ||
780 | videosCount, | 792 | videosCount, |
781 | viewsPerDay, | 793 | viewsPerDay, |
794 | totalViews, | ||
782 | 795 | ||
783 | avatars: actor.avatars, | 796 | avatars: actor.avatars, |
784 | 797 | ||
diff --git a/server/models/video/video-live-session.ts b/server/models/video/video-live-session.ts index 2b4cde9f8..836620872 100644 --- a/server/models/video/video-live-session.ts +++ b/server/models/video/video-live-session.ts | |||
@@ -110,7 +110,7 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv | |||
110 | static listSessionsOfLiveForAPI (options: { videoId: number }) { | 110 | static listSessionsOfLiveForAPI (options: { videoId: number }) { |
111 | const { videoId } = options | 111 | const { videoId } = options |
112 | 112 | ||
113 | const query: FindOptions<VideoLiveSessionModel> = { | 113 | const query: FindOptions<AttributesOnly<VideoLiveSessionModel>> = { |
114 | where: { | 114 | where: { |
115 | liveVideoId: videoId | 115 | liveVideoId: videoId |
116 | }, | 116 | }, |
diff --git a/server/models/video/video-source.ts b/server/models/video/video-source.ts new file mode 100644 index 000000000..e306b160d --- /dev/null +++ b/server/models/video/video-source.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | import { Op } from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | ForeignKey, | ||
8 | Model, | ||
9 | Table, | ||
10 | UpdatedAt | ||
11 | } from 'sequelize-typescript' | ||
12 | import { AttributesOnly } from '@shared/typescript-utils' | ||
13 | import { VideoModel } from './video' | ||
14 | |||
15 | @Table({ | ||
16 | tableName: 'videoSource', | ||
17 | indexes: [ | ||
18 | { | ||
19 | fields: [ 'videoId' ], | ||
20 | where: { | ||
21 | videoId: { | ||
22 | [Op.ne]: null | ||
23 | } | ||
24 | } | ||
25 | } | ||
26 | ] | ||
27 | }) | ||
28 | export class VideoSourceModel extends Model<Partial<AttributesOnly<VideoSourceModel>>> { | ||
29 | @CreatedAt | ||
30 | createdAt: Date | ||
31 | |||
32 | @UpdatedAt | ||
33 | updatedAt: Date | ||
34 | |||
35 | @AllowNull(false) | ||
36 | @Column | ||
37 | filename: string | ||
38 | |||
39 | @ForeignKey(() => VideoModel) | ||
40 | @Column | ||
41 | videoId: number | ||
42 | |||
43 | @BelongsTo(() => VideoModel) | ||
44 | Video: VideoModel | ||
45 | |||
46 | static loadByVideoId (videoId) { | ||
47 | return VideoSourceModel.findOne({ where: { videoId } }) | ||
48 | } | ||
49 | |||
50 | toFormattedJSON () { | ||
51 | return { | ||
52 | filename: this.filename | ||
53 | } | ||
54 | } | ||
55 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e6a8d3f95..08adbced6 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -136,6 +136,7 @@ import { VideoPlaylistElementModel } from './video-playlist-element' | |||
136 | import { VideoShareModel } from './video-share' | 136 | import { VideoShareModel } from './video-share' |
137 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 137 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
138 | import { VideoTagModel } from './video-tag' | 138 | import { VideoTagModel } from './video-tag' |
139 | import { VideoSourceModel } from './video-source' | ||
139 | 140 | ||
140 | export enum ScopeNames { | 141 | export enum ScopeNames { |
141 | FOR_API = 'FOR_API', | 142 | FOR_API = 'FOR_API', |
@@ -597,6 +598,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
597 | }) | 598 | }) |
598 | VideoPlaylistElements: VideoPlaylistElementModel[] | 599 | VideoPlaylistElements: VideoPlaylistElementModel[] |
599 | 600 | ||
601 | @HasOne(() => VideoSourceModel, { | ||
602 | foreignKey: { | ||
603 | name: 'videoId', | ||
604 | allowNull: true | ||
605 | }, | ||
606 | onDelete: 'CASCADE' | ||
607 | }) | ||
608 | VideoSource: VideoSourceModel | ||
609 | |||
600 | @HasMany(() => VideoAbuseModel, { | 610 | @HasMany(() => VideoAbuseModel, { |
601 | foreignKey: { | 611 | foreignKey: { |
602 | name: 'videoId', | 612 | name: 'videoId', |