From 1fd61899eaea245a5844e33e21f04b2562f16e5e Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 3 May 2021 11:06:19 +0200 Subject: Add ability to filter my videos by live --- server/controllers/api/accounts.ts | 27 +++++----- server/controllers/api/users/me.ts | 3 +- server/controllers/api/users/my-subscriptions.ts | 23 +++++---- server/controllers/api/video-channel.ts | 23 +++++---- server/controllers/api/videos/index.ts | 25 ++++----- server/helpers/custom-validators/search.ts | 4 +- server/middlewares/validators/videos/videos.ts | 8 ++- server/models/video/video-query-builder.ts | 12 +++-- server/models/video/video.ts | 52 ++++++++++++++----- server/tests/api/live/live.ts | 65 ++++++++++++++++++++++++ server/tests/api/search/search-videos.ts | 49 +++++++++++++++++- server/tests/api/videos/single-server.ts | 4 +- 12 files changed, 224 insertions(+), 71 deletions(-) (limited to 'server') diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index e31924a94..49a8e3195 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts @@ -1,9 +1,10 @@ import * as express from 'express' import { getServerActor } from '@server/models/application/application' +import { VideosWithSearchCommonQuery } from '@shared/models' import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' import { getFormattedObjects } from '../../helpers/utils' -import { Hooks } from '../../lib/plugins/hooks' import { JobQueue } from '../../lib/job-queue' +import { Hooks } from '../../lib/plugins/hooks' import { asyncMiddleware, authenticate, @@ -158,25 +159,27 @@ async function listAccountVideos (req: express.Request, res: express.Response) { const account = res.locals.account const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined const countVideos = getCountVideos(req) + const query = req.query as VideosWithSearchCommonQuery const apiOptions = await Hooks.wrapObject({ followerActorId, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, + start: query.start, + count: query.count, + sort: query.sort, includeLocalVideos: true, - categoryOneOf: req.query.categoryOneOf, - licenceOneOf: req.query.licenceOneOf, - languageOneOf: req.query.languageOneOf, - tagsOneOf: req.query.tagsOneOf, - tagsAllOf: req.query.tagsAllOf, - filter: req.query.filter, - nsfw: buildNSFWFilter(res, req.query.nsfw), + categoryOneOf: query.categoryOneOf, + licenceOneOf: query.licenceOneOf, + languageOneOf: query.languageOneOf, + tagsOneOf: query.tagsOneOf, + tagsAllOf: query.tagsAllOf, + filter: query.filter, + isLive: query.isLive, + nsfw: buildNSFWFilter(res, query.nsfw), withFiles: false, accountId: account.id, user: res.locals.oauth ? res.locals.oauth.token.User : undefined, countVideos, - search: req.query.search + search: query.search }, 'filter:api.accounts.videos.list.params') const resultList = await Hooks.wrapPromiseFun( diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 9f9d2d77f..0763d1900 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts @@ -111,7 +111,8 @@ async function getUserVideos (req: express.Request, res: express.Response) { start: req.query.start, count: req.query.count, sort: req.query.sort, - search: req.query.search + search: req.query.search, + isLive: req.query.isLive }, 'filter:api.user.me.videos.list.params') const resultList = await Hooks.wrapPromiseFun( diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index e8949ee59..56b93276f 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts @@ -2,8 +2,8 @@ import 'multer' import * as express from 'express' import { sendUndoFollow } from '@server/lib/activitypub/send' import { VideoChannelModel } from '@server/models/video/video-channel' +import { VideosCommonQuery } from '@shared/models' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' -import { VideoFilter } from '../../../../shared/models/videos/video-query.type' import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' import { getFormattedObjects } from '../../../helpers/utils' import { WEBSERVER } from '../../../initializers/constants' @@ -170,19 +170,20 @@ async function getUserSubscriptions (req: express.Request, res: express.Response async function getUserSubscriptionVideos (req: express.Request, res: express.Response) { const user = res.locals.oauth.token.User const countVideos = getCountVideos(req) + const query = req.query as VideosCommonQuery const resultList = await VideoModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, + start: query.start, + count: query.count, + sort: query.sort, includeLocalVideos: false, - categoryOneOf: req.query.categoryOneOf, - licenceOneOf: req.query.licenceOneOf, - languageOneOf: req.query.languageOneOf, - tagsOneOf: req.query.tagsOneOf, - tagsAllOf: req.query.tagsAllOf, - nsfw: buildNSFWFilter(res, req.query.nsfw), - filter: req.query.filter as VideoFilter, + categoryOneOf: query.categoryOneOf, + licenceOneOf: query.licenceOneOf, + languageOneOf: query.languageOneOf, + tagsOneOf: query.tagsOneOf, + tagsAllOf: query.tagsAllOf, + nsfw: buildNSFWFilter(res, query.nsfw), + filter: query.filter, withFiles: false, followerActorId: user.Account.Actor.id, user, diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 149d6cfb4..a755d7e57 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts @@ -2,7 +2,7 @@ import * as express from 'express' import { Hooks } from '@server/lib/plugins/hooks' import { getServerActor } from '@server/models/application/application' import { MChannelBannerAccountDefault } from '@server/types/models' -import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' +import { ActorImageType, VideoChannelCreate, VideoChannelUpdate, VideosCommonQuery } from '../../../shared' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' import { resetSequelizeInstance } from '../../helpers/database-utils' @@ -312,20 +312,21 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon const videoChannelInstance = res.locals.videoChannel const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined const countVideos = getCountVideos(req) + const query = req.query as VideosCommonQuery const apiOptions = await Hooks.wrapObject({ followerActorId, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, + start: query.start, + count: query.count, + sort: query.sort, includeLocalVideos: true, - categoryOneOf: req.query.categoryOneOf, - licenceOneOf: req.query.licenceOneOf, - languageOneOf: req.query.languageOneOf, - tagsOneOf: req.query.tagsOneOf, - tagsAllOf: req.query.tagsAllOf, - filter: req.query.filter, - nsfw: buildNSFWFilter(res, req.query.nsfw), + categoryOneOf: query.categoryOneOf, + licenceOneOf: query.licenceOneOf, + languageOneOf: query.languageOneOf, + tagsOneOf: query.tagsOneOf, + tagsAllOf: query.tagsAllOf, + filter: query.filter, + nsfw: buildNSFWFilter(res, query.nsfw), withFiles: false, videoChannelId: videoChannelInstance.id, user: res.locals.oauth ? res.locals.oauth.token.User : undefined, diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 7fee278f2..6ec6478e4 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -10,9 +10,8 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' import { getServerActor } from '@server/models/application/application' import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' -import { VideoCreate, VideoState, VideoUpdate } from '../../../../shared' +import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' -import { VideoFilter } from '../../../../shared/models/videos/video-query.type' import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' @@ -494,20 +493,22 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response } async function listVideos (req: express.Request, res: express.Response) { + const query = req.query as VideosCommonQuery const countVideos = getCountVideos(req) const apiOptions = await Hooks.wrapObject({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, + start: query.start, + count: query.count, + sort: query.sort, includeLocalVideos: true, - categoryOneOf: req.query.categoryOneOf, - licenceOneOf: req.query.licenceOneOf, - languageOneOf: req.query.languageOneOf, - tagsOneOf: req.query.tagsOneOf, - tagsAllOf: req.query.tagsAllOf, - nsfw: buildNSFWFilter(res, req.query.nsfw), - filter: req.query.filter as VideoFilter, + categoryOneOf: query.categoryOneOf, + licenceOneOf: query.licenceOneOf, + languageOneOf: query.languageOneOf, + tagsOneOf: query.tagsOneOf, + tagsAllOf: query.tagsAllOf, + nsfw: buildNSFWFilter(res, query.nsfw), + isLive: query.isLive, + filter: query.filter, withFiles: false, user: res.locals.oauth ? res.locals.oauth.token.User : undefined, countVideos diff --git a/server/helpers/custom-validators/search.ts b/server/helpers/custom-validators/search.ts index 429fcafcf..a8f258838 100644 --- a/server/helpers/custom-validators/search.ts +++ b/server/helpers/custom-validators/search.ts @@ -11,7 +11,7 @@ function isStringArray (value: any) { return isArray(value) && value.every(v => typeof v === 'string') } -function isNSFWQueryValid (value: any) { +function isBooleanBothQueryValid (value: any) { return value === 'true' || value === 'false' || value === 'both' } @@ -32,6 +32,6 @@ function isSearchTargetValid (value: SearchTargetType) { export { isNumberArray, isStringArray, - isNSFWQueryValid, + isBooleanBothQueryValid, isSearchTargetValid } diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 4d31d3dcb..bb617d77c 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -20,7 +20,7 @@ import { toIntOrNull, toValueOrNull } from '../../../helpers/custom-validators/misc' -import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' +import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' import { isScheduleVideoUpdatePrivacyValid, @@ -439,7 +439,11 @@ const commonVideosFiltersValidator = [ .custom(isStringArray).withMessage('Should have a valid all of tags array'), query('nsfw') .optional() - .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'), + .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'), + query('isLive') + .optional() + .customSanitizer(toBooleanOrNull) + .custom(isBooleanValid).withMessage('Should have a valid live boolean'), query('filter') .optional() .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'), diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 4d95ddee2..155afe64b 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts @@ -16,9 +16,11 @@ export type BuildVideosQueryOptions = { start: number sort: string + nsfw?: boolean filter?: VideoFilter + isLive?: boolean + categoryOneOf?: number[] - nsfw?: boolean licenceOneOf?: number[] languageOneOf?: string[] tagsOneOf?: string[] @@ -199,10 +201,14 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) if (options.nsfw === true) { and.push('"video"."nsfw" IS TRUE') + } else if (options.nsfw === false) { + and.push('"video"."nsfw" IS FALSE') } - if (options.nsfw === false) { - and.push('"video"."nsfw" IS FALSE') + if (options.isLive === true) { + and.push('"video"."isLive" IS TRUE') + } else if (options.isLive === false) { + and.push('"video"."isLive" IS FALSE') } if (options.categoryOneOf) { diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 422bf6deb..e55a21a6b 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1021,14 +1021,28 @@ export class VideoModel extends Model { start: number count: number sort: string + isLive?: boolean search?: string }) { - const { accountId, start, count, sort, search } = options + const { accountId, start, count, sort, search, isLive } = options function buildBaseQuery (): FindOptions { - let baseQuery = { + const where: WhereOptions = {} + + if (search) { + where.name = { + [Op.iLike]: '%' + search + '%' + } + } + + if (isLive) { + where.isLive = isLive + } + + const baseQuery = { offset: start, limit: count, + where, order: getVideoSort(sort), include: [ { @@ -1047,16 +1061,6 @@ export class VideoModel extends Model { ] } - if (search) { - baseQuery = Object.assign(baseQuery, { - where: { - name: { - [Op.iLike]: '%' + search + '%' - } - } - }) - } - return baseQuery } @@ -1084,23 +1088,34 @@ export class VideoModel extends Model { start: number count: number sort: string + nsfw: boolean + filter?: VideoFilter + isLive?: boolean + includeLocalVideos: boolean withFiles: boolean + categoryOneOf?: number[] licenceOneOf?: number[] languageOneOf?: string[] tagsOneOf?: string[] tagsAllOf?: string[] - filter?: VideoFilter + accountId?: number videoChannelId?: number + followerActorId?: number + videoPlaylistId?: number + trendingDays?: number + user?: MUserAccountId historyOfUser?: MUserId + countVideos?: boolean + search?: string }) { if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { @@ -1128,6 +1143,7 @@ export class VideoModel extends Model { followerActorId, serverAccountId: serverActor.Account.id, nsfw: options.nsfw, + isLive: options.isLive, categoryOneOf: options.categoryOneOf, licenceOneOf: options.licenceOneOf, languageOneOf: options.languageOneOf, @@ -1160,6 +1176,7 @@ export class VideoModel extends Model { originallyPublishedStartDate?: string originallyPublishedEndDate?: string nsfw?: boolean + isLive?: boolean categoryOneOf?: number[] licenceOneOf?: number[] languageOneOf?: string[] @@ -1171,23 +1188,32 @@ export class VideoModel extends Model { filter?: VideoFilter }) { const serverActor = await getServerActor() + const queryOptions = { followerActorId: serverActor.id, serverAccountId: serverActor.Account.id, + includeLocalVideos: options.includeLocalVideos, nsfw: options.nsfw, + isLive: options.isLive, + categoryOneOf: options.categoryOneOf, licenceOneOf: options.licenceOneOf, languageOneOf: options.languageOneOf, + tagsOneOf: options.tagsOneOf, tagsAllOf: options.tagsAllOf, + user: options.user, filter: options.filter, + start: options.start, count: options.count, sort: options.sort, + startDate: options.startDate, endDate: options.endDate, + originallyPublishedStartDate: options.originallyPublishedStartDate, originallyPublishedEndDate: options.originallyPublishedEndDate, diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index d48e2a8ee..57fb58150 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts @@ -19,10 +19,12 @@ import { doubleFollow, flushAndRunMultipleServers, getLive, + getMyVideosWithFilter, getPlaylist, getVideo, getVideoIdFromUUID, getVideosList, + getVideosWithFilters, killallServers, makeRawRequest, removeVideo, @@ -37,6 +39,7 @@ import { testImage, updateCustomSubConfig, updateLive, + uploadVideoAndGetId, viewVideo, wait, waitJobs, @@ -229,6 +232,68 @@ describe('Test live', function () { }) }) + describe('Live filters', function () { + let command: any + let liveVideoId: string + let vodVideoId: string + + before(async function () { + this.timeout(120000) + + vodVideoId = (await uploadVideoAndGetId({ server: servers[0], videoName: 'vod video' })).uuid + + const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].videoChannel.id } + const resLive = await createLive(servers[0].url, servers[0].accessToken, liveOptions) + liveVideoId = resLive.body.video.uuid + + command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) + await waitUntilLivePublishedOnAllServers(liveVideoId) + await waitJobs(servers) + }) + + it('Should only display lives', async function () { + const res = await getVideosWithFilters(servers[0].url, { isLive: true }) + + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) + expect(res.body.data[0].name).to.equal('live') + }) + + it('Should not display lives', async function () { + const res = await getVideosWithFilters(servers[0].url, { isLive: false }) + + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) + expect(res.body.data[0].name).to.equal('vod video') + }) + + it('Should display my lives', async function () { + this.timeout(60000) + + await stopFfmpeg(command) + await waitJobs(servers) + + const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: true }) + const videos = res.body.data as Video[] + + const result = videos.every(v => v.isLive) + expect(result).to.be.true + }) + + it('Should not display my lives', async function () { + const res = await getMyVideosWithFilter(servers[0].url, servers[0].accessToken, { isLive: false }) + const videos = res.body.data as Video[] + + const result = videos.every(v => !v.isLive) + expect(result).to.be.true + }) + + after(async function () { + await removeVideo(servers[0].url, servers[0].accessToken, vodVideoId) + await removeVideo(servers[0].url, servers[0].accessToken, liveVideoId) + }) + }) + describe('Stream checks', function () { let liveVideo: LiveVideo & VideoDetails let rtmpUrl: string diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts index e05c3a269..5b8907961 100644 --- a/server/tests/api/search/search-videos.ts +++ b/server/tests/api/search/search-videos.ts @@ -1,17 +1,24 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ -import * as chai from 'chai' import 'mocha' +import * as chai from 'chai' +import { VideoPrivacy } from '@shared/models' import { advancedVideosSearch, cleanupTests, + createLive, flushAndRunServer, immutableAssign, searchVideo, + sendRTMPStreamInVideo, ServerInfo, setAccessTokensToServers, + setDefaultVideoChannel, + stopFfmpeg, + updateCustomSubConfig, uploadVideo, - wait + wait, + waitUntilLivePublished } from '../../../../shared/extra-utils' import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions' @@ -28,6 +35,7 @@ describe('Test videos search', function () { server = await flushAndRunServer(1) await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) { const attributes1 = { @@ -449,6 +457,43 @@ describe('Test videos search', function () { expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3') }) + it('Should search by live', async function () { + this.timeout(30000) + + { + const options = { + search: { + searchIndex: { enabled: false } + }, + live: { enabled: true } + } + await updateCustomSubConfig(server.url, server.accessToken, options) + } + + { + const res = await advancedVideosSearch(server.url, { isLive: true }) + + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + } + + { + const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.videoChannel.id } + const resLive = await createLive(server.url, server.accessToken, liveOptions) + const liveVideoId = resLive.body.video.uuid + + const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId) + await waitUntilLivePublished(server.url, server.accessToken, liveVideoId) + + const res = await advancedVideosSearch(server.url, { isLive: true }) + + expect(res.body.total).to.equal(1) + expect(res.body.data[0].name).to.equal('live') + + await stopFfmpeg(command) + } + }) + after(async function () { await cleanupTests([ server ]) }) diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index da90223b8..a79648bf7 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts @@ -387,11 +387,11 @@ describe('Test a single server', function () { }) it('Should filter by tags and category', async function () { - const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 }) + const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] }) expect(res1.body.total).to.equal(1) expect(res1.body.data[0].name).to.equal('my super video updated') - const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 }) + const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] }) expect(res2.body.total).to.equal(0) }) -- cgit v1.2.3