aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts3
-rw-r--r--server/controllers/api/accounts.ts4
-rw-r--r--server/controllers/api/search.ts4
-rw-r--r--server/controllers/api/server/follows.ts16
-rw-r--r--server/controllers/api/users/index.ts2
-rw-r--r--server/controllers/api/video-channel.ts4
-rw-r--r--server/controllers/api/videos/captions.ts6
-rw-r--r--server/controllers/api/videos/comment.ts6
-rw-r--r--server/controllers/api/videos/index.ts6
-rw-r--r--server/controllers/api/videos/watching.ts36
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/helpers/custom-validators/videos.ts13
-rw-r--r--server/helpers/express-utils.ts3
-rw-r--r--server/helpers/ffmpeg-utils.ts21
-rw-r--r--server/helpers/utils.ts17
-rw-r--r--server/helpers/video.ts4
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/initializers/database.ts4
-rw-r--r--server/lib/activitypub/crawl.ts2
-rw-r--r--server/lib/job-queue/handlers/video-file.ts4
-rw-r--r--server/lib/redis.ts54
-rw-r--r--server/lib/video-transcoding.ts9
-rw-r--r--server/middlewares/cache.ts2
-rw-r--r--server/middlewares/validators/index.ts4
-rw-r--r--server/middlewares/validators/search.ts38
-rw-r--r--server/middlewares/validators/videos/index.ts8
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts (renamed from server/middlewares/validators/video-abuses.ts)10
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts (renamed from server/middlewares/validators/video-blacklist.ts)10
-rw-r--r--server/middlewares/validators/videos/video-captions.ts (renamed from server/middlewares/validators/video-captions.ts)16
-rw-r--r--server/middlewares/validators/videos/video-channels.ts (renamed from server/middlewares/validators/video-channels.ts)18
-rw-r--r--server/middlewares/validators/videos/video-comments.ts (renamed from server/middlewares/validators/video-comments.ts)18
-rw-r--r--server/middlewares/validators/videos/video-imports.ts (renamed from server/middlewares/validators/video-imports.ts)16
-rw-r--r--server/middlewares/validators/videos/video-watch.ts28
-rw-r--r--server/middlewares/validators/videos/videos.ts (renamed from server/middlewares/validators/videos.ts)101
-rw-r--r--server/models/account/account.ts3
-rw-r--r--server/models/account/user-video-history.ts55
-rw-r--r--server/models/account/user.ts23
-rw-r--r--server/models/activitypub/actor-follow.ts90
-rw-r--r--server/models/video/video-format-utils.ts9
-rw-r--r--server/models/video/video.ts111
-rw-r--r--server/tests/api/check-params/index.ts2
-rw-r--r--server/tests/api/check-params/videos-filter.ts127
-rw-r--r--server/tests/api/check-params/videos-history.ts79
-rw-r--r--server/tests/api/server/follows.ts40
-rw-r--r--server/tests/api/users/users-multiple-servers.ts6
-rw-r--r--server/tests/api/users/users.ts36
-rw-r--r--server/tests/api/videos/index.ts2
-rw-r--r--server/tests/api/videos/video-transcoder.ts63
-rw-r--r--server/tests/api/videos/videos-filter.ts130
-rw-r--r--server/tests/api/videos/videos-history.ts128
-rw-r--r--server/tests/utils/miscs/miscs.ts4
-rw-r--r--server/tests/utils/server/follows.ts6
-rw-r--r--server/tests/utils/users/users.ts3
-rw-r--r--server/tests/utils/videos/video-history.ts14
54 files changed, 1200 insertions, 232 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 6229c44aa..433186179 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -13,8 +13,7 @@ import {
13 localVideoChannelValidator, 13 localVideoChannelValidator,
14 videosCustomGetValidator 14 videosCustomGetValidator
15} from '../../middlewares' 15} from '../../middlewares'
16import { videosGetValidator, videosShareValidator } from '../../middlewares/validators' 16import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
17import { videoCommentGetValidator } from '../../middlewares/validators/video-comments'
18import { AccountModel } from '../../models/account/account' 17import { AccountModel } from '../../models/account/account'
19import { ActorModel } from '../../models/activitypub/actor' 18import { ActorModel } from '../../models/activitypub/actor'
20import { ActorFollowModel } from '../../models/activitypub/actor-follow' 19import { ActorFollowModel } from '../../models/activitypub/actor-follow'
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index b7691ccba..8e3f60010 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -86,9 +86,11 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
86 languageOneOf: req.query.languageOneOf, 86 languageOneOf: req.query.languageOneOf,
87 tagsOneOf: req.query.tagsOneOf, 87 tagsOneOf: req.query.tagsOneOf,
88 tagsAllOf: req.query.tagsAllOf, 88 tagsAllOf: req.query.tagsAllOf,
89 filter: req.query.filter,
89 nsfw: buildNSFWFilter(res, req.query.nsfw), 90 nsfw: buildNSFWFilter(res, req.query.nsfw),
90 withFiles: false, 91 withFiles: false,
91 accountId: account.id 92 accountId: account.id,
93 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
92 }) 94 })
93 95
94 return res.json(getFormattedObjects(resultList.data, resultList.total)) 96 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index fd4db7a54..a8a6cfb08 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -117,7 +117,9 @@ function searchVideos (req: express.Request, res: express.Response) {
117async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { 117async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
118 const options = Object.assign(query, { 118 const options = Object.assign(query, {
119 includeLocalVideos: true, 119 includeLocalVideos: true,
120 nsfw: buildNSFWFilter(res, query.nsfw) 120 nsfw: buildNSFWFilter(res, query.nsfw),
121 filter: query.filter,
122 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
121 }) 123 })
122 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 124 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
123 125
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index d62400e42..9fa6c34ba 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -61,14 +61,26 @@ export {
61 61
62async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { 62async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
63 const serverActor = await getServerActor() 63 const serverActor = await getServerActor()
64 const resultList = await ActorFollowModel.listFollowingForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) 64 const resultList = await ActorFollowModel.listFollowingForApi(
65 serverActor.id,
66 req.query.start,
67 req.query.count,
68 req.query.sort,
69 req.query.search
70 )
65 71
66 return res.json(getFormattedObjects(resultList.data, resultList.total)) 72 return res.json(getFormattedObjects(resultList.data, resultList.total))
67} 73}
68 74
69async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { 75async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
70 const serverActor = await getServerActor() 76 const serverActor = await getServerActor()
71 const resultList = await ActorFollowModel.listFollowersForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) 77 const resultList = await ActorFollowModel.listFollowersForApi(
78 serverActor.id,
79 req.query.start,
80 req.query.count,
81 req.query.sort,
82 req.query.search
83 )
72 84
73 return res.json(getFormattedObjects(resultList.data, resultList.total)) 85 return res.json(getFormattedObjects(resultList.data, resultList.total))
74} 86}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 0b0081520..4f8137c03 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -238,7 +238,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response, n
238} 238}
239 239
240async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 240async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) {
241 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) 241 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search)
242 242
243 return res.json(getFormattedObjects(resultList.data, resultList.total)) 243 return res.json(getFormattedObjects(resultList.data, resultList.total))
244} 244}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 1fa842d9c..c84d1be58 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -215,9 +215,11 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
215 languageOneOf: req.query.languageOneOf, 215 languageOneOf: req.query.languageOneOf,
216 tagsOneOf: req.query.tagsOneOf, 216 tagsOneOf: req.query.tagsOneOf,
217 tagsAllOf: req.query.tagsAllOf, 217 tagsAllOf: req.query.tagsAllOf,
218 filter: req.query.filter,
218 nsfw: buildNSFWFilter(res, req.query.nsfw), 219 nsfw: buildNSFWFilter(res, req.query.nsfw),
219 withFiles: false, 220 withFiles: false,
220 videoChannelId: videoChannelInstance.id 221 videoChannelId: videoChannelInstance.id,
222 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
221 }) 223 })
222 224
223 return res.json(getFormattedObjects(resultList.data, resultList.total)) 225 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 4cf8de1ef..3ba918189 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -1,10 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' 2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
3import { 3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
4 addVideoCaptionValidator,
5 deleteVideoCaptionValidator,
6 listVideoCaptionsValidator
7} from '../../../middlewares/validators/video-captions'
8import { createReqFiles } from '../../../helpers/express-utils' 4import { createReqFiles } from '../../../helpers/express-utils'
9import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' 5import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers'
10import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index dc25e1e85..4f2b4faee 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -13,14 +13,14 @@ import {
13 setDefaultPagination, 13 setDefaultPagination,
14 setDefaultSort 14 setDefaultSort
15} from '../../../middlewares' 15} from '../../../middlewares'
16import { videoCommentThreadsSortValidator } from '../../../middlewares/validators'
17import { 16import {
18 addVideoCommentReplyValidator, 17 addVideoCommentReplyValidator,
19 addVideoCommentThreadValidator, 18 addVideoCommentThreadValidator,
20 listVideoCommentThreadsValidator, 19 listVideoCommentThreadsValidator,
21 listVideoThreadCommentsValidator, 20 listVideoThreadCommentsValidator,
22 removeVideoCommentValidator 21 removeVideoCommentValidator,
23} from '../../../middlewares/validators/video-comments' 22 videoCommentThreadsSortValidator
23} from '../../../middlewares/validators'
24import { VideoModel } from '../../../models/video/video' 24import { VideoModel } from '../../../models/video/video'
25import { VideoCommentModel } from '../../../models/video/video-comment' 25import { VideoCommentModel } from '../../../models/video/video-comment'
26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 15ef8d458..6a73e13d0 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -57,6 +57,7 @@ import { videoCaptionsRouter } from './captions'
57import { videoImportsRouter } from './import' 57import { videoImportsRouter } from './import'
58import { resetSequelizeInstance } from '../../../helpers/database-utils' 58import { resetSequelizeInstance } from '../../../helpers/database-utils'
59import { rename } from 'fs-extra' 59import { rename } from 'fs-extra'
60import { watchingRouter } from './watching'
60 61
61const auditLogger = auditLoggerFactory('videos') 62const auditLogger = auditLoggerFactory('videos')
62const videosRouter = express.Router() 63const videosRouter = express.Router()
@@ -86,6 +87,7 @@ videosRouter.use('/', videoCommentRouter)
86videosRouter.use('/', videoCaptionsRouter) 87videosRouter.use('/', videoCaptionsRouter)
87videosRouter.use('/', videoImportsRouter) 88videosRouter.use('/', videoImportsRouter)
88videosRouter.use('/', ownershipVideoRouter) 89videosRouter.use('/', ownershipVideoRouter)
90videosRouter.use('/', watchingRouter)
89 91
90videosRouter.get('/categories', listVideoCategories) 92videosRouter.get('/categories', listVideoCategories)
91videosRouter.get('/licences', listVideoLicences) 93videosRouter.get('/licences', listVideoLicences)
@@ -119,6 +121,7 @@ videosRouter.get('/:id/description',
119 asyncMiddleware(getVideoDescription) 121 asyncMiddleware(getVideoDescription)
120) 122)
121videosRouter.get('/:id', 123videosRouter.get('/:id',
124 optionalAuthenticate,
122 asyncMiddleware(videosGetValidator), 125 asyncMiddleware(videosGetValidator),
123 getVideo 126 getVideo
124) 127)
@@ -433,7 +436,8 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
433 tagsAllOf: req.query.tagsAllOf, 436 tagsAllOf: req.query.tagsAllOf,
434 nsfw: buildNSFWFilter(res, req.query.nsfw), 437 nsfw: buildNSFWFilter(res, req.query.nsfw),
435 filter: req.query.filter as VideoFilter, 438 filter: req.query.filter as VideoFilter,
436 withFiles: false 439 withFiles: false,
440 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
437 }) 441 })
438 442
439 return res.json(getFormattedObjects(resultList.data, resultList.total)) 443 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts
new file mode 100644
index 000000000..e8876b47a
--- /dev/null
+++ b/server/controllers/api/videos/watching.ts
@@ -0,0 +1,36 @@
1import * as express from 'express'
2import { UserWatchingVideo } from '../../../../shared'
3import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
4import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
5import { UserModel } from '../../../models/account/user'
6
7const watchingRouter = express.Router()
8
9watchingRouter.put('/:videoId/watching',
10 authenticate,
11 asyncMiddleware(videoWatchingValidator),
12 asyncRetryTransactionMiddleware(userWatchVideo)
13)
14
15// ---------------------------------------------------------------------------
16
17export {
18 watchingRouter
19}
20
21// ---------------------------------------------------------------------------
22
23async function userWatchVideo (req: express.Request, res: express.Response) {
24 const user = res.locals.oauth.token.User as UserModel
25
26 const body: UserWatchingVideo = req.body
27 const { id: videoId } = res.locals.video as { id: number }
28
29 await UserVideoHistoryModel.upsert({
30 videoId,
31 userId: user.id,
32 currentTime: body.currentTime
33 })
34
35 return res.type('json').status(204).end()
36}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index b30ad8e8d..ccb9b6029 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,7 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' 2import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants'
3import { THUMBNAILS_SIZE } from '../initializers' 3import { THUMBNAILS_SIZE } from '../initializers'
4import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares' 4import {
5 asyncMiddleware,
6 commonVideosFiltersValidator,
7 setDefaultSort,
8 videoCommentsFeedsValidator,
9 videoFeedsValidator,
10 videosSortValidator
11} from '../middlewares'
5import { VideoModel } from '../models/video/video' 12import { VideoModel } from '../models/video/video'
6import * as Feed from 'pfeed' 13import * as Feed from 'pfeed'
7import { AccountModel } from '../models/account/account' 14import { AccountModel } from '../models/account/account'
@@ -22,6 +29,7 @@ feedsRouter.get('/feeds/videos.:format',
22 videosSortValidator, 29 videosSortValidator,
23 setDefaultSort, 30 setDefaultSort,
24 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), 31 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)),
32 commonVideosFiltersValidator,
25 asyncMiddleware(videoFeedsValidator), 33 asyncMiddleware(videoFeedsValidator),
26 asyncMiddleware(generateVideoFeed) 34 asyncMiddleware(generateVideoFeed)
27) 35)
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 9875c68bd..a13b09ac8 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -3,7 +3,7 @@ import 'express-validator'
3import { values } from 'lodash' 3import { values } from 'lodash'
4import 'multer' 4import 'multer'
5import * as validator from 'validator' 5import * as validator from 'validator'
6import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' 6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
7import { 7import {
8 CONSTRAINTS_FIELDS, 8 CONSTRAINTS_FIELDS,
9 VIDEO_CATEGORIES, 9 VIDEO_CATEGORIES,
@@ -22,6 +22,10 @@ import { fetchVideo, VideoFetchType } from '../video'
22 22
23const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS 23const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
24 24
25function isVideoFilterValid (filter: VideoFilter) {
26 return filter === 'local' || filter === 'all-local'
27}
28
25function isVideoCategoryValid (value: any) { 29function isVideoCategoryValid (value: any) {
26 return value === null || VIDEO_CATEGORIES[ value ] !== undefined 30 return value === null || VIDEO_CATEGORIES[ value ] !== undefined
27} 31}
@@ -154,7 +158,9 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use
154} 158}
155 159
156async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { 160async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') {
157 const video = await fetchVideo(id, fetchType) 161 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
162
163 const video = await fetchVideo(id, fetchType, userId)
158 164
159 if (video === null) { 165 if (video === null) {
160 res.status(404) 166 res.status(404)
@@ -223,5 +229,6 @@ export {
223 isVideoExist, 229 isVideoExist,
224 isVideoImage, 230 isVideoImage,
225 isVideoChannelOfAccountExist, 231 isVideoChannelOfAccountExist,
226 isVideoSupportValid 232 isVideoSupportValid,
233 isVideoFilterValid
227} 234}
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 8a9cee8c5..162fe2244 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -2,7 +2,6 @@ import * as express from 'express'
2import * as multer from 'multer' 2import * as multer from 'multer'
3import { CONFIG, REMOTE_SCHEME } from '../initializers' 3import { CONFIG, REMOTE_SCHEME } from '../initializers'
4import { logger } from './logger' 4import { logger } from './logger'
5import { User } from '../../shared/models/users'
6import { deleteFileAsync, generateRandomString } from './utils' 5import { deleteFileAsync, generateRandomString } from './utils'
7import { extname } from 'path' 6import { extname } from 'path'
8import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
@@ -101,7 +100,7 @@ function createReqFiles (
101} 100}
102 101
103function isUserAbleToSearchRemoteURI (res: express.Response) { 102function isUserAbleToSearchRemoteURI (res: express.Response) {
104 const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined 103 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
105 104
106 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || 105 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
107 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) 106 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 22bc25476..a964abdd4 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,6 +1,6 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { join } from 'path'
3import { VideoResolution } from '../../shared/models/videos' 3import { VideoResolution, getTargetBitrate } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' 4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) {
55 return 0 55 return 0
56} 56}
57 57
58async function getVideoFileBitrate (path: string) {
59 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => {
61 if (err) return rej(err)
62
63 return res(metadata.format.bit_rate)
64 })
65 })
66}
67
58function getDurationFromVideoFile (path: string) { 68function getDurationFromVideoFile (path: string) {
59 return new Promise<number>((res, rej) => { 69 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => { 70 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -138,6 +148,12 @@ function transcode (options: TranscodeOptions) {
138 command = command.withFPS(fps) 148 command = command.withFPS(fps)
139 } 149 }
140 150
151 // Constrained Encoding (VBV)
152 // https://slhck.info/video/2017/03/01/rate-control.html
153 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
154 const targetBitrate = getTargetBitrate(options.resolution, fps, VIDEO_TRANSCODING_FPS)
155 command.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
156
141 command 157 command
142 .on('error', (err, stdout, stderr) => { 158 .on('error', (err, stdout, stderr) => {
143 logger.error('Error in transcoding job.', { stdout, stderr }) 159 logger.error('Error in transcoding job.', { stdout, stderr })
@@ -157,7 +173,8 @@ export {
157 transcode, 173 transcode,
158 getVideoFileFPS, 174 getVideoFileFPS,
159 computeResolutionsToTranscode, 175 computeResolutionsToTranscode,
160 audio 176 audio,
177 getVideoFileBitrate
161} 178}
162 179
163// --------------------------------------------------------------------------- 180// ---------------------------------------------------------------------------
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 6228fec04..39afb4e7b 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -77,6 +77,20 @@ async function getVersion () {
77 return require('../../../package.json').version 77 return require('../../../package.json').version
78} 78}
79 79
80/**
81 * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns
82 * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does
83 * not contain a UUID, returns null.
84 */
85function getUUIDFromFilename (filename: string) {
86 const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
87 const result = filename.match(regex)
88
89 if (!result || Array.isArray(result) === false) return null
90
91 return result[0]
92}
93
80// --------------------------------------------------------------------------- 94// ---------------------------------------------------------------------------
81 95
82export { 96export {
@@ -86,5 +100,6 @@ export {
86 getSecureTorrentName, 100 getSecureTorrentName,
87 getServerActor, 101 getServerActor,
88 getVersion, 102 getVersion,
89 generateVideoTmpPath 103 generateVideoTmpPath,
104 getUUIDFromFilename
90} 105}
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index b1577a6b0..1bd21467d 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -2,8 +2,8 @@ import { VideoModel } from '../models/video/video'
2 2
3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' 3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none'
4 4
5function fetchVideo (id: number | string, fetchType: VideoFetchType) { 5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id) 6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
7 7
8 if (fetchType === 'only-video') return VideoModel.load(id) 8 if (fetchType === 'only-video') return VideoModel.load(id)
9 9
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 1a3b52015..a3e5f5dd2 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -3,7 +3,7 @@ import { dirname, join } from 'path'
3import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' 3import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 4import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 5import { FollowState } from '../../shared/models/actors'
6import { VideoAbuseState, VideoImportState, VideoPrivacy } from '../../shared/models/videos' 6import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
7// Do not use barrels, remain constants as independent as possible 7// Do not use barrels, remain constants as independent as possible
8import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 8import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
@@ -393,7 +393,7 @@ const RATES_LIMIT = {
393} 393}
394 394
395let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour 395let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
396const VIDEO_TRANSCODING_FPS = { 396const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
397 MIN: 10, 397 MIN: 10,
398 AVERAGE: 30, 398 AVERAGE: 30,
399 MAX: 60, 399 MAX: 60,
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 4d57bf8aa..482c03b31 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -28,6 +28,7 @@ import { VideoImportModel } from '../models/video/video-import'
28import { VideoViewModel } from '../models/video/video-views' 28import { VideoViewModel } from '../models/video/video-views'
29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' 29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
31import { UserVideoHistoryModel } from '../models/account/user-video-history'
31 32
32require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 33require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
33 34
@@ -89,7 +90,8 @@ async function initDatabaseModels (silent: boolean) {
89 ScheduleVideoUpdateModel, 90 ScheduleVideoUpdateModel,
90 VideoImportModel, 91 VideoImportModel,
91 VideoViewModel, 92 VideoViewModel,
92 VideoRedundancyModel 93 VideoRedundancyModel,
94 UserVideoHistoryModel
93 ]) 95 ])
94 96
95 // Check extensions exist in the database 97 // Check extensions exist in the database
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 55912341c..db9ce3293 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,7 +1,7 @@
1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' 1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
2import { doRequest } from '../../helpers/requests' 2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import Bluebird = require('bluebird') 4import * as Bluebird from 'bluebird'
5 5
6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { 6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
7 logger.info('Crawling ActivityPub data on %s.', uri) 7 logger.info('Crawling ActivityPub data on %s.', uri)
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index 1463c93fc..adc0a2a15 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -8,7 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' 11import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding'
12 12
13export type VideoFilePayload = { 13export type VideoFilePayload = {
14 videoUUID: string 14 videoUUID: string
@@ -56,7 +56,7 @@ async function processVideoFile (job: Bull.Job) {
56 56
57 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) 57 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video)
58 } else { 58 } else {
59 await optimizeOriginalVideofile(video) 59 await optimizeVideofile(video)
60 60
61 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) 61 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo)
62 } 62 }
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index e4e435659..abd75d512 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -48,6 +48,8 @@ class Redis {
48 ) 48 )
49 } 49 }
50 50
51 /************* Forgot password *************/
52
51 async setResetPasswordVerificationString (userId: number) { 53 async setResetPasswordVerificationString (userId: number) {
52 const generatedString = await generateRandomString(32) 54 const generatedString = await generateRandomString(32)
53 55
@@ -60,6 +62,8 @@ class Redis {
60 return this.getValue(this.generateResetPasswordKey(userId)) 62 return this.getValue(this.generateResetPasswordKey(userId))
61 } 63 }
62 64
65 /************* Email verification *************/
66
63 async setVerifyEmailVerificationString (userId: number) { 67 async setVerifyEmailVerificationString (userId: number) {
64 const generatedString = await generateRandomString(32) 68 const generatedString = await generateRandomString(32)
65 69
@@ -72,16 +76,20 @@ class Redis {
72 return this.getValue(this.generateVerifyEmailKey(userId)) 76 return this.getValue(this.generateVerifyEmailKey(userId))
73 } 77 }
74 78
79 /************* Views per IP *************/
80
75 setIPVideoView (ip: string, videoUUID: string) { 81 setIPVideoView (ip: string, videoUUID: string) {
76 return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) 82 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
77 } 83 }
78 84
79 async isVideoIPViewExists (ip: string, videoUUID: string) { 85 async isVideoIPViewExists (ip: string, videoUUID: string) {
80 return this.exists(this.buildViewKey(ip, videoUUID)) 86 return this.exists(this.generateViewKey(ip, videoUUID))
81 } 87 }
82 88
89 /************* API cache *************/
90
83 async getCachedRoute (req: express.Request) { 91 async getCachedRoute (req: express.Request) {
84 const cached = await this.getObject(this.buildCachedRouteKey(req)) 92 const cached = await this.getObject(this.generateCachedRouteKey(req))
85 93
86 return cached as CachedRoute 94 return cached as CachedRoute
87 } 95 }
@@ -94,9 +102,11 @@ class Redis {
94 (statusCode) ? { statusCode: statusCode.toString() } : null 102 (statusCode) ? { statusCode: statusCode.toString() } : null
95 ) 103 )
96 104
97 return this.setObject(this.buildCachedRouteKey(req), cached, lifetime) 105 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
98 } 106 }
99 107
108 /************* Video views *************/
109
100 addVideoView (videoId: number) { 110 addVideoView (videoId: number) {
101 const keyIncr = this.generateVideoViewKey(videoId) 111 const keyIncr = this.generateVideoViewKey(videoId)
102 const keySet = this.generateVideosViewKey() 112 const keySet = this.generateVideosViewKey()
@@ -131,33 +141,37 @@ class Redis {
131 ]) 141 ])
132 } 142 }
133 143
134 generateVideosViewKey (hour?: number) { 144 /************* Keys generation *************/
145
146 generateCachedRouteKey (req: express.Request) {
147 return req.method + '-' + req.originalUrl
148 }
149
150 private generateVideosViewKey (hour?: number) {
135 if (!hour) hour = new Date().getHours() 151 if (!hour) hour = new Date().getHours()
136 152
137 return `videos-view-h${hour}` 153 return `videos-view-h${hour}`
138 } 154 }
139 155
140 generateVideoViewKey (videoId: number, hour?: number) { 156 private generateVideoViewKey (videoId: number, hour?: number) {
141 if (!hour) hour = new Date().getHours() 157 if (!hour) hour = new Date().getHours()
142 158
143 return `video-view-${videoId}-h${hour}` 159 return `video-view-${videoId}-h${hour}`
144 } 160 }
145 161
146 generateResetPasswordKey (userId: number) { 162 private generateResetPasswordKey (userId: number) {
147 return 'reset-password-' + userId 163 return 'reset-password-' + userId
148 } 164 }
149 165
150 generateVerifyEmailKey (userId: number) { 166 private generateVerifyEmailKey (userId: number) {
151 return 'verify-email-' + userId 167 return 'verify-email-' + userId
152 } 168 }
153 169
154 buildViewKey (ip: string, videoUUID: string) { 170 private generateViewKey (ip: string, videoUUID: string) {
155 return videoUUID + '-' + ip 171 return videoUUID + '-' + ip
156 } 172 }
157 173
158 buildCachedRouteKey (req: express.Request) { 174 /************* Redis helpers *************/
159 return req.method + '-' + req.originalUrl
160 }
161 175
162 private getValue (key: string) { 176 private getValue (key: string) {
163 return new Promise<string>((res, rej) => { 177 return new Promise<string>((res, rej) => {
@@ -197,6 +211,12 @@ class Redis {
197 }) 211 })
198 } 212 }
199 213
214 private deleteFieldInHash (key: string, field: string) {
215 return new Promise<void>((res, rej) => {
216 this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
217 })
218 }
219
200 private setValue (key: string, value: string, expirationMilliseconds: number) { 220 private setValue (key: string, value: string, expirationMilliseconds: number) {
201 return new Promise<void>((res, rej) => { 221 return new Promise<void>((res, rej) => {
202 this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => { 222 this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
@@ -235,6 +255,16 @@ class Redis {
235 }) 255 })
236 } 256 }
237 257
258 private setValueInHash (key: string, field: string, value: string) {
259 return new Promise<void>((res, rej) => {
260 this.client.hset(this.prefix + key, field, value, (err) => {
261 if (err) return rej(err)
262
263 return res()
264 })
265 })
266 }
267
238 private increment (key: string) { 268 private increment (key: string) {
239 return new Promise<number>((res, rej) => { 269 return new Promise<number>((res, rej) => {
240 this.client.incr(this.prefix + key, (err, value) => { 270 this.client.incr(this.prefix + key, (err, value) => {
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index bf3ff78c2..a78de61e5 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -1,5 +1,5 @@
1import { CONFIG } from '../initializers' 1import { CONFIG } from '../initializers'
2import { join, extname } from 'path' 2import { extname, join } from 'path'
3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' 3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils'
4import { copy, remove, rename, stat } from 'fs-extra' 4import { copy, remove, rename, stat } from 'fs-extra'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
@@ -7,10 +7,11 @@ import { VideoResolution } from '../../shared/models/videos'
7import { VideoFileModel } from '../models/video/video-file' 7import { VideoFileModel } from '../models/video/video-file'
8import { VideoModel } from '../models/video/video' 8import { VideoModel } from '../models/video/video'
9 9
10async function optimizeOriginalVideofile (video: VideoModel) { 10async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) {
11 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 11 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
12 const newExtname = '.mp4' 12 const newExtname = '.mp4'
13 const inputVideoFile = video.getOriginalFile() 13
14 const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile()
14 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) 15 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
15 const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) 16 const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
16 17
@@ -124,7 +125,7 @@ async function importVideoFile (video: VideoModel, inputFilePath: string) {
124} 125}
125 126
126export { 127export {
127 optimizeOriginalVideofile, 128 optimizeVideofile,
128 transcodeOriginalVideofile, 129 transcodeOriginalVideofile,
129 importVideoFile 130 importVideoFile
130} 131}
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index 1b44957d3..1e00fc731 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -8,7 +8,7 @@ const lock = new AsyncLock({ timeout: 5000 })
8 8
9function cacheRoute (lifetimeArg: string | number) { 9function cacheRoute (lifetimeArg: string | number) {
10 return async function (req: express.Request, res: express.Response, next: express.NextFunction) { 10 return async function (req: express.Request, res: express.Response, next: express.NextFunction) {
11 const redisKey = Redis.Instance.buildCachedRouteKey(req) 11 const redisKey = Redis.Instance.generateCachedRouteKey(req)
12 12
13 try { 13 try {
14 await lock.acquire(redisKey, async (done) => { 14 await lock.acquire(redisKey, async (done) => {
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 940547a3e..17226614c 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -8,9 +8,5 @@ export * from './sort'
8export * from './users' 8export * from './users'
9export * from './user-subscriptions' 9export * from './user-subscriptions'
10export * from './videos' 10export * from './videos'
11export * from './video-abuses'
12export * from './video-blacklist'
13export * from './video-channels'
14export * from './webfinger' 11export * from './webfinger'
15export * from './search' 12export * from './search'
16export * from './video-imports'
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index 8baf643a5..6a95d6095 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -2,8 +2,7 @@ import * as express from 'express'
2import { areValidationErrors } from './utils' 2import { areValidationErrors } from './utils'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { query } from 'express-validator/check' 4import { query } from 'express-validator/check'
5import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' 5import { isDateValid } from '../../helpers/custom-validators/misc'
6import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
7 6
8const videosSearchValidator = [ 7const videosSearchValidator = [
9 query('search').optional().not().isEmpty().withMessage('Should have a valid search'), 8 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
@@ -35,44 +34,9 @@ const videoChannelsSearchValidator = [
35 } 34 }
36] 35]
37 36
38const commonVideosFiltersValidator = [
39 query('categoryOneOf')
40 .optional()
41 .customSanitizer(toArray)
42 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
43 query('licenceOneOf')
44 .optional()
45 .customSanitizer(toArray)
46 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
47 query('languageOneOf')
48 .optional()
49 .customSanitizer(toArray)
50 .custom(isStringArray).withMessage('Should have a valid one of language array'),
51 query('tagsOneOf')
52 .optional()
53 .customSanitizer(toArray)
54 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
55 query('tagsAllOf')
56 .optional()
57 .customSanitizer(toArray)
58 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
59 query('nsfw')
60 .optional()
61 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
62
63 (req: express.Request, res: express.Response, next: express.NextFunction) => {
64 logger.debug('Checking commons video filters query', { parameters: req.query })
65
66 if (areValidationErrors(req, res)) return
67
68 return next()
69 }
70]
71
72// --------------------------------------------------------------------------- 37// ---------------------------------------------------------------------------
73 38
74export { 39export {
75 commonVideosFiltersValidator,
76 videoChannelsSearchValidator, 40 videoChannelsSearchValidator,
77 videosSearchValidator 41 videosSearchValidator
78} 42}
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
new file mode 100644
index 000000000..294783d85
--- /dev/null
+++ b/server/middlewares/validators/videos/index.ts
@@ -0,0 +1,8 @@
1export * from './video-abuses'
2export * from './video-blacklist'
3export * from './video-captions'
4export * from './video-channels'
5export * from './video-comments'
6export * from './video-imports'
7export * from './video-watch'
8export * from './videos'
diff --git a/server/middlewares/validators/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index f15d55a75..be26ca16a 100644
--- a/server/middlewares/validators/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -1,16 +1,16 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param } from 'express-validator/check' 3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../helpers/custom-validators/videos' 5import { isVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from './utils' 7import { areValidationErrors } from '../utils'
8import { 8import {
9 isVideoAbuseExist, 9 isVideoAbuseExist,
10 isVideoAbuseModerationCommentValid, 10 isVideoAbuseModerationCommentValid,
11 isVideoAbuseReasonValid, 11 isVideoAbuseReasonValid,
12 isVideoAbuseStateValid 12 isVideoAbuseStateValid
13} from '../../helpers/custom-validators/video-abuses' 13} from '../../../helpers/custom-validators/video-abuses'
14 14
15const videoAbuseReportValidator = [ 15const videoAbuseReportValidator = [
16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 95a2b9f17..13da7acff 100644
--- a/server/middlewares/validators/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -1,10 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { areValidationErrors } from './utils' 6import { areValidationErrors } from '../utils'
7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
8 8
9const videosBlacklistRemoveValidator = [ 9const videosBlacklistRemoveValidator = [
10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 51ffd7f3c..63d84fbec 100644
--- a/server/middlewares/validators/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -1,13 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { areValidationErrors } from './utils' 2import { areValidationErrors } from '../utils'
3import { checkUserCanManageVideo, isVideoExist } from '../../helpers/custom-validators/videos' 3import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos'
4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
5import { body, param } from 'express-validator/check' 5import { body, param } from 'express-validator/check'
6import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { CONSTRAINTS_FIELDS } from '../../../initializers'
7import { UserRight } from '../../../shared' 7import { UserRight } from '../../../../shared'
8import { logger } from '../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 9import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
10import { cleanUpReqFiles } from '../../helpers/express-utils' 10import { cleanUpReqFiles } from '../../../helpers/express-utils'
11 11
12const addVideoCaptionValidator = [ 12const addVideoCaptionValidator = [
13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 56a347b39..f039794e0 100644
--- a/server/middlewares/validators/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -1,20 +1,20 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { UserRight } from '../../../shared' 3import { UserRight } from '../../../../shared'
4import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' 4import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts'
5import { 5import {
6 isLocalVideoChannelNameExist, 6 isLocalVideoChannelNameExist,
7 isVideoChannelDescriptionValid, 7 isVideoChannelDescriptionValid,
8 isVideoChannelNameValid, 8 isVideoChannelNameValid,
9 isVideoChannelNameWithHostExist, 9 isVideoChannelNameWithHostExist,
10 isVideoChannelSupportValid 10 isVideoChannelSupportValid
11} from '../../helpers/custom-validators/video-channels' 11} from '../../../helpers/custom-validators/video-channels'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../../helpers/logger'
13import { UserModel } from '../../models/account/user' 13import { UserModel } from '../../../models/account/user'
14import { VideoChannelModel } from '../../models/video/video-channel' 14import { VideoChannelModel } from '../../../models/video/video-channel'
15import { areValidationErrors } from './utils' 15import { areValidationErrors } from '../utils'
16import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' 16import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
17import { ActorModel } from '../../models/activitypub/actor' 17import { ActorModel } from '../../../models/activitypub/actor'
18 18
19const listVideoAccountChannelsValidator = [ 19const listVideoAccountChannelsValidator = [
20 param('accountName').exists().withMessage('Should have a valid account name'), 20 param('accountName').exists().withMessage('Should have a valid account name'),
diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 693852499..348d33082 100644
--- a/server/middlewares/validators/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -1,14 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { UserRight } from '../../../shared' 3import { UserRight } from '../../../../shared'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' 5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
6import { isVideoExist } from '../../helpers/custom-validators/videos' 6import { isVideoExist } from '../../../helpers/custom-validators/videos'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { UserModel } from '../../models/account/user' 8import { UserModel } from '../../../models/account/user'
9import { VideoModel } from '../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
10import { VideoCommentModel } from '../../models/video/video-comment' 10import { VideoCommentModel } from '../../../models/video/video-comment'
11import { areValidationErrors } from './utils' 11import { areValidationErrors } from '../utils'
12 12
13const listVideoCommentThreadsValidator = [ 13const listVideoCommentThreadsValidator = [
14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index b2063b8da..48d20f904 100644
--- a/server/middlewares/validators/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -1,14 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isIdValid } from '../../helpers/custom-validators/misc' 3import { isIdValid } from '../../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from '../utils'
6import { getCommonVideoAttributes } from './videos' 6import { getCommonVideoAttributes } from './videos'
7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../helpers/custom-validators/video-imports' 7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
8import { cleanUpReqFiles } from '../../helpers/express-utils' 8import { cleanUpReqFiles } from '../../../helpers/express-utils'
9import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos' 9import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos'
10import { CONFIG } from '../../initializers/constants' 10import { CONFIG } from '../../../initializers/constants'
11import { CONSTRAINTS_FIELDS } from '../../initializers' 11import { CONSTRAINTS_FIELDS } from '../../../initializers'
12 12
13const videoImportAddValidator = getCommonVideoAttributes().concat([ 13const videoImportAddValidator = getCommonVideoAttributes().concat([
14 body('channelId') 14 body('channelId')
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts
new file mode 100644
index 000000000..bca64662f
--- /dev/null
+++ b/server/middlewares/validators/videos/video-watch.ts
@@ -0,0 +1,28 @@
1import { body, param } from 'express-validator/check'
2import * as express from 'express'
3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { areValidationErrors } from '../utils'
6import { logger } from '../../../helpers/logger'
7
8const videoWatchingValidator = [
9 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
10 body('currentTime')
11 .toInt()
12 .isInt().withMessage('Should have correct current time'),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 logger.debug('Checking videoWatching parameters', { parameters: req.body })
16
17 if (areValidationErrors(req, res)) return
18 if (!await isVideoExist(req.params.videoId, res, 'id')) return
19
20 return next()
21 }
22]
23
24// ---------------------------------------------------------------------------
25
26export {
27 videoWatchingValidator
28}
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos/videos.ts
index 67eabe468..9dc52a134 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -1,16 +1,17 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param, ValidationChain } from 'express-validator/check' 3import { body, param, query, ValidationChain } from 'express-validator/check'
4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared' 4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
5import { 5import {
6 isBooleanValid, 6 isBooleanValid,
7 isDateValid, 7 isDateValid,
8 isIdOrUUIDValid, 8 isIdOrUUIDValid,
9 isIdValid, 9 isIdValid,
10 isUUIDValid, 10 isUUIDValid,
11 toArray,
11 toIntOrNull, 12 toIntOrNull,
12 toValueOrNull 13 toValueOrNull
13} from '../../helpers/custom-validators/misc' 14} from '../../../helpers/custom-validators/misc'
14import { 15import {
15 checkUserCanManageVideo, 16 checkUserCanManageVideo,
16 isScheduleVideoUpdatePrivacyValid, 17 isScheduleVideoUpdatePrivacyValid,
@@ -19,6 +20,7 @@ import {
19 isVideoDescriptionValid, 20 isVideoDescriptionValid,
20 isVideoExist, 21 isVideoExist,
21 isVideoFile, 22 isVideoFile,
23 isVideoFilterValid,
22 isVideoImage, 24 isVideoImage,
23 isVideoLanguageValid, 25 isVideoLanguageValid,
24 isVideoLicenceValid, 26 isVideoLicenceValid,
@@ -27,21 +29,22 @@ import {
27 isVideoRatingTypeValid, 29 isVideoRatingTypeValid,
28 isVideoSupportValid, 30 isVideoSupportValid,
29 isVideoTagsValid 31 isVideoTagsValid
30} from '../../helpers/custom-validators/videos' 32} from '../../../helpers/custom-validators/videos'
31import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' 33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
32import { logger } from '../../helpers/logger' 34import { logger } from '../../../helpers/logger'
33import { CONSTRAINTS_FIELDS } from '../../initializers' 35import { CONSTRAINTS_FIELDS } from '../../../initializers'
34import { VideoShareModel } from '../../models/video/video-share' 36import { VideoShareModel } from '../../../models/video/video-share'
35import { authenticate } from '../oauth' 37import { authenticate } from '../../oauth'
36import { areValidationErrors } from './utils' 38import { areValidationErrors } from '../utils'
37import { cleanUpReqFiles } from '../../helpers/express-utils' 39import { cleanUpReqFiles } from '../../../helpers/express-utils'
38import { VideoModel } from '../../models/video/video' 40import { VideoModel } from '../../../models/video/video'
39import { UserModel } from '../../models/account/user' 41import { UserModel } from '../../../models/account/user'
40import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership' 42import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model' 43import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' 44import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
43import { AccountModel } from '../../models/account/account' 45import { AccountModel } from '../../../models/account/account'
44import { VideoFetchType } from '../../helpers/video' 46import { VideoFetchType } from '../../../helpers/video'
47import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
45 48
46const videosAddValidator = getCommonVideoAttributes().concat([ 49const videosAddValidator = getCommonVideoAttributes().concat([
47 body('videofile') 50 body('videofile')
@@ -69,7 +72,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([
69 if (isAble === false) { 72 if (isAble === false) {
70 res.status(403) 73 res.status(403)
71 .json({ error: 'The user video quota is exceeded with this video.' }) 74 .json({ error: 'The user video quota is exceeded with this video.' })
72 .end()
73 75
74 return cleanUpReqFiles(req) 76 return cleanUpReqFiles(req)
75 } 77 }
@@ -82,7 +84,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([
82 logger.error('Invalid input file in videosAddValidator.', { err }) 84 logger.error('Invalid input file in videosAddValidator.', { err })
83 res.status(400) 85 res.status(400)
84 .json({ error: 'Invalid input file.' }) 86 .json({ error: 'Invalid input file.' })
85 .end()
86 87
87 return cleanUpReqFiles(req) 88 return cleanUpReqFiles(req)
88 } 89 }
@@ -120,7 +121,6 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
120 cleanUpReqFiles(req) 121 cleanUpReqFiles(req)
121 return res.status(409) 122 return res.status(409)
122 .json({ error: 'Cannot set "private" a video that was not private.' }) 123 .json({ error: 'Cannot set "private" a video that was not private.' })
123 .end()
124 } 124 }
125 125
126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@@ -150,7 +150,6 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { 150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
151 return res.status(403) 151 return res.status(403)
152 .json({ error: 'Cannot get this private or blacklisted video.' }) 152 .json({ error: 'Cannot get this private or blacklisted video.' })
153 .end()
154 } 153 }
155 154
156 return next() 155 return next()
@@ -239,8 +238,8 @@ const videosChangeOwnershipValidator = [
239 const nextOwner = await AccountModel.loadLocalByName(req.body.username) 238 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
240 if (!nextOwner) { 239 if (!nextOwner) {
241 res.status(400) 240 res.status(400)
242 .type('json') 241 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
243 .end() 242
244 return 243 return
245 } 244 }
246 res.locals.nextOwner = nextOwner 245 res.locals.nextOwner = nextOwner
@@ -271,7 +270,7 @@ const videosTerminateChangeOwnershipValidator = [
271 } else { 270 } else {
272 res.status(403) 271 res.status(403)
273 .json({ error: 'Ownership already accepted or refused' }) 272 .json({ error: 'Ownership already accepted or refused' })
274 .end() 273
275 return 274 return
276 } 275 }
277 } 276 }
@@ -288,7 +287,7 @@ const videosAcceptChangeOwnershipValidator = [
288 if (isAble === false) { 287 if (isAble === false) {
289 res.status(403) 288 res.status(403)
290 .json({ error: 'The user video quota is exceeded with this video.' }) 289 .json({ error: 'The user video quota is exceeded with this video.' })
291 .end() 290
292 return 291 return
293 } 292 }
294 293
@@ -363,6 +362,51 @@ function getCommonVideoAttributes () {
363 ] as (ValidationChain | express.Handler)[] 362 ] as (ValidationChain | express.Handler)[]
364} 363}
365 364
365const commonVideosFiltersValidator = [
366 query('categoryOneOf')
367 .optional()
368 .customSanitizer(toArray)
369 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
370 query('licenceOneOf')
371 .optional()
372 .customSanitizer(toArray)
373 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
374 query('languageOneOf')
375 .optional()
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid one of language array'),
378 query('tagsOneOf')
379 .optional()
380 .customSanitizer(toArray)
381 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
382 query('tagsAllOf')
383 .optional()
384 .customSanitizer(toArray)
385 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
386 query('nsfw')
387 .optional()
388 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
389 query('filter')
390 .optional()
391 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
392
393 (req: express.Request, res: express.Response, next: express.NextFunction) => {
394 logger.debug('Checking commons video filters query', { parameters: req.query })
395
396 if (areValidationErrors(req, res)) return
397
398 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
399 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
400 res.status(401)
401 .json({ error: 'You are not allowed to see all local videos.' })
402
403 return
404 }
405
406 return next()
407 }
408]
409
366// --------------------------------------------------------------------------- 410// ---------------------------------------------------------------------------
367 411
368export { 412export {
@@ -379,7 +423,9 @@ export {
379 videosTerminateChangeOwnershipValidator, 423 videosTerminateChangeOwnershipValidator,
380 videosAcceptChangeOwnershipValidator, 424 videosAcceptChangeOwnershipValidator,
381 425
382 getCommonVideoAttributes 426 getCommonVideoAttributes,
427
428 commonVideosFiltersValidator
383} 429}
384 430
385// --------------------------------------------------------------------------- 431// ---------------------------------------------------------------------------
@@ -389,7 +435,6 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
389 if (!req.body.scheduleUpdate.updateAt) { 435 if (!req.body.scheduleUpdate.updateAt) {
390 res.status(400) 436 res.status(400)
391 .json({ error: 'Schedule update at is mandatory.' }) 437 .json({ error: 'Schedule update at is mandatory.' })
392 .end()
393 438
394 return true 439 return true
395 } 440 }
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 27c75d886..5a237d733 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -248,7 +248,8 @@ export class AccountModel extends Model<AccountModel> {
248 displayName: this.getDisplayName(), 248 displayName: this.getDisplayName(),
249 description: this.description, 249 description: this.description,
250 createdAt: this.createdAt, 250 createdAt: this.createdAt,
251 updatedAt: this.updatedAt 251 updatedAt: this.updatedAt,
252 userId: this.userId ? this.userId : undefined
252 } 253 }
253 254
254 return Object.assign(actor, account) 255 return Object.assign(actor, account)
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
new file mode 100644
index 000000000..0476cad9d
--- /dev/null
+++ b/server/models/account/user-video-history.ts
@@ -0,0 +1,55 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video'
3import { UserModel } from './user'
4
5@Table({
6 tableName: 'userVideoHistory',
7 indexes: [
8 {
9 fields: [ 'userId', 'videoId' ],
10 unique: true
11 },
12 {
13 fields: [ 'userId' ]
14 },
15 {
16 fields: [ 'videoId' ]
17 }
18 ]
19})
20export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
21 @CreatedAt
22 createdAt: Date
23
24 @UpdatedAt
25 updatedAt: Date
26
27 @AllowNull(false)
28 @IsInt
29 @Column
30 currentTime: number
31
32 @ForeignKey(() => VideoModel)
33 @Column
34 videoId: number
35
36 @BelongsTo(() => VideoModel, {
37 foreignKey: {
38 allowNull: false
39 },
40 onDelete: 'CASCADE'
41 })
42 Video: VideoModel
43
44 @ForeignKey(() => UserModel)
45 @Column
46 userId: number
47
48 @BelongsTo(() => UserModel, {
49 foreignKey: {
50 allowNull: false
51 },
52 onDelete: 'CASCADE'
53 })
54 User: UserModel
55}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index e56b0bf40..39654cfcf 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -181,7 +181,25 @@ export class UserModel extends Model<UserModel> {
181 return this.count() 181 return this.count()
182 } 182 }
183 183
184 static listForApi (start: number, count: number, sort: string) { 184 static listForApi (start: number, count: number, sort: string, search?: string) {
185 let where = undefined
186 if (search) {
187 where = {
188 [Sequelize.Op.or]: [
189 {
190 email: {
191 [Sequelize.Op.iLike]: '%' + search + '%'
192 }
193 },
194 {
195 username: {
196 [ Sequelize.Op.iLike ]: '%' + search + '%'
197 }
198 }
199 ]
200 }
201 }
202
185 const query = { 203 const query = {
186 attributes: { 204 attributes: {
187 include: [ 205 include: [
@@ -204,7 +222,8 @@ export class UserModel extends Model<UserModel> {
204 }, 222 },
205 offset: start, 223 offset: start,
206 limit: count, 224 limit: count,
207 order: getSort(sort) 225 order: getSort(sort),
226 where
208 } 227 }
209 228
210 return UserModel.findAndCountAll(query) 229 return UserModel.findAndCountAll(query)
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 27bb43dae..3373355ef 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -280,7 +280,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
280 return ActorFollowModel.findAll(query) 280 return ActorFollowModel.findAll(query)
281 } 281 }
282 282
283 static listFollowingForApi (id: number, start: number, count: number, sort: string) { 283 static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
284 const query = { 284 const query = {
285 distinct: true, 285 distinct: true,
286 offset: start, 286 offset: start,
@@ -299,7 +299,17 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
299 model: ActorModel, 299 model: ActorModel,
300 as: 'ActorFollowing', 300 as: 'ActorFollowing',
301 required: true, 301 required: true,
302 include: [ ServerModel ] 302 include: [
303 {
304 model: ServerModel,
305 required: true,
306 where: search ? {
307 host: {
308 [Sequelize.Op.iLike]: '%' + search + '%'
309 }
310 } : undefined
311 }
312 ]
303 } 313 }
304 ] 314 ]
305 } 315 }
@@ -313,6 +323,49 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
313 }) 323 })
314 } 324 }
315 325
326 static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) {
327 const query = {
328 distinct: true,
329 offset: start,
330 limit: count,
331 order: getSort(sort),
332 include: [
333 {
334 model: ActorModel,
335 required: true,
336 as: 'ActorFollower',
337 include: [
338 {
339 model: ServerModel,
340 required: true,
341 where: search ? {
342 host: {
343 [ Sequelize.Op.iLike ]: '%' + search + '%'
344 }
345 } : undefined
346 }
347 ]
348 },
349 {
350 model: ActorModel,
351 as: 'ActorFollowing',
352 required: true,
353 where: {
354 id
355 }
356 }
357 ]
358 }
359
360 return ActorFollowModel.findAndCountAll(query)
361 .then(({ rows, count }) => {
362 return {
363 data: rows,
364 total: count
365 }
366 })
367 }
368
316 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { 369 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
317 const query = { 370 const query = {
318 attributes: [], 371 attributes: [],
@@ -370,39 +423,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
370 }) 423 })
371 } 424 }
372 425
373 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
374 const query = {
375 distinct: true,
376 offset: start,
377 limit: count,
378 order: getSort(sort),
379 include: [
380 {
381 model: ActorModel,
382 required: true,
383 as: 'ActorFollower',
384 include: [ ServerModel ]
385 },
386 {
387 model: ActorModel,
388 as: 'ActorFollowing',
389 required: true,
390 where: {
391 id
392 }
393 }
394 ]
395 }
396
397 return ActorFollowModel.findAndCountAll(query)
398 .then(({ rows, count }) => {
399 return {
400 data: rows,
401 total: count
402 }
403 })
404 }
405
406 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 426 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
407 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) 427 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
408 } 428 }
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index f23dde9b8..905e84449 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -10,6 +10,7 @@ import {
10 getVideoLikesActivityPubUrl, 10 getVideoLikesActivityPubUrl,
11 getVideoSharesActivityPubUrl 11 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 12} from '../../lib/activitypub'
13import { isArray } from '../../helpers/custom-validators/misc'
13 14
14export type VideoFormattingJSONOptions = { 15export type VideoFormattingJSONOptions = {
15 completeDescription?: boolean 16 completeDescription?: boolean
@@ -24,6 +25,8 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
24 const formattedAccount = video.VideoChannel.Account.toFormattedJSON() 25 const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
25 const formattedVideoChannel = video.VideoChannel.toFormattedJSON() 26 const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
26 27
28 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
29
27 const videoObject: Video = { 30 const videoObject: Video = {
28 id: video.id, 31 id: video.id,
29 uuid: video.uuid, 32 uuid: video.uuid,
@@ -74,7 +77,11 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
74 url: formattedVideoChannel.url, 77 url: formattedVideoChannel.url,
75 host: formattedVideoChannel.host, 78 host: formattedVideoChannel.host,
76 avatar: formattedVideoChannel.avatar 79 avatar: formattedVideoChannel.avatar
77 } 80 },
81
82 userHistory: userHistory ? {
83 currentTime: userHistory.currentTime
84 } : undefined
78 } 85 }
79 86
80 if (options) { 87 if (options) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c89c16bf..4f3f75613 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -92,6 +92,7 @@ import {
92 videoModelToFormattedJSON 92 videoModelToFormattedJSON
93} from './video-format-utils' 93} from './video-format-utils'
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history'
95 96
96// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
97const indexes: Sequelize.DefineIndexesOptions[] = [ 98const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -127,7 +128,8 @@ export enum ScopeNames {
127 WITH_TAGS = 'WITH_TAGS', 128 WITH_TAGS = 'WITH_TAGS',
128 WITH_FILES = 'WITH_FILES', 129 WITH_FILES = 'WITH_FILES',
129 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 130 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
130 WITH_BLACKLISTED = 'WITH_BLACKLISTED' 131 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
132 WITH_USER_HISTORY = 'WITH_USER_HISTORY'
131} 133}
132 134
133type ForAPIOptions = { 135type ForAPIOptions = {
@@ -233,7 +235,14 @@ type AvailableForListIDsOptions = {
233 ) 235 )
234 } 236 }
235 ] 237 ]
236 }, 238 }
239 },
240 include: []
241 }
242
243 // Only list public/published videos
244 if (!options.filter || options.filter !== 'all-local') {
245 const privacyWhere = {
237 // Always list public videos 246 // Always list public videos
238 privacy: VideoPrivacy.PUBLIC, 247 privacy: VideoPrivacy.PUBLIC,
239 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 248 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
@@ -248,8 +257,9 @@ type AvailableForListIDsOptions = {
248 } 257 }
249 } 258 }
250 ] 259 ]
251 }, 260 }
252 include: [] 261
262 Object.assign(query.where, privacyWhere)
253 } 263 }
254 264
255 if (options.filter || options.accountId || options.videoChannelId) { 265 if (options.filter || options.accountId || options.videoChannelId) {
@@ -464,6 +474,8 @@ type AvailableForListIDsOptions = {
464 include: [ 474 include: [
465 { 475 {
466 model: () => VideoFileModel.unscoped(), 476 model: () => VideoFileModel.unscoped(),
477 // FIXME: typings
478 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
467 required: false, 479 required: false,
468 include: [ 480 include: [
469 { 481 {
@@ -482,6 +494,20 @@ type AvailableForListIDsOptions = {
482 required: false 494 required: false
483 } 495 }
484 ] 496 ]
497 },
498 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
499 return {
500 include: [
501 {
502 attributes: [ 'currentTime' ],
503 model: UserVideoHistoryModel.unscoped(),
504 required: false,
505 where: {
506 userId
507 }
508 }
509 ]
510 }
485 } 511 }
486}) 512})
487@Table({ 513@Table({
@@ -672,11 +698,19 @@ export class VideoModel extends Model<VideoModel> {
672 name: 'videoId', 698 name: 'videoId',
673 allowNull: false 699 allowNull: false
674 }, 700 },
675 onDelete: 'cascade', 701 onDelete: 'cascade'
676 hooks: true
677 }) 702 })
678 VideoViews: VideoViewModel[] 703 VideoViews: VideoViewModel[]
679 704
705 @HasMany(() => UserVideoHistoryModel, {
706 foreignKey: {
707 name: 'videoId',
708 allowNull: false
709 },
710 onDelete: 'cascade'
711 })
712 UserVideoHistories: UserVideoHistoryModel[]
713
680 @HasOne(() => ScheduleVideoUpdateModel, { 714 @HasOne(() => ScheduleVideoUpdateModel, {
681 foreignKey: { 715 foreignKey: {
682 name: 'videoId', 716 name: 'videoId',
@@ -762,6 +796,16 @@ export class VideoModel extends Model<VideoModel> {
762 return VideoModel.scope(ScopeNames.WITH_FILES).findAll() 796 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
763 } 797 }
764 798
799 static listLocal () {
800 const query = {
801 where: {
802 remote: false
803 }
804 }
805
806 return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query)
807 }
808
765 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 809 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
766 function getRawQuery (select: string) { 810 function getRawQuery (select: string) {
767 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + 811 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
@@ -930,8 +974,13 @@ export class VideoModel extends Model<VideoModel> {
930 accountId?: number, 974 accountId?: number,
931 videoChannelId?: number, 975 videoChannelId?: number,
932 actorId?: number 976 actorId?: number
933 trendingDays?: number 977 trendingDays?: number,
978 userId?: number
934 }, countVideos = true) { 979 }, countVideos = true) {
980 if (options.filter && options.filter === 'all-local' && !options.userId) {
981 throw new Error('Try to filter all-local but no userId is provided')
982 }
983
935 const query: IFindOptions<VideoModel> = { 984 const query: IFindOptions<VideoModel> = {
936 offset: options.start, 985 offset: options.start,
937 limit: options.count, 986 limit: options.count,
@@ -961,6 +1010,7 @@ export class VideoModel extends Model<VideoModel> {
961 accountId: options.accountId, 1010 accountId: options.accountId,
962 videoChannelId: options.videoChannelId, 1011 videoChannelId: options.videoChannelId,
963 includeLocalVideos: options.includeLocalVideos, 1012 includeLocalVideos: options.includeLocalVideos,
1013 userId: options.userId,
964 trendingDays 1014 trendingDays
965 } 1015 }
966 1016
@@ -983,6 +1033,8 @@ export class VideoModel extends Model<VideoModel> {
983 tagsAllOf?: string[] 1033 tagsAllOf?: string[]
984 durationMin?: number // seconds 1034 durationMin?: number // seconds
985 durationMax?: number // seconds 1035 durationMax?: number // seconds
1036 userId?: number,
1037 filter?: VideoFilter
986 }) { 1038 }) {
987 const whereAnd = [] 1039 const whereAnd = []
988 1040
@@ -1058,7 +1110,9 @@ export class VideoModel extends Model<VideoModel> {
1058 licenceOneOf: options.licenceOneOf, 1110 licenceOneOf: options.licenceOneOf,
1059 languageOneOf: options.languageOneOf, 1111 languageOneOf: options.languageOneOf,
1060 tagsOneOf: options.tagsOneOf, 1112 tagsOneOf: options.tagsOneOf,
1061 tagsAllOf: options.tagsAllOf 1113 tagsAllOf: options.tagsAllOf,
1114 userId: options.userId,
1115 filter: options.filter
1062 } 1116 }
1063 1117
1064 return VideoModel.getAvailableForApi(query, queryOptions) 1118 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1125,7 +1179,7 @@ export class VideoModel extends Model<VideoModel> {
1125 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1179 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1126 } 1180 }
1127 1181
1128 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { 1182 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1129 const where = VideoModel.buildWhereIdOrUUID(id) 1183 const where = VideoModel.buildWhereIdOrUUID(id)
1130 1184
1131 const options = { 1185 const options = {
@@ -1134,14 +1188,20 @@ export class VideoModel extends Model<VideoModel> {
1134 transaction: t 1188 transaction: t
1135 } 1189 }
1136 1190
1191 const scopes = [
1192 ScopeNames.WITH_TAGS,
1193 ScopeNames.WITH_BLACKLISTED,
1194 ScopeNames.WITH_FILES,
1195 ScopeNames.WITH_ACCOUNT_DETAILS,
1196 ScopeNames.WITH_SCHEDULED_UPDATE
1197 ]
1198
1199 if (userId) {
1200 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1201 }
1202
1137 return VideoModel 1203 return VideoModel
1138 .scope([ 1204 .scope(scopes)
1139 ScopeNames.WITH_TAGS,
1140 ScopeNames.WITH_BLACKLISTED,
1141 ScopeNames.WITH_FILES,
1142 ScopeNames.WITH_ACCOUNT_DETAILS,
1143 ScopeNames.WITH_SCHEDULED_UPDATE
1144 ])
1145 .findOne(options) 1205 .findOne(options)
1146 } 1206 }
1147 1207
@@ -1216,7 +1276,7 @@ export class VideoModel extends Model<VideoModel> {
1216 } 1276 }
1217 1277
1218 private static buildActorWhereWithFilter (filter?: VideoFilter) { 1278 private static buildActorWhereWithFilter (filter?: VideoFilter) {
1219 if (filter && filter === 'local') { 1279 if (filter && (filter === 'local' || filter === 'all-local')) {
1220 return { 1280 return {
1221 serverId: null 1281 serverId: null
1222 } 1282 }
@@ -1225,7 +1285,11 @@ export class VideoModel extends Model<VideoModel> {
1225 return {} 1285 return {}
1226 } 1286 }
1227 1287
1228 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { 1288 private static async getAvailableForApi (
1289 query: IFindOptions<VideoModel>,
1290 options: AvailableForListIDsOptions & { userId?: number},
1291 countVideos = true
1292 ) {
1229 const idsScope = { 1293 const idsScope = {
1230 method: [ 1294 method: [
1231 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1295 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1249,8 +1313,15 @@ export class VideoModel extends Model<VideoModel> {
1249 1313
1250 if (ids.length === 0) return { data: [], total: count } 1314 if (ids.length === 0) return { data: [], total: count }
1251 1315
1252 const apiScope = { 1316 // FIXME: typings
1253 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] 1317 const apiScope: any[] = [
1318 {
1319 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1320 }
1321 ]
1322
1323 if (options.userId) {
1324 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] })
1254 } 1325 }
1255 1326
1256 const secondQuery = { 1327 const secondQuery = {
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 44460a167..bfc550ae5 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -15,3 +15,5 @@ import './video-channels'
15import './video-comments' 15import './video-comments'
16import './video-imports' 16import './video-imports'
17import './videos' 17import './videos'
18import './videos-filter'
19import './videos-history'
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts
new file mode 100644
index 000000000..784cd8ba1
--- /dev/null
+++ b/server/tests/api/check-params/videos-filter.ts
@@ -0,0 +1,127 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 createUser,
7 flushTests,
8 killallServers,
9 makeGetRequest,
10 runServer,
11 ServerInfo,
12 setAccessTokensToServers,
13 userLogin
14} from '../../utils'
15import { UserRole } from '../../../../shared/models/users'
16
17const expect = chai.expect
18
19async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
20 const paths = [
21 '/api/v1/video-channels/root_channel/videos',
22 '/api/v1/accounts/root/videos',
23 '/api/v1/videos',
24 '/api/v1/search/videos'
25 ]
26
27 for (const path of paths) {
28 await makeGetRequest({
29 url: server.url,
30 path,
31 token,
32 query: {
33 filter
34 },
35 statusCodeExpected
36 })
37 }
38}
39
40describe('Test videos filters', function () {
41 let server: ServerInfo
42 let userAccessToken: string
43 let moderatorAccessToken: string
44
45 // ---------------------------------------------------------------
46
47 before(async function () {
48 this.timeout(30000)
49
50 await flushTests()
51
52 server = await runServer(1)
53
54 await setAccessTokensToServers([ server ])
55
56 const user = { username: 'user1', password: 'my super password' }
57 await createUser(server.url, server.accessToken, user.username, user.password)
58 userAccessToken = await userLogin(server, user)
59
60 const moderator = { username: 'moderator', password: 'my super password' }
61 await createUser(
62 server.url,
63 server.accessToken,
64 moderator.username,
65 moderator.password,
66 undefined,
67 undefined,
68 UserRole.MODERATOR
69 )
70 moderatorAccessToken = await userLogin(server, moderator)
71 })
72
73 describe('When setting a video filter', function () {
74
75 it('Should fail with a bad filter', async function () {
76 await testEndpoints(server, server.accessToken, 'bad-filter', 400)
77 })
78
79 it('Should succeed with a good filter', async function () {
80 await testEndpoints(server, server.accessToken,'local', 200)
81 })
82
83 it('Should fail to list all-local with a simple user', async function () {
84 await testEndpoints(server, userAccessToken, 'all-local', 401)
85 })
86
87 it('Should succeed to list all-local with a moderator', async function () {
88 await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
89 })
90
91 it('Should succeed to list all-local with an admin', async function () {
92 await testEndpoints(server, server.accessToken, 'all-local', 200)
93 })
94
95 // Because we cannot authenticate the user on the RSS endpoint
96 it('Should fail on the feeds endpoint with the all-local filter', async function () {
97 await makeGetRequest({
98 url: server.url,
99 path: '/feeds/videos.json',
100 statusCodeExpected: 401,
101 query: {
102 filter: 'all-local'
103 }
104 })
105 })
106
107 it('Should succed on the feeds endpoint with the local filter', async function () {
108 await makeGetRequest({
109 url: server.url,
110 path: '/feeds/videos.json',
111 statusCodeExpected: 200,
112 query: {
113 filter: 'local'
114 }
115 })
116 })
117 })
118
119 after(async function () {
120 killallServers([ server ])
121
122 // Keep the logs if the test failed
123 if (this['ok']) {
124 await flushTests()
125 }
126 })
127})
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
new file mode 100644
index 000000000..808c3b616
--- /dev/null
+++ b/server/tests/api/check-params/videos-history.ts
@@ -0,0 +1,79 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 killallServers,
8 makePostBodyRequest,
9 makePutBodyRequest,
10 runServer,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo
14} from '../../utils'
15
16const expect = chai.expect
17
18describe('Test videos history API validator', function () {
19 let path: string
20 let server: ServerInfo
21
22 // ---------------------------------------------------------------
23
24 before(async function () {
25 this.timeout(30000)
26
27 await flushTests()
28
29 server = await runServer(1)
30
31 await setAccessTokensToServers([ server ])
32
33 const res = await uploadVideo(server.url, server.accessToken, {})
34 const videoUUID = res.body.video.uuid
35
36 path = '/api/v1/videos/' + videoUUID + '/watching'
37 })
38
39 describe('When notifying a user is watching a video', function () {
40
41 it('Should fail with an unauthenticated user', async function () {
42 const fields = { currentTime: 5 }
43 await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 })
44 })
45
46 it('Should fail with an incorrect video id', async function () {
47 const fields = { currentTime: 5 }
48 const path = '/api/v1/videos/blabla/watching'
49 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
50 })
51
52 it('Should fail with an unknown video', async function () {
53 const fields = { currentTime: 5 }
54 const path = '/api/v1/videos/d91fff41-c24d-4508-8e13-3bd5902c3b02/watching'
55
56 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 404 })
57 })
58
59 it('Should fail with a bad current time', async function () {
60 const fields = { currentTime: 'hello' }
61 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
62 })
63
64 it('Should succeed with the correct parameters', async function () {
65 const fields = { currentTime: 5 }
66
67 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 })
68 })
69 })
70
71 after(async function () {
72 killallServers([ server ])
73
74 // Keep the logs if the test failed
75 if (this['ok']) {
76 await flushTests()
77 }
78 })
79})
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts
index 310c291bf..e80e93e7f 100644
--- a/server/tests/api/server/follows.ts
+++ b/server/tests/api/server/follows.ts
@@ -93,7 +93,26 @@ describe('Test follows', function () {
93 expect(server3Follow.state).to.equal('accepted') 93 expect(server3Follow.state).to.equal('accepted')
94 }) 94 })
95 95
96 it('Should have 0 followings on server 1 and 2', async function () { 96 it('Should search followings on server 1', async function () {
97 {
98 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', ':9002')
99 const follows = res.body.data
100
101 expect(res.body.total).to.equal(1)
102 expect(follows.length).to.equal(1)
103 expect(follows[ 0 ].following.host).to.equal('localhost:9002')
104 }
105
106 {
107 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', 'bla')
108 const follows = res.body.data
109
110 expect(res.body.total).to.equal(0)
111 expect(follows.length).to.equal(0)
112 }
113 })
114
115 it('Should have 0 followings on server 2 and 3', async function () {
97 for (const server of [ servers[1], servers[2] ]) { 116 for (const server of [ servers[1], servers[2] ]) {
98 const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt') 117 const res = await getFollowingListPaginationAndSort(server.url, 0, 5, 'createdAt')
99 const follows = res.body.data 118 const follows = res.body.data
@@ -116,6 +135,25 @@ describe('Test follows', function () {
116 } 135 }
117 }) 136 })
118 137
138 it('Should search followers on server 2', async function () {
139 {
140 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', '9001')
141 const follows = res.body.data
142
143 expect(res.body.total).to.equal(1)
144 expect(follows.length).to.equal(1)
145 expect(follows[ 0 ].following.host).to.equal('localhost:9003')
146 }
147
148 {
149 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', 'bla')
150 const follows = res.body.data
151
152 expect(res.body.total).to.equal(0)
153 expect(follows.length).to.equal(0)
154 }
155 })
156
119 it('Should have 0 followers on server 1', async function () { 157 it('Should have 0 followers on server 1', async function () {
120 const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt') 158 const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
121 const follows = res.body.data 159 const follows = res.body.data
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts
index b67072851..d8699db17 100644
--- a/server/tests/api/users/users-multiple-servers.ts
+++ b/server/tests/api/users/users-multiple-servers.ts
@@ -148,6 +148,12 @@ describe('Test users with multiple servers', function () {
148 expect(rootServer1Get.displayName).to.equal('my super display name') 148 expect(rootServer1Get.displayName).to.equal('my super display name')
149 expect(rootServer1Get.description).to.equal('my super description updated') 149 expect(rootServer1Get.description).to.equal('my super description updated')
150 150
151 if (server.serverNumber === 1) {
152 expect(rootServer1Get.userId).to.be.a('number')
153 } else {
154 expect(rootServer1Get.userId).to.be.undefined
155 }
156
151 await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png') 157 await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png')
152 } 158 }
153 }) 159 })
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 8b9c6b455..513bca8a0 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -180,7 +180,7 @@ describe('Test users', function () {
180 it('Should be able to upload a video again') 180 it('Should be able to upload a video again')
181 181
182 it('Should be able to create a new user', async function () { 182 it('Should be able to create a new user', async function () {
183 await createUser(server.url, accessToken, user.username,user.password, 2 * 1024 * 1024) 183 await createUser(server.url, accessToken, user.username, user.password, 2 * 1024 * 1024)
184 }) 184 })
185 185
186 it('Should be able to login with this user', async function () { 186 it('Should be able to login with this user', async function () {
@@ -322,6 +322,40 @@ describe('Test users', function () {
322 expect(users[ 1 ].nsfwPolicy).to.equal('display') 322 expect(users[ 1 ].nsfwPolicy).to.equal('display')
323 }) 323 })
324 324
325 it('Should search user by username', async function () {
326 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'oot')
327 const users = res.body.data as User[]
328
329 expect(res.body.total).to.equal(1)
330 expect(users.length).to.equal(1)
331
332 expect(users[ 0 ].username).to.equal('root')
333 })
334
335 it('Should search user by email', async function () {
336 {
337 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'r_1@exam')
338 const users = res.body.data as User[]
339
340 expect(res.body.total).to.equal(1)
341 expect(users.length).to.equal(1)
342
343 expect(users[ 0 ].username).to.equal('user_1')
344 expect(users[ 0 ].email).to.equal('user_1@example.com')
345 }
346
347 {
348 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'example')
349 const users = res.body.data as User[]
350
351 expect(res.body.total).to.equal(2)
352 expect(users.length).to.equal(2)
353
354 expect(users[ 0 ].username).to.equal('root')
355 expect(users[ 1 ].username).to.equal('user_1')
356 }
357 })
358
325 it('Should update my password', async function () { 359 it('Should update my password', async function () {
326 await updateMyUser({ 360 await updateMyUser({
327 url: server.url, 361 url: server.url,
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index bf58f9c79..9bdb78491 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -14,4 +14,6 @@ import './video-nsfw'
14import './video-privacy' 14import './video-privacy'
15import './video-schedule-update' 15import './video-schedule-update'
16import './video-transcoder' 16import './video-transcoder'
17import './videos-filter'
18import './videos-history'
17import './videos-overview' 19import './videos-overview'
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 0f83d4d57..0ce5197ea 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -4,8 +4,8 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import * as ffmpeg from 'fluent-ffmpeg' 6import * as ffmpeg from 'fluent-ffmpeg'
7import { VideoDetails, VideoState } from '../../../../shared/models/videos' 7import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
8import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils' 8import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
9import { 9import {
10 buildAbsoluteFixturePath, 10 buildAbsoluteFixturePath,
11 doubleFollow, 11 doubleFollow,
@@ -22,6 +22,8 @@ import {
22} from '../../utils' 22} from '../../utils'
23import { join } from 'path' 23import { join } from 'path'
24import { waitJobs } from '../../utils/server/jobs' 24import { waitJobs } from '../../utils/server/jobs'
25import { pathExists } from 'fs-extra'
26import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
25 27
26const expect = chai.expect 28const expect = chai.expect
27 29
@@ -228,7 +230,7 @@ describe('Test video transcoding', function () {
228 } 230 }
229 }) 231 })
230 232
231 it('Should wait transcoding before publishing the video', async function () { 233 it('Should wait for transcoding before publishing the video', async function () {
232 this.timeout(80000) 234 this.timeout(80000)
233 235
234 { 236 {
@@ -281,6 +283,61 @@ describe('Test video transcoding', function () {
281 } 283 }
282 }) 284 })
283 285
286 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
287 it('Should respect maximum bitrate values', async function () {
288 this.timeout(160000)
289
290 {
291 const exists = await pathExists(tempFixturePath)
292 if (!exists) {
293
294 // Generate a random, high bitrate video on the fly, so we don't have to include
295 // a large file in the repo. The video needs to have a certain minimum length so
296 // that FFmpeg properly applies bitrate limits.
297 // https://stackoverflow.com/a/15795112
298 await new Promise<void>(async (res, rej) => {
299 ffmpeg()
300 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
301 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
302 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
303 .output(tempFixturePath)
304 .on('error', rej)
305 .on('end', res)
306 .run()
307 })
308 }
309
310 const bitrate = await getVideoFileBitrate(tempFixturePath)
311 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS))
312 }
313
314 const videoAttributes = {
315 name: 'high bitrate video',
316 description: 'high bitrate video',
317 fixture: tempFixturePath
318 }
319
320 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
321
322 await waitJobs(servers)
323
324 for (const server of servers) {
325 const res = await getVideosList(server.url)
326
327 const video = res.body.data.find(v => v.name === videoAttributes.name)
328
329 for (const resolution of ['240', '360', '480', '720', '1080']) {
330 const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4')
331 const bitrate = await getVideoFileBitrate(path)
332 const fps = await getVideoFileFPS(path)
333 const resolution2 = await getVideoFileResolution(path)
334
335 expect(resolution2.videoFileResolution.toString()).to.equal(resolution)
336 expect(bitrate).to.be.below(getMaxBitrate(resolution2.videoFileResolution, fps, VIDEO_TRANSCODING_FPS))
337 }
338 }
339 })
340
284 after(async function () { 341 after(async function () {
285 killallServers(servers) 342 killallServers(servers)
286 }) 343 })
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts
new file mode 100644
index 000000000..a7588129f
--- /dev/null
+++ b/server/tests/api/videos/videos-filter.ts
@@ -0,0 +1,130 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 createUser,
7 doubleFollow,
8 flushAndRunMultipleServers,
9 flushTests,
10 killallServers,
11 makeGetRequest,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo,
15 userLogin
16} from '../../utils'
17import { Video, VideoPrivacy } from '../../../../shared/models/videos'
18import { UserRole } from '../../../../shared/models/users'
19
20const expect = chai.expect
21
22async function getVideosNames (server: ServerInfo, token: string, filter: string, statusCodeExpected = 200) {
23 const paths = [
24 '/api/v1/video-channels/root_channel/videos',
25 '/api/v1/accounts/root/videos',
26 '/api/v1/videos',
27 '/api/v1/search/videos'
28 ]
29
30 const videosResults: Video[][] = []
31
32 for (const path of paths) {
33 const res = await makeGetRequest({
34 url: server.url,
35 path,
36 token,
37 query: {
38 sort: 'createdAt',
39 filter
40 },
41 statusCodeExpected
42 })
43
44 videosResults.push(res.body.data.map(v => v.name))
45 }
46
47 return videosResults
48}
49
50describe('Test videos filter validator', function () {
51 let servers: ServerInfo[]
52
53 // ---------------------------------------------------------------
54
55 before(async function () {
56 this.timeout(120000)
57
58 await flushTests()
59
60 servers = await flushAndRunMultipleServers(2)
61
62 await setAccessTokensToServers(servers)
63
64 for (const server of servers) {
65 const moderator = { username: 'moderator', password: 'my super password' }
66 await createUser(
67 server.url,
68 server.accessToken,
69 moderator.username,
70 moderator.password,
71 undefined,
72 undefined,
73 UserRole.MODERATOR
74 )
75 server['moderatorAccessToken'] = await userLogin(server, moderator)
76
77 await uploadVideo(server.url, server.accessToken, { name: 'public ' + server.serverNumber })
78
79 {
80 const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
81 await uploadVideo(server.url, server.accessToken, attributes)
82 }
83
84 {
85 const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
86 await uploadVideo(server.url, server.accessToken, attributes)
87 }
88 }
89
90 await doubleFollow(servers[0], servers[1])
91 })
92
93 describe('Check videos filter', function () {
94
95 it('Should display local videos', async function () {
96 for (const server of servers) {
97 const namesResults = await getVideosNames(server, server.accessToken, 'local')
98 for (const names of namesResults) {
99 expect(names).to.have.lengthOf(1)
100 expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
101 }
102 }
103 })
104
105 it('Should display all local videos by the admin or the moderator', async function () {
106 for (const server of servers) {
107 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
108
109 const namesResults = await getVideosNames(server, token, 'all-local')
110 for (const names of namesResults) {
111 expect(names).to.have.lengthOf(3)
112
113 expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
114 expect(names[ 1 ]).to.equal('unlisted ' + server.serverNumber)
115 expect(names[ 2 ]).to.equal('private ' + server.serverNumber)
116 }
117 }
118 }
119 })
120 })
121
122 after(async function () {
123 killallServers(servers)
124
125 // Keep the logs if the test failed
126 if (this['ok']) {
127 await flushTests()
128 }
129 })
130})
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
new file mode 100644
index 000000000..6d289b288
--- /dev/null
+++ b/server/tests/api/videos/videos-history.ts
@@ -0,0 +1,128 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 getVideosListWithToken,
8 getVideoWithToken,
9 killallServers, makePutBodyRequest,
10 runServer, searchVideoWithToken,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo
14} from '../../utils'
15import { Video, VideoDetails } from '../../../../shared/models/videos'
16import { userWatchVideo } from '../../utils/videos/video-history'
17
18const expect = chai.expect
19
20describe('Test videos history', function () {
21 let server: ServerInfo = null
22 let video1UUID: string
23 let video2UUID: string
24 let video3UUID: string
25
26 before(async function () {
27 this.timeout(30000)
28
29 await flushTests()
30
31 server = await runServer(1)
32
33 await setAccessTokensToServers([ server ])
34
35 {
36 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
37 video1UUID = res.body.video.uuid
38 }
39
40 {
41 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
42 video2UUID = res.body.video.uuid
43 }
44
45 {
46 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
47 video3UUID = res.body.video.uuid
48 }
49 })
50
51 it('Should get videos, without watching history', async function () {
52 const res = await getVideosListWithToken(server.url, server.accessToken)
53 const videos: Video[] = res.body.data
54
55 for (const video of videos) {
56 const resDetail = await getVideoWithToken(server.url, server.accessToken, video.id)
57 const videoDetails: VideoDetails = resDetail.body
58
59 expect(video.userHistory).to.be.undefined
60 expect(videoDetails.userHistory).to.be.undefined
61 }
62 })
63
64 it('Should watch the first and second video', async function () {
65 await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
66 await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
67 })
68
69 it('Should return the correct history when listing, searching and getting videos', async function () {
70 const videosOfVideos: Video[][] = []
71
72 {
73 const res = await getVideosListWithToken(server.url, server.accessToken)
74 videosOfVideos.push(res.body.data)
75 }
76
77 {
78 const res = await searchVideoWithToken(server.url, 'video', server.accessToken)
79 videosOfVideos.push(res.body.data)
80 }
81
82 for (const videos of videosOfVideos) {
83 const video1 = videos.find(v => v.uuid === video1UUID)
84 const video2 = videos.find(v => v.uuid === video2UUID)
85 const video3 = videos.find(v => v.uuid === video3UUID)
86
87 expect(video1.userHistory).to.not.be.undefined
88 expect(video1.userHistory.currentTime).to.equal(3)
89
90 expect(video2.userHistory).to.not.be.undefined
91 expect(video2.userHistory.currentTime).to.equal(8)
92
93 expect(video3.userHistory).to.be.undefined
94 }
95
96 {
97 const resDetail = await getVideoWithToken(server.url, server.accessToken, video1UUID)
98 const videoDetails: VideoDetails = resDetail.body
99
100 expect(videoDetails.userHistory).to.not.be.undefined
101 expect(videoDetails.userHistory.currentTime).to.equal(3)
102 }
103
104 {
105 const resDetail = await getVideoWithToken(server.url, server.accessToken, video2UUID)
106 const videoDetails: VideoDetails = resDetail.body
107
108 expect(videoDetails.userHistory).to.not.be.undefined
109 expect(videoDetails.userHistory.currentTime).to.equal(8)
110 }
111
112 {
113 const resDetail = await getVideoWithToken(server.url, server.accessToken, video3UUID)
114 const videoDetails: VideoDetails = resDetail.body
115
116 expect(videoDetails.userHistory).to.be.undefined
117 }
118 })
119
120 after(async function () {
121 killallServers([ server ])
122
123 // Keep the logs if the test failed
124 if (this['ok']) {
125 await flushTests()
126 }
127 })
128})
diff --git a/server/tests/utils/miscs/miscs.ts b/server/tests/utils/miscs/miscs.ts
index b2f80e9b1..d20fa96b8 100644
--- a/server/tests/utils/miscs/miscs.ts
+++ b/server/tests/utils/miscs/miscs.ts
@@ -51,11 +51,13 @@ async function testImage (url: string, imageName: string, imagePath: string, ext
51 expect(data.length).to.be.below(maxLength) 51 expect(data.length).to.be.below(maxLength)
52} 52}
53 53
54function buildAbsoluteFixturePath (path: string) { 54function buildAbsoluteFixturePath (path: string, customTravisPath = false) {
55 if (isAbsolute(path)) { 55 if (isAbsolute(path)) {
56 return path 56 return path
57 } 57 }
58 58
59 if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path)
60
59 return join(__dirname, '..', '..', 'fixtures', path) 61 return join(__dirname, '..', '..', 'fixtures', path)
60} 62}
61 63
diff --git a/server/tests/utils/server/follows.ts b/server/tests/utils/server/follows.ts
index 8a65a958b..7741757a6 100644
--- a/server/tests/utils/server/follows.ts
+++ b/server/tests/utils/server/follows.ts
@@ -2,7 +2,7 @@ import * as request from 'supertest'
2import { ServerInfo } from './servers' 2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs' 3import { waitJobs } from './jobs'
4 4
5function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string) { 5function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
6 const path = '/api/v1/server/followers' 6 const path = '/api/v1/server/followers'
7 7
8 return request(url) 8 return request(url)
@@ -10,12 +10,13 @@ function getFollowersListPaginationAndSort (url: string, start: number, count: n
10 .query({ start }) 10 .query({ start })
11 .query({ count }) 11 .query({ count })
12 .query({ sort }) 12 .query({ sort })
13 .query({ search })
13 .set('Accept', 'application/json') 14 .set('Accept', 'application/json')
14 .expect(200) 15 .expect(200)
15 .expect('Content-Type', /json/) 16 .expect('Content-Type', /json/)
16} 17}
17 18
18function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string) { 19function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
19 const path = '/api/v1/server/following' 20 const path = '/api/v1/server/following'
20 21
21 return request(url) 22 return request(url)
@@ -23,6 +24,7 @@ function getFollowingListPaginationAndSort (url: string, start: number, count: n
23 .query({ start }) 24 .query({ start })
24 .query({ count }) 25 .query({ count })
25 .query({ sort }) 26 .query({ sort })
27 .query({ search })
26 .set('Accept', 'application/json') 28 .set('Accept', 'application/json')
27 .expect(200) 29 .expect(200)
28 .expect('Content-Type', /json/) 30 .expect('Content-Type', /json/)
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts
index 41d8ce265..d77233d62 100644
--- a/server/tests/utils/users/users.ts
+++ b/server/tests/utils/users/users.ts
@@ -112,7 +112,7 @@ function getUsersList (url: string, accessToken: string) {
112 .expect('Content-Type', /json/) 112 .expect('Content-Type', /json/)
113} 113}
114 114
115function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) { 115function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) {
116 const path = '/api/v1/users' 116 const path = '/api/v1/users'
117 117
118 return request(url) 118 return request(url)
@@ -120,6 +120,7 @@ function getUsersListPaginationAndSort (url: string, accessToken: string, start:
120 .query({ start }) 120 .query({ start })
121 .query({ count }) 121 .query({ count })
122 .query({ sort }) 122 .query({ sort })
123 .query({ search })
123 .set('Accept', 'application/json') 124 .set('Accept', 'application/json')
124 .set('Authorization', 'Bearer ' + accessToken) 125 .set('Authorization', 'Bearer ' + accessToken)
125 .expect(200) 126 .expect(200)
diff --git a/server/tests/utils/videos/video-history.ts b/server/tests/utils/videos/video-history.ts
new file mode 100644
index 000000000..7635478f7
--- /dev/null
+++ b/server/tests/utils/videos/video-history.ts
@@ -0,0 +1,14 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) {
4 const path = '/api/v1/videos/' + videoId + '/watching'
5 const fields = { currentTime }
6
7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 })
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 userWatchVideo
14}