diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/utils.ts | 44 | ||||
-rw-r--r-- | server/models/video/video.ts | 63 |
2 files changed, 85 insertions, 22 deletions
diff --git a/server/models/utils.ts b/server/models/utils.ts index eb6653f3d..edb8e1161 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -1,23 +1,31 @@ | |||
1 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
2 | import { Sequelize } from 'sequelize-typescript' | 1 | import { Sequelize } from 'sequelize-typescript' |
3 | 2 | ||
4 | type SortType = { sortModel: any, sortValue: string } | 3 | type SortType = { sortModel: any, sortValue: string } |
5 | 4 | ||
5 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
6 | function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { | 6 | function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { |
7 | let field: any | 7 | const { direction, field } = buildDirectionAndField(value) |
8 | let direction: 'ASC' | 'DESC' | ||
9 | 8 | ||
10 | if (value.substring(0, 1) === '-') { | 9 | return [ [ field, direction ], lastSort ] |
11 | direction = 'DESC' | 10 | } |
12 | field = value.substring(1) | 11 | |
13 | } else { | 12 | function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { |
14 | direction = 'ASC' | 13 | let { direction, field } = buildDirectionAndField(value) |
15 | field = value | ||
16 | } | ||
17 | 14 | ||
18 | // Alias | 15 | // Alias |
19 | if (field.toLowerCase() === 'match') field = Sequelize.col('similarity') | 16 | if (field.toLowerCase() === 'match') field = Sequelize.col('similarity') |
20 | 17 | ||
18 | // Sort by aggregation | ||
19 | if (field.toLowerCase() === 'trending') { | ||
20 | return [ | ||
21 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], | ||
22 | |||
23 | [ Sequelize.col('VideoModel.views'), direction ], | ||
24 | |||
25 | lastSort | ||
26 | ] | ||
27 | } | ||
28 | |||
21 | return [ [ field, direction ], lastSort ] | 29 | return [ [ field, direction ], lastSort ] |
22 | } | 30 | } |
23 | 31 | ||
@@ -58,6 +66,7 @@ function createSimilarityAttribute (col: string, value: string) { | |||
58 | export { | 66 | export { |
59 | SortType, | 67 | SortType, |
60 | getSort, | 68 | getSort, |
69 | getVideoSort, | ||
61 | getSortOnModel, | 70 | getSortOnModel, |
62 | createSimilarityAttribute, | 71 | createSimilarityAttribute, |
63 | throwIfNotValid, | 72 | throwIfNotValid, |
@@ -73,3 +82,18 @@ function searchTrigramNormalizeValue (value: string) { | |||
73 | function searchTrigramNormalizeCol (col: string) { | 82 | function searchTrigramNormalizeCol (col: string) { |
74 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) | 83 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) |
75 | } | 84 | } |
85 | |||
86 | function buildDirectionAndField (value: string) { | ||
87 | let field: any | ||
88 | let direction: 'ASC' | 'DESC' | ||
89 | |||
90 | if (value.substring(0, 1) === '-') { | ||
91 | direction = 'DESC' | ||
92 | field = value.substring(1) | ||
93 | } else { | ||
94 | direction = 'ASC' | ||
95 | field = value | ||
96 | } | ||
97 | |||
98 | return { direction, field } | ||
99 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 67b123d77..6fb5ececa 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -17,6 +17,7 @@ import { | |||
17 | HasMany, | 17 | HasMany, |
18 | HasOne, | 18 | HasOne, |
19 | IFindOptions, | 19 | IFindOptions, |
20 | IIncludeOptions, | ||
20 | Is, | 21 | Is, |
21 | IsInt, | 22 | IsInt, |
22 | IsUUID, | 23 | IsUUID, |
@@ -24,8 +25,7 @@ import { | |||
24 | Model, | 25 | Model, |
25 | Scopes, | 26 | Scopes, |
26 | Table, | 27 | Table, |
27 | UpdatedAt, | 28 | UpdatedAt |
28 | IIncludeOptions | ||
29 | } from 'sequelize-typescript' | 29 | } from 'sequelize-typescript' |
30 | import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared' | 30 | import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared' |
31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
@@ -77,7 +77,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate' | |||
77 | import { ActorModel } from '../activitypub/actor' | 77 | import { ActorModel } from '../activitypub/actor' |
78 | import { AvatarModel } from '../avatar/avatar' | 78 | import { AvatarModel } from '../avatar/avatar' |
79 | import { ServerModel } from '../server/server' | 79 | import { ServerModel } from '../server/server' |
80 | import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 80 | import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' |
81 | import { TagModel } from './tag' | 81 | import { TagModel } from './tag' |
82 | import { VideoAbuseModel } from './video-abuse' | 82 | import { VideoAbuseModel } from './video-abuse' |
83 | import { VideoChannelModel } from './video-channel' | 83 | import { VideoChannelModel } from './video-channel' |
@@ -89,7 +89,7 @@ import { ScheduleVideoUpdateModel } from './schedule-video-update' | |||
89 | import { VideoCaptionModel } from './video-caption' | 89 | import { VideoCaptionModel } from './video-caption' |
90 | import { VideoBlacklistModel } from './video-blacklist' | 90 | import { VideoBlacklistModel } from './video-blacklist' |
91 | import { copy, remove, rename, stat, writeFile } from 'fs-extra' | 91 | import { copy, remove, rename, stat, writeFile } from 'fs-extra' |
92 | import { immutableAssign } from '../../tests/utils' | 92 | import { VideoViewModel } from './video-views' |
93 | 93 | ||
94 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 94 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
95 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 95 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -146,6 +146,7 @@ type AvailableForListIDsOptions = { | |||
146 | withFiles?: boolean | 146 | withFiles?: boolean |
147 | accountId?: number | 147 | accountId?: number |
148 | videoChannelId?: number | 148 | videoChannelId?: number |
149 | trendingDays?: number | ||
149 | } | 150 | } |
150 | 151 | ||
151 | @Scopes({ | 152 | @Scopes({ |
@@ -384,6 +385,21 @@ type AvailableForListIDsOptions = { | |||
384 | } | 385 | } |
385 | } | 386 | } |
386 | 387 | ||
388 | if (options.trendingDays) { | ||
389 | query.include.push({ | ||
390 | attributes: [], | ||
391 | model: VideoViewModel, | ||
392 | required: false, | ||
393 | where: { | ||
394 | startDate: { | ||
395 | [ Sequelize.Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays) | ||
396 | } | ||
397 | } | ||
398 | }) | ||
399 | |||
400 | query.subQuery = false | ||
401 | } | ||
402 | |||
387 | return query | 403 | return query |
388 | }, | 404 | }, |
389 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { | 405 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { |
@@ -649,6 +665,16 @@ export class VideoModel extends Model<VideoModel> { | |||
649 | }) | 665 | }) |
650 | VideoComments: VideoCommentModel[] | 666 | VideoComments: VideoCommentModel[] |
651 | 667 | ||
668 | @HasMany(() => VideoViewModel, { | ||
669 | foreignKey: { | ||
670 | name: 'videoId', | ||
671 | allowNull: false | ||
672 | }, | ||
673 | onDelete: 'cascade', | ||
674 | hooks: true | ||
675 | }) | ||
676 | VideoViews: VideoViewModel[] | ||
677 | |||
652 | @HasOne(() => ScheduleVideoUpdateModel, { | 678 | @HasOne(() => ScheduleVideoUpdateModel, { |
653 | foreignKey: { | 679 | foreignKey: { |
654 | name: 'videoId', | 680 | name: 'videoId', |
@@ -754,7 +780,7 @@ export class VideoModel extends Model<VideoModel> { | |||
754 | distinct: true, | 780 | distinct: true, |
755 | offset: start, | 781 | offset: start, |
756 | limit: count, | 782 | limit: count, |
757 | order: getSort('createdAt', [ 'Tags', 'name', 'ASC' ]), | 783 | order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ]), |
758 | where: { | 784 | where: { |
759 | id: { | 785 | id: { |
760 | [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')') | 786 | [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')') |
@@ -845,7 +871,7 @@ export class VideoModel extends Model<VideoModel> { | |||
845 | const query: IFindOptions<VideoModel> = { | 871 | const query: IFindOptions<VideoModel> = { |
846 | offset: start, | 872 | offset: start, |
847 | limit: count, | 873 | limit: count, |
848 | order: getSort(sort), | 874 | order: getVideoSort(sort), |
849 | include: [ | 875 | include: [ |
850 | { | 876 | { |
851 | model: VideoChannelModel, | 877 | model: VideoChannelModel, |
@@ -902,11 +928,19 @@ export class VideoModel extends Model<VideoModel> { | |||
902 | accountId?: number, | 928 | accountId?: number, |
903 | videoChannelId?: number, | 929 | videoChannelId?: number, |
904 | actorId?: number | 930 | actorId?: number |
931 | trendingDays?: number | ||
905 | }) { | 932 | }) { |
906 | const query = { | 933 | const query: IFindOptions<VideoModel> = { |
907 | offset: options.start, | 934 | offset: options.start, |
908 | limit: options.count, | 935 | limit: options.count, |
909 | order: getSort(options.sort) | 936 | order: getVideoSort(options.sort) |
937 | } | ||
938 | |||
939 | let trendingDays: number | ||
940 | if (options.sort.endsWith('trending')) { | ||
941 | trendingDays = CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | ||
942 | |||
943 | query.group = 'VideoModel.id' | ||
910 | } | 944 | } |
911 | 945 | ||
912 | // actorId === null has a meaning, so just check undefined | 946 | // actorId === null has a meaning, so just check undefined |
@@ -924,7 +958,8 @@ export class VideoModel extends Model<VideoModel> { | |||
924 | withFiles: options.withFiles, | 958 | withFiles: options.withFiles, |
925 | accountId: options.accountId, | 959 | accountId: options.accountId, |
926 | videoChannelId: options.videoChannelId, | 960 | videoChannelId: options.videoChannelId, |
927 | includeLocalVideos: options.includeLocalVideos | 961 | includeLocalVideos: options.includeLocalVideos, |
962 | trendingDays | ||
928 | } | 963 | } |
929 | 964 | ||
930 | return VideoModel.getAvailableForApi(query, queryOptions) | 965 | return VideoModel.getAvailableForApi(query, queryOptions) |
@@ -1006,7 +1041,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1006 | }, | 1041 | }, |
1007 | offset: options.start, | 1042 | offset: options.start, |
1008 | limit: options.count, | 1043 | limit: options.count, |
1009 | order: getSort(options.sort), | 1044 | order: getVideoSort(options.sort), |
1010 | where: { | 1045 | where: { |
1011 | [ Sequelize.Op.and ]: whereAnd | 1046 | [ Sequelize.Op.and ]: whereAnd |
1012 | } | 1047 | } |
@@ -1177,8 +1212,12 @@ export class VideoModel extends Model<VideoModel> { | |||
1177 | const secondQuery = { | 1212 | const secondQuery = { |
1178 | offset: 0, | 1213 | offset: 0, |
1179 | limit: query.limit, | 1214 | limit: query.limit, |
1180 | order: query.order, | 1215 | attributes: query.attributes, |
1181 | attributes: query.attributes | 1216 | order: [ // Keep original order |
1217 | Sequelize.literal( | ||
1218 | ids.map(id => `"VideoModel".id = ${id} DESC`).join(', ') | ||
1219 | ) | ||
1220 | ] | ||
1182 | } | 1221 | } |
1183 | const rows = await VideoModel.scope(apiScope).findAll(secondQuery) | 1222 | const rows = await VideoModel.scope(apiScope).findAll(secondQuery) |
1184 | 1223 | ||