diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/utils.ts | 2 | ||||
-rw-r--r-- | server/models/video/video.ts | 140 |
2 files changed, 114 insertions, 28 deletions
diff --git a/server/models/utils.ts b/server/models/utils.ts index 49d32c24f..393f8f036 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -14,7 +14,7 @@ function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { | |||
14 | } | 14 | } |
15 | 15 | ||
16 | // Alias | 16 | // Alias |
17 | if (field.toLowerCase() === 'bestmatch') field = Sequelize.col('similarity') | 17 | if (field.toLowerCase() === 'match') field = Sequelize.col('similarity') |
18 | 18 | ||
19 | return [ [ field, direction ], lastSort ] | 19 | return [ [ field, direction ], lastSort ] |
20 | } | 20 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 15b4dda5b..68116e309 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -93,6 +93,7 @@ import { VideoShareModel } from './video-share' | |||
93 | import { VideoTagModel } from './video-tag' | 93 | import { VideoTagModel } from './video-tag' |
94 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 94 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
95 | import { VideoCaptionModel } from './video-caption' | 95 | import { VideoCaptionModel } from './video-caption' |
96 | import { VideosSearchQuery } from '../../../shared/models/search' | ||
96 | 97 | ||
97 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 98 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
98 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 99 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -133,16 +134,22 @@ export enum ScopeNames { | |||
133 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE' | 134 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE' |
134 | } | 135 | } |
135 | 136 | ||
137 | type AvailableForListOptions = { | ||
138 | actorId: number, | ||
139 | filter?: VideoFilter, | ||
140 | categoryOneOf?: number[], | ||
141 | nsfw?: boolean, | ||
142 | licenceOneOf?: number[], | ||
143 | languageOneOf?: string[], | ||
144 | tagsOneOf?: string[], | ||
145 | tagsAllOf?: string[], | ||
146 | withFiles?: boolean, | ||
147 | accountId?: number, | ||
148 | videoChannelId?: number | ||
149 | } | ||
150 | |||
136 | @Scopes({ | 151 | @Scopes({ |
137 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: { | 152 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { |
138 | actorId: number, | ||
139 | hideNSFW: boolean, | ||
140 | filter?: VideoFilter, | ||
141 | category?: number, | ||
142 | withFiles?: boolean, | ||
143 | accountId?: number, | ||
144 | videoChannelId?: number | ||
145 | }) => { | ||
146 | const accountInclude = { | 153 | const accountInclude = { |
147 | attributes: [ 'id', 'name' ], | 154 | attributes: [ 'id', 'name' ], |
148 | model: AccountModel.unscoped(), | 155 | model: AccountModel.unscoped(), |
@@ -243,13 +250,55 @@ export enum ScopeNames { | |||
243 | }) | 250 | }) |
244 | } | 251 | } |
245 | 252 | ||
246 | // Hide nsfw videos? | 253 | // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN() |
247 | if (options.hideNSFW === true) { | 254 | if (options.tagsAllOf || options.tagsOneOf) { |
248 | query.where['nsfw'] = false | 255 | const createTagsIn = (tags: string[]) => { |
256 | return tags.map(t => VideoModel.sequelize.escape(t)) | ||
257 | .join(', ') | ||
258 | } | ||
259 | |||
260 | if (options.tagsOneOf) { | ||
261 | query.where['id'][Sequelize.Op.in] = Sequelize.literal( | ||
262 | '(' + | ||
263 | 'SELECT "videoId" FROM "videoTag" ' + | ||
264 | 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
265 | 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsOneOf) + ')' + | ||
266 | ')' | ||
267 | ) | ||
268 | } | ||
269 | |||
270 | if (options.tagsAllOf) { | ||
271 | query.where['id'][Sequelize.Op.in] = Sequelize.literal( | ||
272 | '(' + | ||
273 | 'SELECT "videoId" FROM "videoTag" ' + | ||
274 | 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
275 | 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsAllOf) + ')' + | ||
276 | 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length + | ||
277 | ')' | ||
278 | ) | ||
279 | } | ||
280 | } | ||
281 | |||
282 | if (options.nsfw === true || options.nsfw === false) { | ||
283 | query.where['nsfw'] = options.nsfw | ||
284 | } | ||
285 | |||
286 | if (options.categoryOneOf) { | ||
287 | query.where['category'] = { | ||
288 | [Sequelize.Op.or]: options.categoryOneOf | ||
289 | } | ||
290 | } | ||
291 | |||
292 | if (options.licenceOneOf) { | ||
293 | query.where['licence'] = { | ||
294 | [Sequelize.Op.or]: options.licenceOneOf | ||
295 | } | ||
249 | } | 296 | } |
250 | 297 | ||
251 | if (options.category) { | 298 | if (options.languageOneOf) { |
252 | query.where['category'] = options.category | 299 | query.where['language'] = { |
300 | [Sequelize.Op.or]: options.languageOneOf | ||
301 | } | ||
253 | } | 302 | } |
254 | 303 | ||
255 | if (options.accountId) { | 304 | if (options.accountId) { |
@@ -756,9 +805,13 @@ export class VideoModel extends Model<VideoModel> { | |||
756 | start: number, | 805 | start: number, |
757 | count: number, | 806 | count: number, |
758 | sort: string, | 807 | sort: string, |
759 | hideNSFW: boolean, | 808 | nsfw: boolean, |
760 | withFiles: boolean, | 809 | withFiles: boolean, |
761 | category?: number, | 810 | categoryOneOf?: number[], |
811 | licenceOneOf?: number[], | ||
812 | languageOneOf?: string[], | ||
813 | tagsOneOf?: string[], | ||
814 | tagsAllOf?: string[], | ||
762 | filter?: VideoFilter, | 815 | filter?: VideoFilter, |
763 | accountId?: number, | 816 | accountId?: number, |
764 | videoChannelId?: number | 817 | videoChannelId?: number |
@@ -774,13 +827,17 @@ export class VideoModel extends Model<VideoModel> { | |||
774 | method: [ | 827 | method: [ |
775 | ScopeNames.AVAILABLE_FOR_LIST, { | 828 | ScopeNames.AVAILABLE_FOR_LIST, { |
776 | actorId: serverActor.id, | 829 | actorId: serverActor.id, |
777 | hideNSFW: options.hideNSFW, | 830 | nsfw: options.nsfw, |
778 | category: options.category, | 831 | categoryOneOf: options.categoryOneOf, |
832 | licenceOneOf: options.licenceOneOf, | ||
833 | languageOneOf: options.languageOneOf, | ||
834 | tagsOneOf: options.tagsOneOf, | ||
835 | tagsAllOf: options.tagsAllOf, | ||
779 | filter: options.filter, | 836 | filter: options.filter, |
780 | withFiles: options.withFiles, | 837 | withFiles: options.withFiles, |
781 | accountId: options.accountId, | 838 | accountId: options.accountId, |
782 | videoChannelId: options.videoChannelId | 839 | videoChannelId: options.videoChannelId |
783 | } | 840 | } as AvailableForListOptions |
784 | ] | 841 | ] |
785 | } | 842 | } |
786 | 843 | ||
@@ -794,15 +851,39 @@ export class VideoModel extends Model<VideoModel> { | |||
794 | }) | 851 | }) |
795 | } | 852 | } |
796 | 853 | ||
797 | static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) { | 854 | static async searchAndPopulateAccountAndServer (options: VideosSearchQuery) { |
855 | const whereAnd = [ ] | ||
856 | |||
857 | if (options.startDate || options.endDate) { | ||
858 | const publishedAtRange = { } | ||
859 | |||
860 | if (options.startDate) publishedAtRange[Sequelize.Op.gte] = options.startDate | ||
861 | if (options.endDate) publishedAtRange[Sequelize.Op.lte] = options.endDate | ||
862 | |||
863 | whereAnd.push({ publishedAt: publishedAtRange }) | ||
864 | } | ||
865 | |||
866 | if (options.durationMin || options.durationMax) { | ||
867 | const durationRange = { } | ||
868 | |||
869 | if (options.durationMin) durationRange[Sequelize.Op.gte] = options.durationMin | ||
870 | if (options.durationMax) durationRange[Sequelize.Op.lte] = options.durationMax | ||
871 | |||
872 | whereAnd.push({ duration: durationRange }) | ||
873 | } | ||
874 | |||
875 | whereAnd.push(createSearchTrigramQuery('VideoModel.name', options.search)) | ||
876 | |||
798 | const query: IFindOptions<VideoModel> = { | 877 | const query: IFindOptions<VideoModel> = { |
799 | attributes: { | 878 | attributes: { |
800 | include: [ createSimilarityAttribute('VideoModel.name', value) ] | 879 | include: [ createSimilarityAttribute('VideoModel.name', options.search) ] |
801 | }, | 880 | }, |
802 | offset: start, | 881 | offset: options.start, |
803 | limit: count, | 882 | limit: options.count, |
804 | order: getSort(sort), | 883 | order: getSort(options.sort), |
805 | where: createSearchTrigramQuery('VideoModel.name', value) | 884 | where: { |
885 | [ Sequelize.Op.and ]: whereAnd | ||
886 | } | ||
806 | } | 887 | } |
807 | 888 | ||
808 | const serverActor = await getServerActor() | 889 | const serverActor = await getServerActor() |
@@ -810,8 +891,13 @@ export class VideoModel extends Model<VideoModel> { | |||
810 | method: [ | 891 | method: [ |
811 | ScopeNames.AVAILABLE_FOR_LIST, { | 892 | ScopeNames.AVAILABLE_FOR_LIST, { |
812 | actorId: serverActor.id, | 893 | actorId: serverActor.id, |
813 | hideNSFW | 894 | nsfw: options.nsfw, |
814 | } | 895 | categoryOneOf: options.categoryOneOf, |
896 | licenceOneOf: options.licenceOneOf, | ||
897 | languageOneOf: options.languageOneOf, | ||
898 | tagsOneOf: options.tagsOneOf, | ||
899 | tagsAllOf: options.tagsAllOf | ||
900 | } as AvailableForListOptions | ||
815 | ] | 901 | ] |
816 | } | 902 | } |
817 | 903 | ||