aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts3
-rw-r--r--server/controllers/api/search.ts3
-rw-r--r--server/controllers/api/videos/captions.ts6
-rw-r--r--server/controllers/api/videos/comment.ts6
-rw-r--r--server/controllers/api/videos/index.ts6
-rw-r--r--server/controllers/api/videos/watching.ts36
-rw-r--r--server/helpers/custom-validators/videos.ts4
-rw-r--r--server/helpers/video.ts4
-rw-r--r--server/initializers/database.ts4
-rw-r--r--server/lib/redis.ts54
-rw-r--r--server/middlewares/cache.ts2
-rw-r--r--server/middlewares/validators/index.ts4
-rw-r--r--server/middlewares/validators/videos/index.ts8
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts (renamed from server/middlewares/validators/video-abuses.ts)10
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts (renamed from server/middlewares/validators/video-blacklist.ts)10
-rw-r--r--server/middlewares/validators/videos/video-captions.ts (renamed from server/middlewares/validators/video-captions.ts)16
-rw-r--r--server/middlewares/validators/videos/video-channels.ts (renamed from server/middlewares/validators/video-channels.ts)18
-rw-r--r--server/middlewares/validators/videos/video-comments.ts (renamed from server/middlewares/validators/video-comments.ts)18
-rw-r--r--server/middlewares/validators/videos/video-imports.ts (renamed from server/middlewares/validators/video-imports.ts)16
-rw-r--r--server/middlewares/validators/videos/video-watch.ts28
-rw-r--r--server/middlewares/validators/videos/videos.ts (renamed from server/middlewares/validators/videos.ts)34
-rw-r--r--server/models/account/user-video-history.ts55
-rw-r--r--server/models/video/video-format-utils.ts9
-rw-r--r--server/models/video/video.ts80
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/videos-history.ts79
-rw-r--r--server/tests/api/videos/index.ts1
-rw-r--r--server/tests/api/videos/videos-history.ts128
-rw-r--r--server/tests/utils/videos/video-history.ts14
29 files changed, 546 insertions, 111 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 6229c44aa..433186179 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -13,8 +13,7 @@ import {
13 localVideoChannelValidator, 13 localVideoChannelValidator,
14 videosCustomGetValidator 14 videosCustomGetValidator
15} from '../../middlewares' 15} from '../../middlewares'
16import { videosGetValidator, videosShareValidator } from '../../middlewares/validators' 16import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
17import { videoCommentGetValidator } from '../../middlewares/validators/video-comments'
18import { AccountModel } from '../../models/account/account' 17import { AccountModel } from '../../models/account/account'
19import { ActorModel } from '../../models/activitypub/actor' 18import { ActorModel } from '../../models/activitypub/actor'
20import { ActorFollowModel } from '../../models/activitypub/actor-follow' 19import { ActorFollowModel } from '../../models/activitypub/actor-follow'
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index fd4db7a54..4be2b5ef7 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -117,7 +117,8 @@ function searchVideos (req: express.Request, res: express.Response) {
117async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { 117async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
118 const options = Object.assign(query, { 118 const options = Object.assign(query, {
119 includeLocalVideos: true, 119 includeLocalVideos: true,
120 nsfw: buildNSFWFilter(res, query.nsfw) 120 nsfw: buildNSFWFilter(res, query.nsfw),
121 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
121 }) 122 })
122 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 123 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
123 124
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 4cf8de1ef..3ba918189 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -1,10 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' 2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
3import { 3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
4 addVideoCaptionValidator,
5 deleteVideoCaptionValidator,
6 listVideoCaptionsValidator
7} from '../../../middlewares/validators/video-captions'
8import { createReqFiles } from '../../../helpers/express-utils' 4import { createReqFiles } from '../../../helpers/express-utils'
9import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' 5import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers'
10import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index dc25e1e85..4f2b4faee 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -13,14 +13,14 @@ import {
13 setDefaultPagination, 13 setDefaultPagination,
14 setDefaultSort 14 setDefaultSort
15} from '../../../middlewares' 15} from '../../../middlewares'
16import { videoCommentThreadsSortValidator } from '../../../middlewares/validators'
17import { 16import {
18 addVideoCommentReplyValidator, 17 addVideoCommentReplyValidator,
19 addVideoCommentThreadValidator, 18 addVideoCommentThreadValidator,
20 listVideoCommentThreadsValidator, 19 listVideoCommentThreadsValidator,
21 listVideoThreadCommentsValidator, 20 listVideoThreadCommentsValidator,
22 removeVideoCommentValidator 21 removeVideoCommentValidator,
23} from '../../../middlewares/validators/video-comments' 22 videoCommentThreadsSortValidator
23} from '../../../middlewares/validators'
24import { VideoModel } from '../../../models/video/video' 24import { VideoModel } from '../../../models/video/video'
25import { VideoCommentModel } from '../../../models/video/video-comment' 25import { VideoCommentModel } from '../../../models/video/video-comment'
26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 15ef8d458..6a73e13d0 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -57,6 +57,7 @@ import { videoCaptionsRouter } from './captions'
57import { videoImportsRouter } from './import' 57import { videoImportsRouter } from './import'
58import { resetSequelizeInstance } from '../../../helpers/database-utils' 58import { resetSequelizeInstance } from '../../../helpers/database-utils'
59import { rename } from 'fs-extra' 59import { rename } from 'fs-extra'
60import { watchingRouter } from './watching'
60 61
61const auditLogger = auditLoggerFactory('videos') 62const auditLogger = auditLoggerFactory('videos')
62const videosRouter = express.Router() 63const videosRouter = express.Router()
@@ -86,6 +87,7 @@ videosRouter.use('/', videoCommentRouter)
86videosRouter.use('/', videoCaptionsRouter) 87videosRouter.use('/', videoCaptionsRouter)
87videosRouter.use('/', videoImportsRouter) 88videosRouter.use('/', videoImportsRouter)
88videosRouter.use('/', ownershipVideoRouter) 89videosRouter.use('/', ownershipVideoRouter)
90videosRouter.use('/', watchingRouter)
89 91
90videosRouter.get('/categories', listVideoCategories) 92videosRouter.get('/categories', listVideoCategories)
91videosRouter.get('/licences', listVideoLicences) 93videosRouter.get('/licences', listVideoLicences)
@@ -119,6 +121,7 @@ videosRouter.get('/:id/description',
119 asyncMiddleware(getVideoDescription) 121 asyncMiddleware(getVideoDescription)
120) 122)
121videosRouter.get('/:id', 123videosRouter.get('/:id',
124 optionalAuthenticate,
122 asyncMiddleware(videosGetValidator), 125 asyncMiddleware(videosGetValidator),
123 getVideo 126 getVideo
124) 127)
@@ -433,7 +436,8 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
433 tagsAllOf: req.query.tagsAllOf, 436 tagsAllOf: req.query.tagsAllOf,
434 nsfw: buildNSFWFilter(res, req.query.nsfw), 437 nsfw: buildNSFWFilter(res, req.query.nsfw),
435 filter: req.query.filter as VideoFilter, 438 filter: req.query.filter as VideoFilter,
436 withFiles: false 439 withFiles: false,
440 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
437 }) 441 })
438 442
439 return res.json(getFormattedObjects(resultList.data, resultList.total)) 443 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts
new file mode 100644
index 000000000..e8876b47a
--- /dev/null
+++ b/server/controllers/api/videos/watching.ts
@@ -0,0 +1,36 @@
1import * as express from 'express'
2import { UserWatchingVideo } from '../../../../shared'
3import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
4import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
5import { UserModel } from '../../../models/account/user'
6
7const watchingRouter = express.Router()
8
9watchingRouter.put('/:videoId/watching',
10 authenticate,
11 asyncMiddleware(videoWatchingValidator),
12 asyncRetryTransactionMiddleware(userWatchVideo)
13)
14
15// ---------------------------------------------------------------------------
16
17export {
18 watchingRouter
19}
20
21// ---------------------------------------------------------------------------
22
23async function userWatchVideo (req: express.Request, res: express.Response) {
24 const user = res.locals.oauth.token.User as UserModel
25
26 const body: UserWatchingVideo = req.body
27 const { id: videoId } = res.locals.video as { id: number }
28
29 await UserVideoHistoryModel.upsert({
30 videoId,
31 userId: user.id,
32 currentTime: body.currentTime
33 })
34
35 return res.type('json').status(204).end()
36}
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 9875c68bd..714f7ac95 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -154,7 +154,9 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use
154} 154}
155 155
156async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { 156async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') {
157 const video = await fetchVideo(id, fetchType) 157 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
158
159 const video = await fetchVideo(id, fetchType, userId)
158 160
159 if (video === null) { 161 if (video === null) {
160 res.status(404) 162 res.status(404)
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index b1577a6b0..1bd21467d 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -2,8 +2,8 @@ import { VideoModel } from '../models/video/video'
2 2
3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' 3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none'
4 4
5function fetchVideo (id: number | string, fetchType: VideoFetchType) { 5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id) 6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
7 7
8 if (fetchType === 'only-video') return VideoModel.load(id) 8 if (fetchType === 'only-video') return VideoModel.load(id)
9 9
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 4d57bf8aa..482c03b31 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -28,6 +28,7 @@ import { VideoImportModel } from '../models/video/video-import'
28import { VideoViewModel } from '../models/video/video-views' 28import { VideoViewModel } from '../models/video/video-views'
29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership' 29import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 30import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
31import { UserVideoHistoryModel } from '../models/account/user-video-history'
31 32
32require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 33require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
33 34
@@ -89,7 +90,8 @@ async function initDatabaseModels (silent: boolean) {
89 ScheduleVideoUpdateModel, 90 ScheduleVideoUpdateModel,
90 VideoImportModel, 91 VideoImportModel,
91 VideoViewModel, 92 VideoViewModel,
92 VideoRedundancyModel 93 VideoRedundancyModel,
94 UserVideoHistoryModel
93 ]) 95 ])
94 96
95 // Check extensions exist in the database 97 // Check extensions exist in the database
diff --git a/server/lib/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/middlewares/cache.ts b/server/middlewares/cache.ts
index 1b44957d3..1e00fc731 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -8,7 +8,7 @@ const lock = new AsyncLock({ timeout: 5000 })
8 8
9function cacheRoute (lifetimeArg: string | number) { 9function cacheRoute (lifetimeArg: string | number) {
10 return async function (req: express.Request, res: express.Response, next: express.NextFunction) { 10 return async function (req: express.Request, res: express.Response, next: express.NextFunction) {
11 const redisKey = Redis.Instance.buildCachedRouteKey(req) 11 const redisKey = Redis.Instance.generateCachedRouteKey(req)
12 12
13 try { 13 try {
14 await lock.acquire(redisKey, async (done) => { 14 await lock.acquire(redisKey, async (done) => {
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 940547a3e..17226614c 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -8,9 +8,5 @@ export * from './sort'
8export * from './users' 8export * from './users'
9export * from './user-subscriptions' 9export * from './user-subscriptions'
10export * from './videos' 10export * from './videos'
11export * from './video-abuses'
12export * from './video-blacklist'
13export * from './video-channels'
14export * from './webfinger' 11export * from './webfinger'
15export * from './search' 12export * from './search'
16export * from './video-imports'
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
new file mode 100644
index 000000000..294783d85
--- /dev/null
+++ b/server/middlewares/validators/videos/index.ts
@@ -0,0 +1,8 @@
1export * from './video-abuses'
2export * from './video-blacklist'
3export * from './video-captions'
4export * from './video-channels'
5export * from './video-comments'
6export * from './video-imports'
7export * from './video-watch'
8export * from './videos'
diff --git a/server/middlewares/validators/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index f15d55a75..be26ca16a 100644
--- a/server/middlewares/validators/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -1,16 +1,16 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param } from 'express-validator/check' 3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../helpers/custom-validators/videos' 5import { isVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from './utils' 7import { areValidationErrors } from '../utils'
8import { 8import {
9 isVideoAbuseExist, 9 isVideoAbuseExist,
10 isVideoAbuseModerationCommentValid, 10 isVideoAbuseModerationCommentValid,
11 isVideoAbuseReasonValid, 11 isVideoAbuseReasonValid,
12 isVideoAbuseStateValid 12 isVideoAbuseStateValid
13} from '../../helpers/custom-validators/video-abuses' 13} from '../../../helpers/custom-validators/video-abuses'
14 14
15const videoAbuseReportValidator = [ 15const videoAbuseReportValidator = [
16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 95a2b9f17..13da7acff 100644
--- a/server/middlewares/validators/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -1,10 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { areValidationErrors } from './utils' 6import { areValidationErrors } from '../utils'
7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
8 8
9const videosBlacklistRemoveValidator = [ 9const videosBlacklistRemoveValidator = [
10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 51ffd7f3c..63d84fbec 100644
--- a/server/middlewares/validators/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -1,13 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { areValidationErrors } from './utils' 2import { areValidationErrors } from '../utils'
3import { checkUserCanManageVideo, isVideoExist } from '../../helpers/custom-validators/videos' 3import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos'
4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
5import { body, param } from 'express-validator/check' 5import { body, param } from 'express-validator/check'
6import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { CONSTRAINTS_FIELDS } from '../../../initializers'
7import { UserRight } from '../../../shared' 7import { UserRight } from '../../../../shared'
8import { logger } from '../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 9import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
10import { cleanUpReqFiles } from '../../helpers/express-utils' 10import { cleanUpReqFiles } from '../../../helpers/express-utils'
11 11
12const addVideoCaptionValidator = [ 12const addVideoCaptionValidator = [
13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 56a347b39..f039794e0 100644
--- a/server/middlewares/validators/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -1,20 +1,20 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { UserRight } from '../../../shared' 3import { UserRight } from '../../../../shared'
4import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' 4import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts'
5import { 5import {
6 isLocalVideoChannelNameExist, 6 isLocalVideoChannelNameExist,
7 isVideoChannelDescriptionValid, 7 isVideoChannelDescriptionValid,
8 isVideoChannelNameValid, 8 isVideoChannelNameValid,
9 isVideoChannelNameWithHostExist, 9 isVideoChannelNameWithHostExist,
10 isVideoChannelSupportValid 10 isVideoChannelSupportValid
11} from '../../helpers/custom-validators/video-channels' 11} from '../../../helpers/custom-validators/video-channels'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../../helpers/logger'
13import { UserModel } from '../../models/account/user' 13import { UserModel } from '../../../models/account/user'
14import { VideoChannelModel } from '../../models/video/video-channel' 14import { VideoChannelModel } from '../../../models/video/video-channel'
15import { areValidationErrors } from './utils' 15import { areValidationErrors } from '../utils'
16import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' 16import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
17import { ActorModel } from '../../models/activitypub/actor' 17import { ActorModel } from '../../../models/activitypub/actor'
18 18
19const listVideoAccountChannelsValidator = [ 19const listVideoAccountChannelsValidator = [
20 param('accountName').exists().withMessage('Should have a valid account name'), 20 param('accountName').exists().withMessage('Should have a valid account name'),
diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 693852499..348d33082 100644
--- a/server/middlewares/validators/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -1,14 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { UserRight } from '../../../shared' 3import { UserRight } from '../../../../shared'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' 5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
6import { isVideoExist } from '../../helpers/custom-validators/videos' 6import { isVideoExist } from '../../../helpers/custom-validators/videos'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { UserModel } from '../../models/account/user' 8import { UserModel } from '../../../models/account/user'
9import { VideoModel } from '../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
10import { VideoCommentModel } from '../../models/video/video-comment' 10import { VideoCommentModel } from '../../../models/video/video-comment'
11import { areValidationErrors } from './utils' 11import { areValidationErrors } from '../utils'
12 12
13const listVideoCommentThreadsValidator = [ 13const listVideoCommentThreadsValidator = [
14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
diff --git a/server/middlewares/validators/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index b2063b8da..48d20f904 100644
--- a/server/middlewares/validators/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -1,14 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isIdValid } from '../../helpers/custom-validators/misc' 3import { isIdValid } from '../../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from '../utils'
6import { getCommonVideoAttributes } from './videos' 6import { getCommonVideoAttributes } from './videos'
7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../helpers/custom-validators/video-imports' 7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
8import { cleanUpReqFiles } from '../../helpers/express-utils' 8import { cleanUpReqFiles } from '../../../helpers/express-utils'
9import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos' 9import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos'
10import { CONFIG } from '../../initializers/constants' 10import { CONFIG } from '../../../initializers/constants'
11import { CONSTRAINTS_FIELDS } from '../../initializers' 11import { CONSTRAINTS_FIELDS } from '../../../initializers'
12 12
13const videoImportAddValidator = getCommonVideoAttributes().concat([ 13const videoImportAddValidator = getCommonVideoAttributes().concat([
14 body('channelId') 14 body('channelId')
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts
new file mode 100644
index 000000000..bca64662f
--- /dev/null
+++ b/server/middlewares/validators/videos/video-watch.ts
@@ -0,0 +1,28 @@
1import { body, param } from 'express-validator/check'
2import * as express from 'express'
3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { areValidationErrors } from '../utils'
6import { logger } from '../../../helpers/logger'
7
8const videoWatchingValidator = [
9 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
10 body('currentTime')
11 .toInt()
12 .isInt().withMessage('Should have correct current time'),
13
14 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 logger.debug('Checking videoWatching parameters', { parameters: req.body })
16
17 if (areValidationErrors(req, res)) return
18 if (!await isVideoExist(req.params.videoId, res, 'id')) return
19
20 return next()
21 }
22]
23
24// ---------------------------------------------------------------------------
25
26export {
27 videoWatchingValidator
28}
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos/videos.ts
index 67eabe468..d6b8aa725 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param, ValidationChain } from 'express-validator/check' 3import { body, param, ValidationChain } from 'express-validator/check'
4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared' 4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
5import { 5import {
6 isBooleanValid, 6 isBooleanValid,
7 isDateValid, 7 isDateValid,
@@ -10,7 +10,7 @@ import {
10 isUUIDValid, 10 isUUIDValid,
11 toIntOrNull, 11 toIntOrNull,
12 toValueOrNull 12 toValueOrNull
13} from '../../helpers/custom-validators/misc' 13} from '../../../helpers/custom-validators/misc'
14import { 14import {
15 checkUserCanManageVideo, 15 checkUserCanManageVideo,
16 isScheduleVideoUpdatePrivacyValid, 16 isScheduleVideoUpdatePrivacyValid,
@@ -27,21 +27,21 @@ import {
27 isVideoRatingTypeValid, 27 isVideoRatingTypeValid,
28 isVideoSupportValid, 28 isVideoSupportValid,
29 isVideoTagsValid 29 isVideoTagsValid
30} from '../../helpers/custom-validators/videos' 30} from '../../../helpers/custom-validators/videos'
31import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils' 31import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
32import { logger } from '../../helpers/logger' 32import { logger } from '../../../helpers/logger'
33import { CONSTRAINTS_FIELDS } from '../../initializers' 33import { CONSTRAINTS_FIELDS } from '../../../initializers'
34import { VideoShareModel } from '../../models/video/video-share' 34import { VideoShareModel } from '../../../models/video/video-share'
35import { authenticate } from '../oauth' 35import { authenticate } from '../../oauth'
36import { areValidationErrors } from './utils' 36import { areValidationErrors } from '../utils'
37import { cleanUpReqFiles } from '../../helpers/express-utils' 37import { cleanUpReqFiles } from '../../../helpers/express-utils'
38import { VideoModel } from '../../models/video/video' 38import { VideoModel } from '../../../models/video/video'
39import { UserModel } from '../../models/account/user' 39import { UserModel } from '../../../models/account/user'
40import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership' 40import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model' 41import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' 42import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
43import { AccountModel } from '../../models/account/account' 43import { AccountModel } from '../../../models/account/account'
44import { VideoFetchType } from '../../helpers/video' 44import { VideoFetchType } from '../../../helpers/video'
45 45
46const videosAddValidator = getCommonVideoAttributes().concat([ 46const videosAddValidator = getCommonVideoAttributes().concat([
47 body('videofile') 47 body('videofile')
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
new file mode 100644
index 000000000..0476cad9d
--- /dev/null
+++ b/server/models/account/user-video-history.ts
@@ -0,0 +1,55 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video'
3import { UserModel } from './user'
4
5@Table({
6 tableName: 'userVideoHistory',
7 indexes: [
8 {
9 fields: [ 'userId', 'videoId' ],
10 unique: true
11 },
12 {
13 fields: [ 'userId' ]
14 },
15 {
16 fields: [ 'videoId' ]
17 }
18 ]
19})
20export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
21 @CreatedAt
22 createdAt: Date
23
24 @UpdatedAt
25 updatedAt: Date
26
27 @AllowNull(false)
28 @IsInt
29 @Column
30 currentTime: number
31
32 @ForeignKey(() => VideoModel)
33 @Column
34 videoId: number
35
36 @BelongsTo(() => VideoModel, {
37 foreignKey: {
38 allowNull: false
39 },
40 onDelete: 'CASCADE'
41 })
42 Video: VideoModel
43
44 @ForeignKey(() => UserModel)
45 @Column
46 userId: number
47
48 @BelongsTo(() => UserModel, {
49 foreignKey: {
50 allowNull: false
51 },
52 onDelete: 'CASCADE'
53 })
54 User: UserModel
55}
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index f23dde9b8..78972b199 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -10,6 +10,7 @@ import {
10 getVideoLikesActivityPubUrl, 10 getVideoLikesActivityPubUrl,
11 getVideoSharesActivityPubUrl 11 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 12} from '../../lib/activitypub'
13import { isArray } from 'util'
13 14
14export type VideoFormattingJSONOptions = { 15export type VideoFormattingJSONOptions = {
15 completeDescription?: boolean 16 completeDescription?: boolean
@@ -24,6 +25,8 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
24 const formattedAccount = video.VideoChannel.Account.toFormattedJSON() 25 const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
25 const formattedVideoChannel = video.VideoChannel.toFormattedJSON() 26 const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
26 27
28 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
29
27 const videoObject: Video = { 30 const videoObject: Video = {
28 id: video.id, 31 id: video.id,
29 uuid: video.uuid, 32 uuid: video.uuid,
@@ -74,7 +77,11 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
74 url: formattedVideoChannel.url, 77 url: formattedVideoChannel.url,
75 host: formattedVideoChannel.host, 78 host: formattedVideoChannel.host,
76 avatar: formattedVideoChannel.avatar 79 avatar: formattedVideoChannel.avatar
77 } 80 },
81
82 userHistory: userHistory ? {
83 currentTime: userHistory.currentTime
84 } : undefined
78 } 85 }
79 86
80 if (options) { 87 if (options) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c89c16bf..0a2d7e6de 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -92,6 +92,8 @@ import {
92 videoModelToFormattedJSON 92 videoModelToFormattedJSON
93} from './video-format-utils' 93} from './video-format-utils'
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history'
96
95 97
96// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
97const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -127,7 +129,8 @@ export enum ScopeNames {
127 WITH_TAGS = 'WITH_TAGS', 129 WITH_TAGS = 'WITH_TAGS',
128 WITH_FILES = 'WITH_FILES', 130 WITH_FILES = 'WITH_FILES',
129 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 131 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
130 WITH_BLACKLISTED = 'WITH_BLACKLISTED' 132 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
133 WITH_USER_HISTORY = 'WITH_USER_HISTORY'
131} 134}
132 135
133type ForAPIOptions = { 136type ForAPIOptions = {
@@ -464,6 +467,8 @@ type AvailableForListIDsOptions = {
464 include: [ 467 include: [
465 { 468 {
466 model: () => VideoFileModel.unscoped(), 469 model: () => VideoFileModel.unscoped(),
470 // FIXME: typings
471 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
467 required: false, 472 required: false,
468 include: [ 473 include: [
469 { 474 {
@@ -482,6 +487,20 @@ type AvailableForListIDsOptions = {
482 required: false 487 required: false
483 } 488 }
484 ] 489 ]
490 },
491 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
492 return {
493 include: [
494 {
495 attributes: [ 'currentTime' ],
496 model: UserVideoHistoryModel.unscoped(),
497 required: false,
498 where: {
499 userId
500 }
501 }
502 ]
503 }
485 } 504 }
486}) 505})
487@Table({ 506@Table({
@@ -672,11 +691,19 @@ export class VideoModel extends Model<VideoModel> {
672 name: 'videoId', 691 name: 'videoId',
673 allowNull: false 692 allowNull: false
674 }, 693 },
675 onDelete: 'cascade', 694 onDelete: 'cascade'
676 hooks: true
677 }) 695 })
678 VideoViews: VideoViewModel[] 696 VideoViews: VideoViewModel[]
679 697
698 @HasMany(() => UserVideoHistoryModel, {
699 foreignKey: {
700 name: 'videoId',
701 allowNull: false
702 },
703 onDelete: 'cascade'
704 })
705 UserVideoHistories: UserVideoHistoryModel[]
706
680 @HasOne(() => ScheduleVideoUpdateModel, { 707 @HasOne(() => ScheduleVideoUpdateModel, {
681 foreignKey: { 708 foreignKey: {
682 name: 'videoId', 709 name: 'videoId',
@@ -930,7 +957,8 @@ export class VideoModel extends Model<VideoModel> {
930 accountId?: number, 957 accountId?: number,
931 videoChannelId?: number, 958 videoChannelId?: number,
932 actorId?: number 959 actorId?: number
933 trendingDays?: number 960 trendingDays?: number,
961 userId?: number
934 }, countVideos = true) { 962 }, countVideos = true) {
935 const query: IFindOptions<VideoModel> = { 963 const query: IFindOptions<VideoModel> = {
936 offset: options.start, 964 offset: options.start,
@@ -961,6 +989,7 @@ export class VideoModel extends Model<VideoModel> {
961 accountId: options.accountId, 989 accountId: options.accountId,
962 videoChannelId: options.videoChannelId, 990 videoChannelId: options.videoChannelId,
963 includeLocalVideos: options.includeLocalVideos, 991 includeLocalVideos: options.includeLocalVideos,
992 userId: options.userId,
964 trendingDays 993 trendingDays
965 } 994 }
966 995
@@ -983,6 +1012,7 @@ export class VideoModel extends Model<VideoModel> {
983 tagsAllOf?: string[] 1012 tagsAllOf?: string[]
984 durationMin?: number // seconds 1013 durationMin?: number // seconds
985 durationMax?: number // seconds 1014 durationMax?: number // seconds
1015 userId?: number
986 }) { 1016 }) {
987 const whereAnd = [] 1017 const whereAnd = []
988 1018
@@ -1058,7 +1088,8 @@ export class VideoModel extends Model<VideoModel> {
1058 licenceOneOf: options.licenceOneOf, 1088 licenceOneOf: options.licenceOneOf,
1059 languageOneOf: options.languageOneOf, 1089 languageOneOf: options.languageOneOf,
1060 tagsOneOf: options.tagsOneOf, 1090 tagsOneOf: options.tagsOneOf,
1061 tagsAllOf: options.tagsAllOf 1091 tagsAllOf: options.tagsAllOf,
1092 userId: options.userId
1062 } 1093 }
1063 1094
1064 return VideoModel.getAvailableForApi(query, queryOptions) 1095 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1125,7 +1156,7 @@ export class VideoModel extends Model<VideoModel> {
1125 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1156 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1126 } 1157 }
1127 1158
1128 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { 1159 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1129 const where = VideoModel.buildWhereIdOrUUID(id) 1160 const where = VideoModel.buildWhereIdOrUUID(id)
1130 1161
1131 const options = { 1162 const options = {
@@ -1134,14 +1165,20 @@ export class VideoModel extends Model<VideoModel> {
1134 transaction: t 1165 transaction: t
1135 } 1166 }
1136 1167
1168 const scopes = [
1169 ScopeNames.WITH_TAGS,
1170 ScopeNames.WITH_BLACKLISTED,
1171 ScopeNames.WITH_FILES,
1172 ScopeNames.WITH_ACCOUNT_DETAILS,
1173 ScopeNames.WITH_SCHEDULED_UPDATE
1174 ]
1175
1176 if (userId) {
1177 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1178 }
1179
1137 return VideoModel 1180 return VideoModel
1138 .scope([ 1181 .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) 1182 .findOne(options)
1146 } 1183 }
1147 1184
@@ -1225,7 +1262,11 @@ export class VideoModel extends Model<VideoModel> {
1225 return {} 1262 return {}
1226 } 1263 }
1227 1264
1228 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { 1265 private static async getAvailableForApi (
1266 query: IFindOptions<VideoModel>,
1267 options: AvailableForListIDsOptions & { userId?: number},
1268 countVideos = true
1269 ) {
1229 const idsScope = { 1270 const idsScope = {
1230 method: [ 1271 method: [
1231 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1272 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1249,8 +1290,15 @@ export class VideoModel extends Model<VideoModel> {
1249 1290
1250 if (ids.length === 0) return { data: [], total: count } 1291 if (ids.length === 0) return { data: [], total: count }
1251 1292
1252 const apiScope = { 1293 // FIXME: typings
1253 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] 1294 const apiScope: any[] = [
1295 {
1296 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1297 }
1298 ]
1299
1300 if (options.userId) {
1301 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] })
1254 } 1302 }
1255 1303
1256 const secondQuery = { 1304 const secondQuery = {
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 44460a167..71a217649 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -15,3 +15,4 @@ import './video-channels'
15import './video-comments' 15import './video-comments'
16import './video-imports' 16import './video-imports'
17import './videos' 17import './videos'
18import './videos-history'
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
new file mode 100644
index 000000000..808c3b616
--- /dev/null
+++ b/server/tests/api/check-params/videos-history.ts
@@ -0,0 +1,79 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 killallServers,
8 makePostBodyRequest,
9 makePutBodyRequest,
10 runServer,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo
14} from '../../utils'
15
16const expect = chai.expect
17
18describe('Test videos history API validator', function () {
19 let path: string
20 let server: ServerInfo
21
22 // ---------------------------------------------------------------
23
24 before(async function () {
25 this.timeout(30000)
26
27 await flushTests()
28
29 server = await runServer(1)
30
31 await setAccessTokensToServers([ server ])
32
33 const res = await uploadVideo(server.url, server.accessToken, {})
34 const videoUUID = res.body.video.uuid
35
36 path = '/api/v1/videos/' + videoUUID + '/watching'
37 })
38
39 describe('When notifying a user is watching a video', function () {
40
41 it('Should fail with an unauthenticated user', async function () {
42 const fields = { currentTime: 5 }
43 await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 })
44 })
45
46 it('Should fail with an incorrect video id', async function () {
47 const fields = { currentTime: 5 }
48 const path = '/api/v1/videos/blabla/watching'
49 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
50 })
51
52 it('Should fail with an unknown video', async function () {
53 const fields = { currentTime: 5 }
54 const path = '/api/v1/videos/d91fff41-c24d-4508-8e13-3bd5902c3b02/watching'
55
56 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 404 })
57 })
58
59 it('Should fail with a bad current time', async function () {
60 const fields = { currentTime: 'hello' }
61 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
62 })
63
64 it('Should succeed with the correct parameters', async function () {
65 const fields = { currentTime: 5 }
66
67 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 })
68 })
69 })
70
71 after(async function () {
72 killallServers([ server ])
73
74 // Keep the logs if the test failed
75 if (this['ok']) {
76 await flushTests()
77 }
78 })
79})
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index bf58f9c79..09bb62a8d 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -14,4 +14,5 @@ import './video-nsfw'
14import './video-privacy' 14import './video-privacy'
15import './video-schedule-update' 15import './video-schedule-update'
16import './video-transcoder' 16import './video-transcoder'
17import './videos-history'
17import './videos-overview' 18import './videos-overview'
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
new file mode 100644
index 000000000..6d289b288
--- /dev/null
+++ b/server/tests/api/videos/videos-history.ts
@@ -0,0 +1,128 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 getVideosListWithToken,
8 getVideoWithToken,
9 killallServers, makePutBodyRequest,
10 runServer, searchVideoWithToken,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo
14} from '../../utils'
15import { Video, VideoDetails } from '../../../../shared/models/videos'
16import { userWatchVideo } from '../../utils/videos/video-history'
17
18const expect = chai.expect
19
20describe('Test videos history', function () {
21 let server: ServerInfo = null
22 let video1UUID: string
23 let video2UUID: string
24 let video3UUID: string
25
26 before(async function () {
27 this.timeout(30000)
28
29 await flushTests()
30
31 server = await runServer(1)
32
33 await setAccessTokensToServers([ server ])
34
35 {
36 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
37 video1UUID = res.body.video.uuid
38 }
39
40 {
41 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
42 video2UUID = res.body.video.uuid
43 }
44
45 {
46 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
47 video3UUID = res.body.video.uuid
48 }
49 })
50
51 it('Should get videos, without watching history', async function () {
52 const res = await getVideosListWithToken(server.url, server.accessToken)
53 const videos: Video[] = res.body.data
54
55 for (const video of videos) {
56 const resDetail = await getVideoWithToken(server.url, server.accessToken, video.id)
57 const videoDetails: VideoDetails = resDetail.body
58
59 expect(video.userHistory).to.be.undefined
60 expect(videoDetails.userHistory).to.be.undefined
61 }
62 })
63
64 it('Should watch the first and second video', async function () {
65 await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
66 await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
67 })
68
69 it('Should return the correct history when listing, searching and getting videos', async function () {
70 const videosOfVideos: Video[][] = []
71
72 {
73 const res = await getVideosListWithToken(server.url, server.accessToken)
74 videosOfVideos.push(res.body.data)
75 }
76
77 {
78 const res = await searchVideoWithToken(server.url, 'video', server.accessToken)
79 videosOfVideos.push(res.body.data)
80 }
81
82 for (const videos of videosOfVideos) {
83 const video1 = videos.find(v => v.uuid === video1UUID)
84 const video2 = videos.find(v => v.uuid === video2UUID)
85 const video3 = videos.find(v => v.uuid === video3UUID)
86
87 expect(video1.userHistory).to.not.be.undefined
88 expect(video1.userHistory.currentTime).to.equal(3)
89
90 expect(video2.userHistory).to.not.be.undefined
91 expect(video2.userHistory.currentTime).to.equal(8)
92
93 expect(video3.userHistory).to.be.undefined
94 }
95
96 {
97 const resDetail = await getVideoWithToken(server.url, server.accessToken, video1UUID)
98 const videoDetails: VideoDetails = resDetail.body
99
100 expect(videoDetails.userHistory).to.not.be.undefined
101 expect(videoDetails.userHistory.currentTime).to.equal(3)
102 }
103
104 {
105 const resDetail = await getVideoWithToken(server.url, server.accessToken, video2UUID)
106 const videoDetails: VideoDetails = resDetail.body
107
108 expect(videoDetails.userHistory).to.not.be.undefined
109 expect(videoDetails.userHistory.currentTime).to.equal(8)
110 }
111
112 {
113 const resDetail = await getVideoWithToken(server.url, server.accessToken, video3UUID)
114 const videoDetails: VideoDetails = resDetail.body
115
116 expect(videoDetails.userHistory).to.be.undefined
117 }
118 })
119
120 after(async function () {
121 killallServers([ server ])
122
123 // Keep the logs if the test failed
124 if (this['ok']) {
125 await flushTests()
126 }
127 })
128})
diff --git a/server/tests/utils/videos/video-history.ts b/server/tests/utils/videos/video-history.ts
new file mode 100644
index 000000000..7635478f7
--- /dev/null
+++ b/server/tests/utils/videos/video-history.ts
@@ -0,0 +1,14 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) {
4 const path = '/api/v1/videos/' + videoId + '/watching'
5 const fields = { currentTime }
6
7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 })
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 userWatchVideo
14}