diff options
Diffstat (limited to 'server')
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' |
16 | import { videosGetValidator, videosShareValidator } from '../../middlewares/validators' | 16 | import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' |
17 | import { videoCommentGetValidator } from '../../middlewares/validators/video-comments' | ||
18 | import { AccountModel } from '../../models/account/account' | 17 | import { AccountModel } from '../../models/account/account' |
19 | import { ActorModel } from '../../models/activitypub/actor' | 18 | import { ActorModel } from '../../models/activitypub/actor' |
20 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 19 | import { 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) { | |||
117 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | 117 | async 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 | ||
62 | async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { | 62 | async 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 | ||
69 | async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { | 75 | async 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 | ||
240 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 240 | async 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' | 2 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' |
3 | import { | 3 | import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' |
4 | addVideoCaptionValidator, | ||
5 | deleteVideoCaptionValidator, | ||
6 | listVideoCaptionsValidator | ||
7 | } from '../../../middlewares/validators/video-captions' | ||
8 | import { createReqFiles } from '../../../helpers/express-utils' | 4 | import { createReqFiles } from '../../../helpers/express-utils' |
9 | import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' | 5 | import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' |
10 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { 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' |
16 | import { videoCommentThreadsSortValidator } from '../../../middlewares/validators' | ||
17 | import { | 16 | import { |
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' | ||
24 | import { VideoModel } from '../../../models/video/video' | 24 | import { VideoModel } from '../../../models/video/video' |
25 | import { VideoCommentModel } from '../../../models/video/video-comment' | 25 | import { VideoCommentModel } from '../../../models/video/video-comment' |
26 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | 26 | import { 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' | |||
57 | import { videoImportsRouter } from './import' | 57 | import { videoImportsRouter } from './import' |
58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
59 | import { rename } from 'fs-extra' | 59 | import { rename } from 'fs-extra' |
60 | import { watchingRouter } from './watching' | ||
60 | 61 | ||
61 | const auditLogger = auditLoggerFactory('videos') | 62 | const auditLogger = auditLoggerFactory('videos') |
62 | const videosRouter = express.Router() | 63 | const videosRouter = express.Router() |
@@ -86,6 +87,7 @@ videosRouter.use('/', videoCommentRouter) | |||
86 | videosRouter.use('/', videoCaptionsRouter) | 87 | videosRouter.use('/', videoCaptionsRouter) |
87 | videosRouter.use('/', videoImportsRouter) | 88 | videosRouter.use('/', videoImportsRouter) |
88 | videosRouter.use('/', ownershipVideoRouter) | 89 | videosRouter.use('/', ownershipVideoRouter) |
90 | videosRouter.use('/', watchingRouter) | ||
89 | 91 | ||
90 | videosRouter.get('/categories', listVideoCategories) | 92 | videosRouter.get('/categories', listVideoCategories) |
91 | videosRouter.get('/licences', listVideoLicences) | 93 | videosRouter.get('/licences', listVideoLicences) |
@@ -119,6 +121,7 @@ videosRouter.get('/:id/description', | |||
119 | asyncMiddleware(getVideoDescription) | 121 | asyncMiddleware(getVideoDescription) |
120 | ) | 122 | ) |
121 | videosRouter.get('/:id', | 123 | videosRouter.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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { UserWatchingVideo } from '../../../../shared' | ||
3 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' | ||
4 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | ||
5 | import { UserModel } from '../../../models/account/user' | ||
6 | |||
7 | const watchingRouter = express.Router() | ||
8 | |||
9 | watchingRouter.put('/:videoId/watching', | ||
10 | authenticate, | ||
11 | asyncMiddleware(videoWatchingValidator), | ||
12 | asyncRetryTransactionMiddleware(userWatchVideo) | ||
13 | ) | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | watchingRouter | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | async 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' | 2 | import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' |
3 | import { THUMBNAILS_SIZE } from '../initializers' | 3 | import { THUMBNAILS_SIZE } from '../initializers' |
4 | import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares' | 4 | import { |
5 | asyncMiddleware, | ||
6 | commonVideosFiltersValidator, | ||
7 | setDefaultSort, | ||
8 | videoCommentsFeedsValidator, | ||
9 | videoFeedsValidator, | ||
10 | videosSortValidator | ||
11 | } from '../middlewares' | ||
5 | import { VideoModel } from '../models/video/video' | 12 | import { VideoModel } from '../models/video/video' |
6 | import * as Feed from 'pfeed' | 13 | import * as Feed from 'pfeed' |
7 | import { AccountModel } from '../models/account/account' | 14 | import { 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' | |||
3 | import { values } from 'lodash' | 3 | import { values } from 'lodash' |
4 | import 'multer' | 4 | import 'multer' |
5 | import * as validator from 'validator' | 5 | import * as validator from 'validator' |
6 | import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' | 6 | import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
7 | import { | 7 | import { |
8 | CONSTRAINTS_FIELDS, | 8 | CONSTRAINTS_FIELDS, |
9 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
@@ -22,6 +22,10 @@ import { fetchVideo, VideoFetchType } from '../video' | |||
22 | 22 | ||
23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
24 | 24 | ||
25 | function isVideoFilterValid (filter: VideoFilter) { | ||
26 | return filter === 'local' || filter === 'all-local' | ||
27 | } | ||
28 | |||
25 | function isVideoCategoryValid (value: any) { | 29 | function 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 | ||
156 | async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { | 160 | async 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' | |||
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' | 3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { User } from '../../shared/models/users' | ||
6 | import { deleteFileAsync, generateRandomString } from './utils' | 5 | import { deleteFileAsync, generateRandomString } from './utils' |
7 | import { extname } from 'path' | 6 | import { extname } from 'path' |
8 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
@@ -101,7 +100,7 @@ function createReqFiles ( | |||
101 | } | 100 | } |
102 | 101 | ||
103 | function isUserAbleToSearchRemoteURI (res: express.Response) { | 102 | function 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 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { VideoResolution } from '../../shared/models/videos' | 3 | import { VideoResolution, getTargetBitrate } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -55,6 +55,16 @@ async function getVideoFileFPS (path: string) { | |||
55 | return 0 | 55 | return 0 |
56 | } | 56 | } |
57 | 57 | ||
58 | async 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 | |||
58 | function getDurationFromVideoFile (path: string) { | 68 | function 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 | */ | ||
85 | function 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 | ||
82 | export { | 96 | export { |
@@ -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 | ||
3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' | 3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' |
4 | 4 | ||
5 | function fetchVideo (id: number | string, fetchType: VideoFetchType) { | 5 | function 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' | |||
3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' | 3 | import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models' |
4 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 4 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
5 | import { FollowState } from '../../shared/models/actors' | 5 | import { FollowState } from '../../shared/models/actors' |
6 | import { VideoAbuseState, VideoImportState, VideoPrivacy } from '../../shared/models/videos' | 6 | import { 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 |
8 | import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 8 | import { buildPath, isTestInstance, parseDuration, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' |
9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | 9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' |
@@ -393,7 +393,7 @@ const RATES_LIMIT = { | |||
393 | } | 393 | } |
394 | 394 | ||
395 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour | 395 | let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour |
396 | const VIDEO_TRANSCODING_FPS = { | 396 | const 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' | |||
28 | import { VideoViewModel } from '../models/video/video-views' | 28 | import { VideoViewModel } from '../models/video/video-views' |
29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' | 29 | import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' |
30 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | 30 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' |
31 | import { UserVideoHistoryModel } from '../models/account/user-video-history' | ||
31 | 32 | ||
32 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 33 | require('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 @@ | |||
1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' | 1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' |
2 | import { doRequest } from '../../helpers/requests' | 2 | import { doRequest } from '../../helpers/requests' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import Bluebird = require('bluebird') | 4 | import * as Bluebird from 'bluebird' |
5 | 5 | ||
6 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { | 6 | async 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' | |||
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' | 11 | import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding' |
12 | 12 | ||
13 | export type VideoFilePayload = { | 13 | export 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 @@ | |||
1 | import { CONFIG } from '../initializers' | 1 | import { CONFIG } from '../initializers' |
2 | import { join, extname } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' | 3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' |
4 | import { copy, remove, rename, stat } from 'fs-extra' | 4 | import { copy, remove, rename, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
@@ -7,10 +7,11 @@ import { VideoResolution } from '../../shared/models/videos' | |||
7 | import { VideoFileModel } from '../models/video/video-file' | 7 | import { VideoFileModel } from '../models/video/video-file' |
8 | import { VideoModel } from '../models/video/video' | 8 | import { VideoModel } from '../models/video/video' |
9 | 9 | ||
10 | async function optimizeOriginalVideofile (video: VideoModel) { | 10 | async 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 | ||
126 | export { | 127 | export { |
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 | ||
9 | function cacheRoute (lifetimeArg: string | number) { | 9 | function 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' | |||
8 | export * from './users' | 8 | export * from './users' |
9 | export * from './user-subscriptions' | 9 | export * from './user-subscriptions' |
10 | export * from './videos' | 10 | export * from './videos' |
11 | export * from './video-abuses' | ||
12 | export * from './video-blacklist' | ||
13 | export * from './video-channels' | ||
14 | export * from './webfinger' | 11 | export * from './webfinger' |
15 | export * from './search' | 12 | export * from './search' |
16 | export * 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' | |||
2 | import { areValidationErrors } from './utils' | 2 | import { areValidationErrors } from './utils' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { query } from 'express-validator/check' | 4 | import { query } from 'express-validator/check' |
5 | import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' | 5 | import { isDateValid } from '../../helpers/custom-validators/misc' |
6 | import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc' | ||
7 | 6 | ||
8 | const videosSearchValidator = [ | 7 | const 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 | ||
38 | const 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 | ||
74 | export { | 39 | export { |
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 @@ | |||
1 | export * from './video-abuses' | ||
2 | export * from './video-blacklist' | ||
3 | export * from './video-captions' | ||
4 | export * from './video-channels' | ||
5 | export * from './video-comments' | ||
6 | export * from './video-imports' | ||
7 | export * from './video-watch' | ||
8 | export * 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param } from 'express-validator/check' | 3 | import { body, param } from 'express-validator/check' |
4 | import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' |
5 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 5 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
6 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
7 | import { areValidationErrors } from './utils' | 7 | import { areValidationErrors } from '../utils' |
8 | import { | 8 | import { |
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 | ||
15 | const videoAbuseReportValidator = [ | 15 | const 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' |
4 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
5 | import { logger } from '../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { areValidationErrors } from './utils' | 6 | import { areValidationErrors } from '../utils' |
7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' | 7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' |
8 | 8 | ||
9 | const videosBlacklistRemoveValidator = [ | 9 | const 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { areValidationErrors } from './utils' | 2 | import { areValidationErrors } from '../utils' |
3 | import { checkUserCanManageVideo, isVideoExist } from '../../helpers/custom-validators/videos' | 3 | import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos' |
4 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' |
5 | import { body, param } from 'express-validator/check' | 5 | import { body, param } from 'express-validator/check' |
6 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 6 | import { CONSTRAINTS_FIELDS } from '../../../initializers' |
7 | import { UserRight } from '../../../shared' | 7 | import { UserRight } from '../../../../shared' |
8 | import { logger } from '../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
9 | import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' | 9 | import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' |
10 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 10 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
11 | 11 | ||
12 | const addVideoCaptionValidator = [ | 12 | const 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { UserRight } from '../../../shared' | 3 | import { UserRight } from '../../../../shared' |
4 | import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' | 4 | import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts' |
5 | import { | 5 | import { |
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' |
12 | import { logger } from '../../helpers/logger' | 12 | import { logger } from '../../../helpers/logger' |
13 | import { UserModel } from '../../models/account/user' | 13 | import { UserModel } from '../../../models/account/user' |
14 | import { VideoChannelModel } from '../../models/video/video-channel' | 14 | import { VideoChannelModel } from '../../../models/video/video-channel' |
15 | import { areValidationErrors } from './utils' | 15 | import { areValidationErrors } from '../utils' |
16 | import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' | 16 | import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' |
17 | import { ActorModel } from '../../models/activitypub/actor' | 17 | import { ActorModel } from '../../../models/activitypub/actor' |
18 | 18 | ||
19 | const listVideoAccountChannelsValidator = [ | 19 | const 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { UserRight } from '../../../shared' | 3 | import { UserRight } from '../../../../shared' |
4 | import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' |
5 | import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' | 5 | import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' |
6 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 6 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
7 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../../helpers/logger' |
8 | import { UserModel } from '../../models/account/user' | 8 | import { UserModel } from '../../../models/account/user' |
9 | import { VideoModel } from '../../models/video/video' | 9 | import { VideoModel } from '../../../models/video/video' |
10 | import { VideoCommentModel } from '../../models/video/video-comment' | 10 | import { VideoCommentModel } from '../../../models/video/video-comment' |
11 | import { areValidationErrors } from './utils' | 11 | import { areValidationErrors } from '../utils' |
12 | 12 | ||
13 | const listVideoCommentThreadsValidator = [ | 13 | const 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body } from 'express-validator/check' | 2 | import { body } from 'express-validator/check' |
3 | import { isIdValid } from '../../helpers/custom-validators/misc' | 3 | import { isIdValid } from '../../../helpers/custom-validators/misc' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from '../utils' |
6 | import { getCommonVideoAttributes } from './videos' | 6 | import { getCommonVideoAttributes } from './videos' |
7 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../helpers/custom-validators/video-imports' | 7 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' |
8 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 8 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
9 | import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos' | 9 | import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' |
10 | import { CONFIG } from '../../initializers/constants' | 10 | import { CONFIG } from '../../../initializers/constants' |
11 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 11 | import { CONSTRAINTS_FIELDS } from '../../../initializers' |
12 | 12 | ||
13 | const videoImportAddValidator = getCommonVideoAttributes().concat([ | 13 | const 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 @@ | |||
1 | import { body, param } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | ||
4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | ||
5 | import { areValidationErrors } from '../utils' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | |||
8 | const 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 | |||
26 | export { | ||
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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param, ValidationChain } from 'express-validator/check' | 3 | import { body, param, query, ValidationChain } from 'express-validator/check' |
4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared' | 4 | import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' |
5 | import { | 5 | import { |
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' |
14 | import { | 15 | import { |
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' |
31 | import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' | 33 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' |
32 | import { logger } from '../../helpers/logger' | 34 | import { logger } from '../../../helpers/logger' |
33 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 35 | import { CONSTRAINTS_FIELDS } from '../../../initializers' |
34 | import { VideoShareModel } from '../../models/video/video-share' | 36 | import { VideoShareModel } from '../../../models/video/video-share' |
35 | import { authenticate } from '../oauth' | 37 | import { authenticate } from '../../oauth' |
36 | import { areValidationErrors } from './utils' | 38 | import { areValidationErrors } from '../utils' |
37 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 39 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
38 | import { VideoModel } from '../../models/video/video' | 40 | import { VideoModel } from '../../../models/video/video' |
39 | import { UserModel } from '../../models/account/user' | 41 | import { UserModel } from '../../../models/account/user' |
40 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership' | 42 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' |
41 | import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model' | 43 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' |
42 | import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' | 44 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' |
43 | import { AccountModel } from '../../models/account/account' | 45 | import { AccountModel } from '../../../models/account/account' |
44 | import { VideoFetchType } from '../../helpers/video' | 46 | import { VideoFetchType } from '../../../helpers/video' |
47 | import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | ||
45 | 48 | ||
46 | const videosAddValidator = getCommonVideoAttributes().concat([ | 49 | const 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 | ||
365 | const 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 | ||
368 | export { | 412 | export { |
@@ -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 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoModel } from '../video/video' | ||
3 | import { 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 | }) | ||
20 | export 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' |
13 | import { isArray } from '../../helpers/custom-validators/misc' | ||
13 | 14 | ||
14 | export type VideoFormattingJSONOptions = { | 15 | export 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' |
94 | import * as validator from 'validator' | 94 | import * as validator from 'validator' |
95 | import { 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 |
97 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 98 | const 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 | ||
133 | type ForAPIOptions = { | 135 | type 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' | |||
15 | import './video-comments' | 15 | import './video-comments' |
16 | import './video-imports' | 16 | import './video-imports' |
17 | import './videos' | 17 | import './videos' |
18 | import './videos-filter' | ||
19 | import './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 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | createUser, | ||
7 | flushTests, | ||
8 | killallServers, | ||
9 | makeGetRequest, | ||
10 | runServer, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | userLogin | ||
14 | } from '../../utils' | ||
15 | import { UserRole } from '../../../../shared/models/users' | ||
16 | |||
17 | const expect = chai.expect | ||
18 | |||
19 | async 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 | |||
40 | describe('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 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | flushTests, | ||
7 | killallServers, | ||
8 | makePostBodyRequest, | ||
9 | makePutBodyRequest, | ||
10 | runServer, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | uploadVideo | ||
14 | } from '../../utils' | ||
15 | |||
16 | const expect = chai.expect | ||
17 | |||
18 | describe('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' | |||
14 | import './video-privacy' | 14 | import './video-privacy' |
15 | import './video-schedule-update' | 15 | import './video-schedule-update' |
16 | import './video-transcoder' | 16 | import './video-transcoder' |
17 | import './videos-filter' | ||
18 | import './videos-history' | ||
17 | import './videos-overview' | 19 | import './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' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import * as ffmpeg from 'fluent-ffmpeg' | 6 | import * as ffmpeg from 'fluent-ffmpeg' |
7 | import { VideoDetails, VideoState } from '../../../../shared/models/videos' | 7 | import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' |
8 | import { getVideoFileFPS, audio } from '../../../helpers/ffmpeg-utils' | 8 | import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
9 | import { | 9 | import { |
10 | buildAbsoluteFixturePath, | 10 | buildAbsoluteFixturePath, |
11 | doubleFollow, | 11 | doubleFollow, |
@@ -22,6 +22,8 @@ import { | |||
22 | } from '../../utils' | 22 | } from '../../utils' |
23 | import { join } from 'path' | 23 | import { join } from 'path' |
24 | import { waitJobs } from '../../utils/server/jobs' | 24 | import { waitJobs } from '../../utils/server/jobs' |
25 | import { pathExists } from 'fs-extra' | ||
26 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' | ||
25 | 27 | ||
26 | const expect = chai.expect | 28 | const 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 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | createUser, | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | flushTests, | ||
10 | killallServers, | ||
11 | makeGetRequest, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | uploadVideo, | ||
15 | userLogin | ||
16 | } from '../../utils' | ||
17 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' | ||
18 | import { UserRole } from '../../../../shared/models/users' | ||
19 | |||
20 | const expect = chai.expect | ||
21 | |||
22 | async 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 | |||
50 | describe('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 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | flushTests, | ||
7 | getVideosListWithToken, | ||
8 | getVideoWithToken, | ||
9 | killallServers, makePutBodyRequest, | ||
10 | runServer, searchVideoWithToken, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | uploadVideo | ||
14 | } from '../../utils' | ||
15 | import { Video, VideoDetails } from '../../../../shared/models/videos' | ||
16 | import { userWatchVideo } from '../../utils/videos/video-history' | ||
17 | |||
18 | const expect = chai.expect | ||
19 | |||
20 | describe('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 | ||
54 | function buildAbsoluteFixturePath (path: string) { | 54 | function 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' | |||
2 | import { ServerInfo } from './servers' | 2 | import { ServerInfo } from './servers' |
3 | import { waitJobs } from './jobs' | 3 | import { waitJobs } from './jobs' |
4 | 4 | ||
5 | function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string) { | 5 | function 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 | ||
18 | function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string) { | 19 | function 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 | ||
115 | function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) { | 115 | function 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 @@ | |||
1 | import { makePutBodyRequest } from '../requests/requests' | ||
2 | |||
3 | function 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 | |||
12 | export { | ||
13 | userWatchVideo | ||
14 | } | ||