aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/utils.ts44
-rw-r--r--server/models/video/video.ts63
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' ] ]
2import { Sequelize } from 'sequelize-typescript' 1import { Sequelize } from 'sequelize-typescript'
3 2
4type SortType = { sortModel: any, sortValue: string } 3type SortType = { sortModel: any, sortValue: string }
5 4
5// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
6function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { 6function 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 { 12function 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) {
58export { 66export {
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) {
73function searchTrigramNormalizeCol (col: string) { 82function 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
86function 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'
30import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared' 30import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
@@ -77,7 +77,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
77import { ActorModel } from '../activitypub/actor' 77import { ActorModel } from '../activitypub/actor'
78import { AvatarModel } from '../avatar/avatar' 78import { AvatarModel } from '../avatar/avatar'
79import { ServerModel } from '../server/server' 79import { ServerModel } from '../server/server'
80import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 80import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
81import { TagModel } from './tag' 81import { TagModel } from './tag'
82import { VideoAbuseModel } from './video-abuse' 82import { VideoAbuseModel } from './video-abuse'
83import { VideoChannelModel } from './video-channel' 83import { VideoChannelModel } from './video-channel'
@@ -89,7 +89,7 @@ import { ScheduleVideoUpdateModel } from './schedule-video-update'
89import { VideoCaptionModel } from './video-caption' 89import { VideoCaptionModel } from './video-caption'
90import { VideoBlacklistModel } from './video-blacklist' 90import { VideoBlacklistModel } from './video-blacklist'
91import { copy, remove, rename, stat, writeFile } from 'fs-extra' 91import { copy, remove, rename, stat, writeFile } from 'fs-extra'
92import { immutableAssign } from '../../tests/utils' 92import { 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
95const indexes: Sequelize.DefineIndexesOptions[] = [ 95const 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