aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/index.ts4
-rw-r--r--server/controllers/api/users/index.ts12
-rw-r--r--server/controllers/api/users/token.ts5
-rw-r--r--server/controllers/api/videos/index.ts13
-rw-r--r--server/controllers/api/videos/upload.ts7
-rw-r--r--server/controllers/api/videos/view.ts2
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/controllers/lazy-static.ts21
-rw-r--r--server/controllers/services.ts44
-rw-r--r--server/helpers/audit-logger.ts12
-rw-r--r--server/helpers/core-utils.ts37
-rw-r--r--server/helpers/image-utils.ts2
-rw-r--r--server/helpers/logger.ts2
-rw-r--r--server/helpers/upload.ts9
-rw-r--r--server/initializers/checker-before-init.ts5
-rw-r--r--server/initializers/config.ts9
-rw-r--r--server/initializers/constants.ts21
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/migrations/0715-video-source.ts34
-rw-r--r--server/lib/activitypub/process/process-create.ts2
-rw-r--r--server/lib/activitypub/videos/shared/creator.ts2
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts2
-rw-r--r--server/lib/client-html.ts6
-rw-r--r--server/lib/emailer.ts2
-rw-r--r--server/lib/job-queue/handlers/move-to-object-storage.ts17
-rw-r--r--server/lib/job-queue/job-queue.ts11
-rw-r--r--server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts2
-rw-r--r--server/lib/redis.ts10
-rw-r--r--server/lib/schedulers/geo-ip-update-scheduler.ts2
-rw-r--r--server/lib/signup.ts2
-rw-r--r--server/lib/video.ts24
-rw-r--r--server/lib/views/shared/video-viewer-counters.ts8
-rw-r--r--server/lib/views/shared/video-viewer-stats.ts6
-rw-r--r--server/lib/views/shared/video-views.ts17
-rw-r--r--server/lib/views/video-views-manager.ts4
-rw-r--r--server/middlewares/auth.ts4
-rw-r--r--server/middlewares/index.ts1
-rw-r--r--server/middlewares/rate-limiter.ts31
-rw-r--r--server/middlewares/validators/feeds.ts6
-rw-r--r--server/middlewares/validators/shared/videos.ts87
-rw-r--r--server/middlewares/validators/sort.ts4
-rw-r--r--server/middlewares/validators/users.ts2
-rw-r--r--server/middlewares/validators/videos/index.ts1
-rw-r--r--server/middlewares/validators/videos/video-captions.ts4
-rw-r--r--server/middlewares/validators/videos/video-comments.ts10
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts4
-rw-r--r--server/middlewares/validators/videos/video-rates.ts4
-rw-r--r--server/middlewares/validators/videos/video-source.ts37
-rw-r--r--server/middlewares/validators/videos/video-view.ts13
-rw-r--r--server/middlewares/validators/videos/videos.ts30
-rw-r--r--server/models/abuse/abuse-query-builder.ts2
-rw-r--r--server/models/shared/abstract-run-query.ts2
-rw-r--r--server/models/user/user.ts27
-rw-r--r--server/models/utils.ts24
-rw-r--r--server/models/video/video-channel.ts13
-rw-r--r--server/models/video/video-live-session.ts2
-rw-r--r--server/models/video/video-source.ts55
-rw-r--r--server/models/video/video.ts10
-rw-r--r--server/tests/api/check-params/abuses.ts4
-rw-r--r--server/tests/api/check-params/index.ts11
-rw-r--r--server/tests/api/check-params/video-source.ts44
-rw-r--r--server/tests/api/check-params/videos.ts5
-rw-r--r--server/tests/api/live/live.ts4
-rw-r--r--server/tests/api/moderation/abuses.ts4
-rw-r--r--server/tests/api/notifications/user-notifications.ts2
-rw-r--r--server/tests/api/server/contact-form.ts2
-rw-r--r--server/tests/api/server/reverse-proxy.ts11
-rw-r--r--server/tests/api/server/services.ts34
-rw-r--r--server/tests/api/users/users.ts2
-rw-r--r--server/tests/api/videos/index.ts1
-rw-r--r--server/tests/api/videos/video-channels.ts19
-rw-r--r--server/tests/api/videos/video-privacy.ts2
-rw-r--r--server/tests/api/videos/video-source.ts39
-rw-r--r--server/tests/helpers/core-utils.ts65
-rw-r--r--server/types/express.d.ts6
-rw-r--r--server/types/models/video/video-source.ts3
76 files changed, 743 insertions, 256 deletions
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 5f49336b1..d1d4ef765 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -1,6 +1,6 @@
1import cors from 'cors' 1import cors from 'cors'
2import express from 'express' 2import express from 'express'
3import RateLimit from 'express-rate-limit' 3import { buildRateLimiter } from '@server/middlewares'
4import { HttpStatusCode } from '../../../shared/models' 4import { HttpStatusCode } from '../../../shared/models'
5import { badRequest } from '../../helpers/express-utils' 5import { badRequest } from '../../helpers/express-utils'
6import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
@@ -29,7 +29,7 @@ apiRouter.use(cors({
29 credentials: true 29 credentials: true
30})) 30}))
31 31
32const apiRateLimiter = RateLimit({ 32const apiRateLimiter = buildRateLimiter({
33 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, 33 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
34 max: CONFIG.RATES_LIMIT.API.MAX 34 max: CONFIG.RATES_LIMIT.API.MAX
35}) 35})
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 8a06bfe93..46e80d56d 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -1,5 +1,4 @@
1import express from 'express' 1import express from 'express'
2import RateLimit from 'express-rate-limit'
3import { tokensRouter } from '@server/controllers/api/users/token' 2import { tokensRouter } from '@server/controllers/api/users/token'
4import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
5import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 4import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
@@ -17,9 +16,11 @@ import { Notifier } from '../../../lib/notifier'
17import { Redis } from '../../../lib/redis' 16import { Redis } from '../../../lib/redis'
18import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 17import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
19import { 18import {
19 adminUsersSortValidator,
20 asyncMiddleware, 20 asyncMiddleware,
21 asyncRetryTransactionMiddleware, 21 asyncRetryTransactionMiddleware,
22 authenticate, 22 authenticate,
23 buildRateLimiter,
23 ensureUserHasRight, 24 ensureUserHasRight,
24 ensureUserRegistrationAllowed, 25 ensureUserRegistrationAllowed,
25 ensureUserRegistrationAllowedForIP, 26 ensureUserRegistrationAllowedForIP,
@@ -32,7 +33,6 @@ import {
32 usersListValidator, 33 usersListValidator,
33 usersRegisterValidator, 34 usersRegisterValidator,
34 usersRemoveValidator, 35 usersRemoveValidator,
35 usersSortValidator,
36 usersUpdateValidator 36 usersUpdateValidator
37} from '../../../middlewares' 37} from '../../../middlewares'
38import { 38import {
@@ -54,13 +54,13 @@ import { myVideoPlaylistsRouter } from './my-video-playlists'
54 54
55const auditLogger = auditLoggerFactory('users') 55const auditLogger = auditLoggerFactory('users')
56 56
57const signupRateLimiter = RateLimit({ 57const signupRateLimiter = buildRateLimiter({
58 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, 58 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
59 max: CONFIG.RATES_LIMIT.SIGNUP.MAX, 59 max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
60 skipFailedRequests: true 60 skipFailedRequests: true
61}) 61})
62 62
63const askSendEmailLimiter = RateLimit({ 63const askSendEmailLimiter = buildRateLimiter({
64 windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, 64 windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
65 max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX 65 max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
66}) 66})
@@ -84,7 +84,7 @@ usersRouter.get('/',
84 authenticate, 84 authenticate,
85 ensureUserHasRight(UserRight.MANAGE_USERS), 85 ensureUserHasRight(UserRight.MANAGE_USERS),
86 paginationValidator, 86 paginationValidator,
87 usersSortValidator, 87 adminUsersSortValidator,
88 setDefaultSort, 88 setDefaultSort,
89 setDefaultPagination, 89 setDefaultPagination,
90 usersListValidator, 90 usersListValidator,
@@ -277,7 +277,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response) {
277} 277}
278 278
279async function listUsers (req: express.Request, res: express.Response) { 279async function listUsers (req: express.Request, res: express.Response) {
280 const resultList = await UserModel.listForApi({ 280 const resultList = await UserModel.listForAdminApi({
281 start: req.query.start, 281 start: req.query.start,
282 count: req.query.count, 282 count: req.query.count,
283 sort: req.query.sort, 283 sort: req.query.sort,
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 258b50fe9..012a49791 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -1,18 +1,17 @@
1import express from 'express' 1import express from 'express'
2import RateLimit from 'express-rate-limit'
3import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
4import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
5import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' 4import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
6import { handleOAuthToken } from '@server/lib/auth/oauth' 5import { handleOAuthToken } from '@server/lib/auth/oauth'
7import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' 6import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
8import { Hooks } from '@server/lib/plugins/hooks' 7import { Hooks } from '@server/lib/plugins/hooks'
9import { asyncMiddleware, authenticate, openapiOperationDoc } from '@server/middlewares' 8import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares'
10import { buildUUID } from '@shared/extra-utils' 9import { buildUUID } from '@shared/extra-utils'
11import { ScopedToken } from '@shared/models/users/user-scoped-token' 10import { ScopedToken } from '@shared/models/users/user-scoped-token'
12 11
13const tokensRouter = express.Router() 12const tokensRouter = express.Router()
14 13
15const loginRateLimiter = RateLimit({ 14const loginRateLimiter = buildRateLimiter({
16 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, 15 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
17 max: CONFIG.RATES_LIMIT.LOGIN.MAX 16 max: CONFIG.RATES_LIMIT.LOGIN.MAX
18}) 17})
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index be233722c..d4e08293e 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -26,6 +26,7 @@ import {
26 setDefaultVideosSort, 26 setDefaultVideosSort,
27 videosCustomGetValidator, 27 videosCustomGetValidator,
28 videosGetValidator, 28 videosGetValidator,
29 videoSourceGetValidator,
29 videosRemoveValidator, 30 videosRemoveValidator,
30 videosSortValidator 31 videosSortValidator
31} from '../../../middlewares' 32} from '../../../middlewares'
@@ -96,6 +97,14 @@ videosRouter.get('/:id/description',
96 asyncMiddleware(videosGetValidator), 97 asyncMiddleware(videosGetValidator),
97 asyncMiddleware(getVideoDescription) 98 asyncMiddleware(getVideoDescription)
98) 99)
100
101videosRouter.get('/:id/source',
102 openapiOperationDoc({ operationId: 'getVideoSource' }),
103 authenticate,
104 asyncMiddleware(videoSourceGetValidator),
105 getVideoSource
106)
107
99videosRouter.get('/:id', 108videosRouter.get('/:id',
100 openapiOperationDoc({ operationId: 'getVideo' }), 109 openapiOperationDoc({ operationId: 'getVideo' }),
101 optionalAuthenticate, 110 optionalAuthenticate,
@@ -155,6 +164,10 @@ async function getVideoDescription (req: express.Request, res: express.Response)
155 return res.json({ description }) 164 return res.json({ description })
156} 165}
157 166
167function getVideoSource (req: express.Request, res: express.Response) {
168 return res.json(res.locals.videoSource.toFormattedJSON())
169}
170
158async function listVideos (req: express.Request, res: express.Response) { 171async function listVideos (req: express.Request, res: express.Response) {
159 const serverActor = await getServerActor() 172 const serverActor = await getServerActor()
160 173
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 3afbedbb2..c5890691e 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -44,6 +44,7 @@ import {
44import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 44import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
45import { VideoModel } from '../../../models/video/video' 45import { VideoModel } from '../../../models/video/video'
46import { VideoFileModel } from '../../../models/video/video-file' 46import { VideoFileModel } from '../../../models/video/video-file'
47import { VideoSourceModel } from '@server/models/video/video-source'
47 48
48const lTags = loggerTagsFactory('api', 'video') 49const lTags = loggerTagsFactory('api', 'video')
49const auditLogger = auditLoggerFactory('videos') 50const auditLogger = auditLoggerFactory('videos')
@@ -151,6 +152,7 @@ async function addVideo (options: {
151 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 152 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
152 153
153 const videoFile = await buildNewFile(videoPhysicalFile) 154 const videoFile = await buildNewFile(videoPhysicalFile)
155 const originalFilename = videoPhysicalFile.originalname
154 156
155 // Move physical file 157 // Move physical file
156 const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) 158 const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
@@ -181,6 +183,11 @@ async function addVideo (options: {
181 183
182 video.VideoFiles = [ videoFile ] 184 video.VideoFiles = [ videoFile ]
183 185
186 await VideoSourceModel.create({
187 filename: originalFilename,
188 videoId: video.id
189 }, { transaction: t })
190
184 await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) 191 await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
185 192
186 // Schedule an update in the future? 193 // Schedule an update in the future?
diff --git a/server/controllers/api/videos/view.ts b/server/controllers/api/videos/view.ts
index db1091f2d..dee1ec67c 100644
--- a/server/controllers/api/videos/view.ts
+++ b/server/controllers/api/videos/view.ts
@@ -26,7 +26,7 @@ export {
26// --------------------------------------------------------------------------- 26// ---------------------------------------------------------------------------
27 27
28async function viewVideo (req: express.Request, res: express.Response) { 28async function viewVideo (req: express.Request, res: express.Response) {
29 const video = res.locals.onlyVideo 29 const video = res.locals.onlyImmutableVideo
30 30
31 const body = req.body as VideoView 31 const body = req.body as VideoView
32 32
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index c929a6726..9eb31ed93 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,13 +1,13 @@
1import express from 'express' 1import express from 'express'
2import { Feed } from '@peertube/feed'
3import { extname } from 'path' 2import { extname } from 'path'
3import { Feed } from '@peertube/feed'
4import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' 4import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' 6import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
7import { VideoInclude } from '@shared/models' 7import { VideoInclude } from '@shared/models'
8import { buildNSFWFilter } from '../helpers/express-utils' 8import { buildNSFWFilter } from '../helpers/express-utils'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import { FEEDS, MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 10import { MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
11import { 11import {
12 asyncMiddleware, 12 asyncMiddleware,
13 commonVideosFiltersValidator, 13 commonVideosFiltersValidator,
@@ -76,7 +76,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
76 76
77 const comments = await VideoCommentModel.listForFeed({ 77 const comments = await VideoCommentModel.listForFeed({
78 start, 78 start,
79 count: FEEDS.COUNT, 79 count: CONFIG.FEEDS.COMMENTS.COUNT,
80 videoId: video ? video.id : undefined, 80 videoId: video ? video.id : undefined,
81 accountId: account ? account.id : undefined, 81 accountId: account ? account.id : undefined,
82 videoChannelId: videoChannel ? videoChannel.id : undefined 82 videoChannelId: videoChannel ? videoChannel.id : undefined
@@ -166,7 +166,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
166 const server = await getServerActor() 166 const server = await getServerActor()
167 const { data } = await VideoModel.listForApi({ 167 const { data } = await VideoModel.listForApi({
168 start, 168 start,
169 count: FEEDS.COUNT, 169 count: CONFIG.FEEDS.VIDEOS.COUNT,
170 sort: req.query.sort, 170 sort: req.query.sort,
171 displayOnlyForFollower: { 171 displayOnlyForFollower: {
172 actorId: server.id, 172 actorId: server.id,
@@ -202,7 +202,7 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
202 202
203 const { data } = await VideoModel.listForApi({ 203 const { data } = await VideoModel.listForApi({
204 start, 204 start,
205 count: FEEDS.COUNT, 205 count: CONFIG.FEEDS.VIDEOS.COUNT,
206 sort: req.query.sort, 206 sort: req.query.sort,
207 nsfw, 207 nsfw,
208 208
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 55bf02660..8a180b5bc 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -1,9 +1,10 @@
1import cors from 'cors' 1import cors from 'cors'
2import express from 'express' 2import express from 'express'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
4import { MActorImage } from '@server/types/models'
4import { HttpStatusCode } from '../../shared/models/http/http-error-codes' 5import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
5import { logger } from '../helpers/logger' 6import { logger } from '../helpers/logger'
6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' 7import { ACTOR_IMAGES_SIZE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
7import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' 8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
8import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/local-actor' 9import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/local-actor'
9import { asyncMiddleware } from '../middlewares' 10import { asyncMiddleware } from '../middlewares'
@@ -67,10 +68,7 @@ async function getActorImage (req: express.Request, res: express.Response, next:
67 await pushActorImageProcessInQueue({ 68 await pushActorImageProcessInQueue({
68 filename: image.filename, 69 filename: image.filename,
69 fileUrl: image.fileUrl, 70 fileUrl: image.fileUrl,
70 size: { 71 size: getActorImageSize(image),
71 height: image.height,
72 width: image.width
73 },
74 type: image.type 72 type: image.type
75 }) 73 })
76 } catch (err) { 74 } catch (err) {
@@ -94,7 +92,7 @@ async function getActorImage (req: express.Request, res: express.Response, next:
94 if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { 92 if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) {
95 logger.error('Cannot lazy serve actor image %s.', filename, { err }) 93 logger.error('Cannot lazy serve actor image %s.', filename, { err })
96 94
97 actorImagePathUnsafeCache.del(filename) 95 actorImagePathUnsafeCache.delete(filename)
98 96
99 image.onDisk = false 97 image.onDisk = false
100 image.save() 98 image.save()
@@ -105,6 +103,17 @@ async function getActorImage (req: express.Request, res: express.Response, next:
105 }) 103 })
106} 104}
107 105
106function getActorImageSize (image: MActorImage): { width: number, height: number } {
107 if (image.width && image.height) {
108 return {
109 height: image.height,
110 width: image.width
111 }
112 }
113
114 return ACTOR_IMAGES_SIZE[image.type][0]
115}
116
108async function getPreview (req: express.Request, res: express.Response) { 117async function getPreview (req: express.Request, res: express.Response) {
109 const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) 118 const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename)
110 if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() 119 if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
index 9151e1b04..70d08ab69 100644
--- a/server/controllers/services.ts
+++ b/server/controllers/services.ts
@@ -1,9 +1,9 @@
1import express from 'express' 1import express from 'express'
2import { EMBED_SIZE, PREVIEWS_SIZE, WEBSERVER, THUMBNAILS_SIZE } from '../initializers/constants'
3import { asyncMiddleware, oembedValidator } from '../middlewares'
4import { accountNameWithHostGetValidator } from '../middlewares/validators'
5import { MChannelSummary } from '@server/types/models' 2import { MChannelSummary } from '@server/types/models'
6import { escapeHTML } from '@shared/core-utils/renderer' 3import { escapeHTML } from '@shared/core-utils/renderer'
4import { EMBED_SIZE, PREVIEWS_SIZE, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants'
5import { asyncMiddleware, oembedValidator } from '../middlewares'
6import { accountNameWithHostGetValidator } from '../middlewares/validators'
7 7
8const servicesRouter = express.Router() 8const servicesRouter = express.Router()
9 9
@@ -36,7 +36,7 @@ function generatePlaylistOEmbed (req: express.Request, res: express.Response) {
36 const json = buildOEmbed({ 36 const json = buildOEmbed({
37 channel: playlist.VideoChannel, 37 channel: playlist.VideoChannel,
38 title: playlist.name, 38 title: playlist.name,
39 embedPath: playlist.getEmbedStaticPath(), 39 embedPath: playlist.getEmbedStaticPath() + buildPlayerURLQuery(req.query.url),
40 previewPath: playlist.getThumbnailStaticPath(), 40 previewPath: playlist.getThumbnailStaticPath(),
41 previewSize: THUMBNAILS_SIZE, 41 previewSize: THUMBNAILS_SIZE,
42 req 42 req
@@ -51,7 +51,7 @@ function generateVideoOEmbed (req: express.Request, res: express.Response) {
51 const json = buildOEmbed({ 51 const json = buildOEmbed({
52 channel: video.VideoChannel, 52 channel: video.VideoChannel,
53 title: video.name, 53 title: video.name,
54 embedPath: video.getEmbedStaticPath(), 54 embedPath: video.getEmbedStaticPath() + buildPlayerURLQuery(req.query.url),
55 previewPath: video.getPreviewStaticPath(), 55 previewPath: video.getPreviewStaticPath(),
56 previewSize: PREVIEWS_SIZE, 56 previewSize: PREVIEWS_SIZE,
57 req 57 req
@@ -60,6 +60,40 @@ function generateVideoOEmbed (req: express.Request, res: express.Response) {
60 return res.json(json) 60 return res.json(json)
61} 61}
62 62
63function buildPlayerURLQuery (inputQueryUrl: string) {
64 const allowedParameters = new Set([
65 'start',
66 'stop',
67 'loop',
68 'autoplay',
69 'muted',
70 'controls',
71 'controlBar',
72 'title',
73 'api',
74 'warningTitle',
75 'peertubeLink',
76 'p2p',
77 'subtitle',
78 'bigPlayBackgroundColor',
79 'mode',
80 'foregroundColor'
81 ])
82
83 const params = new URLSearchParams()
84
85 new URL(inputQueryUrl).searchParams.forEach((v, k) => {
86 if (allowedParameters.has(k)) {
87 params.append(k, v)
88 }
89 })
90
91 const stringQuery = params.toString()
92 if (!stringQuery) return ''
93
94 return '?' + stringQuery
95}
96
63function buildOEmbed (options: { 97function buildOEmbed (options: {
64 req: express.Request 98 req: express.Request
65 title: string 99 title: string
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts
index 79ef44be1..076b7f11d 100644
--- a/server/helpers/audit-logger.ts
+++ b/server/helpers/audit-logger.ts
@@ -120,7 +120,7 @@ const videoKeysToKeep = [
120 'downloadEnabled' 120 'downloadEnabled'
121] 121]
122class VideoAuditView extends EntityAuditView { 122class VideoAuditView extends EntityAuditView {
123 constructor (private readonly video: VideoDetails) { 123 constructor (video: VideoDetails) {
124 super(videoKeysToKeep, 'video', video) 124 super(videoKeysToKeep, 'video', video)
125 } 125 }
126} 126}
@@ -131,7 +131,7 @@ const videoImportKeysToKeep = [
131 'video-name' 131 'video-name'
132] 132]
133class VideoImportAuditView extends EntityAuditView { 133class VideoImportAuditView extends EntityAuditView {
134 constructor (private readonly videoImport: VideoImport) { 134 constructor (videoImport: VideoImport) {
135 super(videoImportKeysToKeep, 'video-import', videoImport) 135 super(videoImportKeysToKeep, 'video-import', videoImport)
136 } 136 }
137} 137}
@@ -150,7 +150,7 @@ const commentKeysToKeep = [
150 'account-name' 150 'account-name'
151] 151]
152class CommentAuditView extends EntityAuditView { 152class CommentAuditView extends EntityAuditView {
153 constructor (private readonly comment: VideoComment) { 153 constructor (comment: VideoComment) {
154 super(commentKeysToKeep, 'comment', comment) 154 super(commentKeysToKeep, 'comment', comment)
155 } 155 }
156} 156}
@@ -179,7 +179,7 @@ const userKeysToKeep = [
179 'videoChannels' 179 'videoChannels'
180] 180]
181class UserAuditView extends EntityAuditView { 181class UserAuditView extends EntityAuditView {
182 constructor (private readonly user: User) { 182 constructor (user: User) {
183 super(userKeysToKeep, 'user', user) 183 super(userKeysToKeep, 'user', user)
184 } 184 }
185} 185}
@@ -205,7 +205,7 @@ const channelKeysToKeep = [
205 'ownerAccount-displayedName' 205 'ownerAccount-displayedName'
206] 206]
207class VideoChannelAuditView extends EntityAuditView { 207class VideoChannelAuditView extends EntityAuditView {
208 constructor (private readonly channel: VideoChannel) { 208 constructor (channel: VideoChannel) {
209 super(channelKeysToKeep, 'channel', channel) 209 super(channelKeysToKeep, 'channel', channel)
210 } 210 }
211} 211}
@@ -217,7 +217,7 @@ const abuseKeysToKeep = [
217 'createdAt' 217 'createdAt'
218] 218]
219class AbuseAuditView extends EntityAuditView { 219class AbuseAuditView extends EntityAuditView {
220 constructor (private readonly abuse: AdminAbuse) { 220 constructor (abuse: AdminAbuse) {
221 super(abuseKeysToKeep, 'abuse', abuse) 221 super(abuseKeysToKeep, 'abuse', abuse)
222 } 222 }
223} 223}
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 0ec45eb2e..6ebe8e2ac 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -56,6 +56,7 @@ const timeTable = {
56export function parseDurationToMs (duration: number | string): number { 56export function parseDurationToMs (duration: number | string): number {
57 if (duration === null) return null 57 if (duration === null) return null
58 if (typeof duration === 'number') return duration 58 if (typeof duration === 'number') return duration
59 if (!isNaN(+duration)) return +duration
59 60
60 if (typeof duration === 'string') { 61 if (typeof duration === 'string') {
61 const split = duration.match(/^([\d.,]+)\s?(\w+)$/) 62 const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
@@ -76,6 +77,7 @@ export function parseDurationToMs (duration: number | string): number {
76 77
77export function parseBytes (value: string | number): number { 78export function parseBytes (value: string | number): number {
78 if (typeof value === 'number') return value 79 if (typeof value === 'number') return value
80 if (!isNaN(+value)) return +value
79 81
80 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ 82 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
81 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ 83 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
@@ -85,40 +87,55 @@ export function parseBytes (value: string | number): number {
85 const g = /^(\d+)\s*GB$/ 87 const g = /^(\d+)\s*GB$/
86 const m = /^(\d+)\s*MB$/ 88 const m = /^(\d+)\s*MB$/
87 const b = /^(\d+)\s*B$/ 89 const b = /^(\d+)\s*B$/
88 let match 90
91 let match: RegExpMatchArray
89 92
90 if (value.match(tgm)) { 93 if (value.match(tgm)) {
91 match = value.match(tgm) 94 match = value.match(tgm)
92 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + 95 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
93 parseInt(match[2], 10) * 1024 * 1024 * 1024 + 96 parseInt(match[2], 10) * 1024 * 1024 * 1024 +
94 parseInt(match[3], 10) * 1024 * 1024 97 parseInt(match[3], 10) * 1024 * 1024
95 } else if (value.match(tg)) { 98 }
99
100 if (value.match(tg)) {
96 match = value.match(tg) 101 match = value.match(tg)
97 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + 102 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
98 parseInt(match[2], 10) * 1024 * 1024 * 1024 103 parseInt(match[2], 10) * 1024 * 1024 * 1024
99 } else if (value.match(tm)) { 104 }
105
106 if (value.match(tm)) {
100 match = value.match(tm) 107 match = value.match(tm)
101 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + 108 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
102 parseInt(match[2], 10) * 1024 * 1024 109 parseInt(match[2], 10) * 1024 * 1024
103 } else if (value.match(gm)) { 110 }
111
112 if (value.match(gm)) {
104 match = value.match(gm) 113 match = value.match(gm)
105 return parseInt(match[1], 10) * 1024 * 1024 * 1024 + 114 return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
106 parseInt(match[2], 10) * 1024 * 1024 115 parseInt(match[2], 10) * 1024 * 1024
107 } else if (value.match(t)) { 116 }
117
118 if (value.match(t)) {
108 match = value.match(t) 119 match = value.match(t)
109 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 120 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
110 } else if (value.match(g)) { 121 }
122
123 if (value.match(g)) {
111 match = value.match(g) 124 match = value.match(g)
112 return parseInt(match[1], 10) * 1024 * 1024 * 1024 125 return parseInt(match[1], 10) * 1024 * 1024 * 1024
113 } else if (value.match(m)) { 126 }
127
128 if (value.match(m)) {
114 match = value.match(m) 129 match = value.match(m)
115 return parseInt(match[1], 10) * 1024 * 1024 130 return parseInt(match[1], 10) * 1024 * 1024
116 } else if (value.match(b)) { 131 }
132
133 if (value.match(b)) {
117 match = value.match(b) 134 match = value.match(b)
118 return parseInt(match[1], 10) * 1024 135 return parseInt(match[1], 10) * 1024
119 } else {
120 return parseInt(value, 10)
121 } 136 }
137
138 return parseInt(value, 10)
122} 139}
123 140
124// --------------------------------------------------------------------------- 141// ---------------------------------------------------------------------------
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index 7d6451db9..ebb102a0d 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -114,7 +114,7 @@ async function autoResize (options: {
114}) { 114}) {
115 const { sourceImage, newSize, destination } = options 115 const { sourceImage, newSize, destination } = options
116 116
117 // Portrait mode targetting a landscape, apply some effect on the image 117 // Portrait mode targeting a landscape, apply some effect on the image
118 const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight() 118 const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight()
119 const destIsPortraitOrSquare = newSize.width <= newSize.height 119 const destIsPortraitOrSquare = newSize.width <= newSize.height
120 120
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 5fe3646c5..4fbaf8a73 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -57,7 +57,7 @@ const consoleLoggerFormat = format.printf(info => {
57 if (CONFIG.LOG.PRETTIFY_SQL) { 57 if (CONFIG.LOG.PRETTIFY_SQL) {
58 additionalInfos += '\n' + sqlFormat(info.sql, { 58 additionalInfos += '\n' + sqlFormat(info.sql, {
59 language: 'sql', 59 language: 'sql',
60 indent: ' ' 60 tabWidth: 2
61 }) 61 })
62 } else { 62 } else {
63 additionalInfos += ' - ' + info.sql 63 additionalInfos += ' - ' + info.sql
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts
index c94c7ab82..3cb17edd0 100644
--- a/server/helpers/upload.ts
+++ b/server/helpers/upload.ts
@@ -1,5 +1,4 @@
1import { join } from 'path' 1import { join } from 'path'
2import { JobQueue } from '@server/lib/job-queue'
3import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' 2import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants'
4 3
5function getResumableUploadPath (filename?: string) { 4function getResumableUploadPath (filename?: string) {
@@ -8,14 +7,8 @@ function getResumableUploadPath (filename?: string) {
8 return RESUMABLE_UPLOAD_DIRECTORY 7 return RESUMABLE_UPLOAD_DIRECTORY
9} 8}
10 9
11function scheduleDeleteResumableUploadMetaFile (filepath: string) {
12 const payload = { filepath }
13 JobQueue.Instance.createJob({ type: 'delete-resumable-upload-meta-file', payload }, { delay: 900 * 1000 }) // executed in 15 min
14}
15
16// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
17 11
18export { 12export {
19 getResumableUploadPath, 13 getResumableUploadPath
20 scheduleDeleteResumableUploadMetaFile
21} 14}
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 794303743..359f0c31d 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -31,8 +31,8 @@ function checkMissedConfig () {
31 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', 31 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
32 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', 32 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
33 'transcoding.resolutions.2160p', 'video_studio.enabled', 33 'transcoding.resolutions.2160p', 'video_studio.enabled',
34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled', 34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout',
35 'trending.videos.interval_days', 35 'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days',
36 'client.videos.miniature.display_author_avatar', 36 'client.videos.miniature.display_author_avatar',
37 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth', 37 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth',
38 'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence', 38 'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence',
@@ -44,6 +44,7 @@ function checkMissedConfig () {
44 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration', 44 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration',
45 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', 45 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max',
46 'theme.default', 46 'theme.default',
47 'feeds.videos.count', 'feeds.comments.count',
47 'geo_ip.enabled', 'geo_ip.country.database_url', 48 'geo_ip.enabled', 'geo_ip.country.database_url',
48 'remote_redundancy.videos.accept_from', 49 'remote_redundancy.videos.accept_from',
49 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', 50 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 59a65d6a5..c76a839bc 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -247,6 +247,14 @@ const CONFIG = {
247 } 247 }
248 } 248 }
249 }, 249 },
250 FEEDS: {
251 VIDEOS: {
252 COUNT: config.get<number>('feeds.videos.count')
253 },
254 COMMENTS: {
255 COUNT: config.get<number>('feeds.comments.count')
256 }
257 },
250 ADMIN: { 258 ADMIN: {
251 get EMAIL () { return config.get<string>('admin.email') } 259 get EMAIL () { return config.get<string>('admin.email') }
252 }, 260 },
@@ -349,6 +357,7 @@ const CONFIG = {
349 IMPORT: { 357 IMPORT: {
350 VIDEOS: { 358 VIDEOS: {
351 get CONCURRENCY () { return config.get<number>('import.videos.concurrency') }, 359 get CONCURRENCY () { return config.get<number>('import.videos.concurrency') },
360 get TIMEOUT () { return parseDurationToMs(config.get<string>('import.videos.timeout')) },
352 361
353 HTTP: { 362 HTTP: {
354 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }, 363 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') },
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 88f370a9a..edc379088 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 710 27const LAST_MIGRATION_VERSION = 715
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
@@ -58,7 +58,7 @@ const WEBSERVER = {
58 58
59// Sortable columns per schema 59// Sortable columns per schema
60const SORTABLE_COLUMNS = { 60const SORTABLE_COLUMNS = {
61 USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ], 61 ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
62 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], 62 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
63 ACCOUNTS: [ 'createdAt' ], 63 ACCOUNTS: [ 'createdAt' ],
64 JOBS: [ 'createdAt' ], 64 JOBS: [ 'createdAt' ],
@@ -186,7 +186,7 @@ const JOB_TTL: { [id in JobType]: number } = {
186 'video-file-import': 1000 * 3600, // 1 hour 186 'video-file-import': 1000 * 3600, // 1 hour
187 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long 187 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long
188 'video-studio-edition': 1000 * 3600 * 10, // 10 hours 188 'video-studio-edition': 1000 * 3600 * 10, // 10 hours
189 'video-import': 1000 * 3600 * 2, // 2 hours 189 'video-import': CONFIG.IMPORT.VIDEOS.TIMEOUT,
190 'email': 60000 * 10, // 10 minutes 190 'email': 60000 * 10, // 10 minutes
191 'actor-keys': 60000 * 20, // 20 minutes 191 'actor-keys': 60000 * 20, // 20 minutes
192 'videos-views-stats': undefined, // Unlimited 192 'videos-views-stats': undefined, // Unlimited
@@ -213,7 +213,7 @@ const CRAWL_REQUEST_CONCURRENCY = 1 // How many requests in parallel to fetch re
213 213
214const AP_CLEANER = { 214const AP_CLEANER = {
215 CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job 215 CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job
216 UNAVAILABLE_TRESHOLD: 3, // How many attemps we do before removing an unavailable remote resource 216 UNAVAILABLE_TRESHOLD: 3, // How many attempts we do before removing an unavailable remote resource
217 PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS 217 PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS
218} 218}
219 219
@@ -734,12 +734,14 @@ const VIDEO_LIVE = {
734const MEMOIZE_TTL = { 734const MEMOIZE_TTL = {
735 OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours 735 OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours
736 INFO_HASH_EXISTS: 1000 * 3600 * 12, // 12 hours 736 INFO_HASH_EXISTS: 1000 * 3600 * 12, // 12 hours
737 VIDEO_DURATION: 1000 * 10, // 10 seconds
737 LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute 738 LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute
738 LIVE_CHECK_SOCKET_HEALTH: 1000 * 60 // 1 minute 739 LIVE_CHECK_SOCKET_HEALTH: 1000 * 60 // 1 minute
739} 740}
740 741
741const MEMOIZE_LENGTH = { 742const MEMOIZE_LENGTH = {
742 INFO_HASH_EXISTS: 200 743 INFO_HASH_EXISTS: 200,
744 VIDEO_DURATION: 200
743} 745}
744 746
745const QUEUE_CONCURRENCY = { 747const QUEUE_CONCURRENCY = {
@@ -769,12 +771,6 @@ const CUSTOM_HTML_TAG_COMMENTS = {
769 SERVER_CONFIG: '<!-- server config -->' 771 SERVER_CONFIG: '<!-- server config -->'
770} 772}
771 773
772// ---------------------------------------------------------------------------
773
774const FEEDS = {
775 COUNT: 20
776}
777
778const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 774const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
779const LOG_FILENAME = 'peertube.log' 775const LOG_FILENAME = 'peertube.log'
780const AUDIT_LOG_FILENAME = 'peertube-audit.log' 776const AUDIT_LOG_FILENAME = 'peertube-audit.log'
@@ -818,7 +814,7 @@ const STATS_TIMESERIE = {
818// --------------------------------------------------------------------------- 814// ---------------------------------------------------------------------------
819 815
820// Special constants for a test instance 816// Special constants for a test instance
821if (isTestInstance() === true) { 817if (isTestInstance() === true && process.env.PRODUCTION_CONSTANTS !== 'true') {
822 PRIVATE_RSA_KEY_SIZE = 1024 818 PRIVATE_RSA_KEY_SIZE = 1024
823 819
824 ACTOR_FOLLOW_SCORE.BASE = 20 820 ACTOR_FOLLOW_SCORE.BASE = 20
@@ -942,7 +938,6 @@ export {
942 ROUTE_CACHE_LIFETIME, 938 ROUTE_CACHE_LIFETIME,
943 SORTABLE_COLUMNS, 939 SORTABLE_COLUMNS,
944 HLS_STREAMING_PLAYLIST_DIRECTORY, 940 HLS_STREAMING_PLAYLIST_DIRECTORY,
945 FEEDS,
946 JOB_TTL, 941 JOB_TTL,
947 DEFAULT_THEME_NAME, 942 DEFAULT_THEME_NAME,
948 NSFW_POLICY_TYPES, 943 NSFW_POLICY_TYPES,
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 3576f444c..09786a91f 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -49,6 +49,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
49import { VideoTagModel } from '../models/video/video-tag' 49import { VideoTagModel } from '../models/video/video-tag'
50import { VideoViewModel } from '../models/view/video-view' 50import { VideoViewModel } from '../models/view/video-view'
51import { CONFIG } from './config' 51import { CONFIG } from './config'
52import { VideoSourceModel } from '@server/models/video/video-source'
52 53
53require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 54require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
54 55
@@ -126,6 +127,7 @@ async function initDatabaseModels (silent: boolean) {
126 VideoChannelModel, 127 VideoChannelModel,
127 VideoShareModel, 128 VideoShareModel,
128 VideoFileModel, 129 VideoFileModel,
130 VideoSourceModel,
129 VideoCaptionModel, 131 VideoCaptionModel,
130 VideoBlacklistModel, 132 VideoBlacklistModel,
131 VideoTagModel, 133 VideoTagModel,
diff --git a/server/initializers/migrations/0715-video-source.ts b/server/initializers/migrations/0715-video-source.ts
new file mode 100644
index 000000000..efcf77ebd
--- /dev/null
+++ b/server/initializers/migrations/0715-video-source.ts
@@ -0,0 +1,34 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = `
11 CREATE TABLE IF NOT EXISTS "videoSource" (
12 "id" SERIAL ,
13 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
14 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
15 "filename" VARCHAR(255) DEFAULT NULL,
16 "videoId" INTEGER
17 REFERENCES "video" ("id")
18 ON DELETE CASCADE
19 ON UPDATE CASCADE,
20 PRIMARY KEY ("id")
21 );
22 `
23 await utils.sequelize.query(query)
24 }
25}
26
27function down (options) {
28 throw new Error('Not implemented.')
29}
30
31export {
32 up,
33 down
34}
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 3e7931bb2..76ed37aae 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -124,7 +124,7 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc
124 return 124 return
125 } 125 }
126 126
127 // Try to not forward unwanted commments on our videos 127 // Try to not forward unwanted comments on our videos
128 if (video.isOwned()) { 128 if (video.isOwned()) {
129 if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) { 129 if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) {
130 logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url) 130 logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url)
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts
index 688bcbb53..07252fea2 100644
--- a/server/lib/activitypub/videos/shared/creator.ts
+++ b/server/lib/activitypub/videos/shared/creator.ts
@@ -24,7 +24,7 @@ export class APVideoCreator extends APVideoAbstractBuilder {
24 const channel = channelActor.VideoChannel 24 const channel = channelActor.VideoChannel
25 25
26 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) 26 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to)
27 const video = VideoModel.build(videoData) as MVideoThumbnail 27 const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail
28 28
29 const promiseThumbnail = this.tryToGenerateThumbnail(video) 29 const promiseThumbnail = this.tryToGenerateThumbnail(video)
30 30
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
index f02b9cba6..86699c5b8 100644
--- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
@@ -210,8 +210,6 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi
210 210
211 updatedAt: new Date(videoObject.updated), 211 updatedAt: new Date(videoObject.updated),
212 views: videoObject.views, 212 views: videoObject.views,
213 likes: 0,
214 dislikes: 0,
215 remote: true, 213 remote: true,
216 privacy 214 privacy
217 } 215 }
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 337364ac9..1e8d03023 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -30,6 +30,7 @@ import { MAccountActor, MChannelActor } from '../types/models'
30import { getActivityStreamDuration } from './activitypub/activity' 30import { getActivityStreamDuration } from './activitypub/activity'
31import { getBiggestActorImage } from './actor-image' 31import { getBiggestActorImage } from './actor-image'
32import { ServerConfigManager } from './server-config-manager' 32import { ServerConfigManager } from './server-config-manager'
33import { isTestInstance } from '@server/helpers/core-utils'
33 34
34type Tags = { 35type Tags = {
35 ogType: string 36 ogType: string
@@ -232,7 +233,10 @@ class ClientHtml {
232 static async getEmbedHTML () { 233 static async getEmbedHTML () {
233 const path = ClientHtml.getEmbedPath() 234 const path = ClientHtml.getEmbedPath()
234 235
235 if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] 236 // Disable HTML cache in dev mode because webpack can regenerate JS files
237 if (!isTestInstance() && ClientHtml.htmlCache[path]) {
238 return ClientHtml.htmlCache[path]
239 }
236 240
237 const buffer = await readFile(path) 241 const buffer = await readFile(path)
238 const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig() 242 const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index aebca04fe..edc99057c 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -179,7 +179,7 @@ class Emailer {
179 } 179 }
180 } 180 }
181 181
182 // overriden/new variables given for a specific template in the payload 182 // overridden/new variables given for a specific template in the payload
183 const sendOptions = merge(baseOptions, options) 183 const sendOptions = merge(baseOptions, options)
184 184
185 await email.send(sendOptions) 185 await email.send(sendOptions)
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts
index f480b32cd..49064052c 100644
--- a/server/lib/job-queue/handlers/move-to-object-storage.ts
+++ b/server/lib/job-queue/handlers/move-to-object-storage.ts
@@ -48,15 +48,24 @@ export async function processMoveToObjectStorage (job: Job) {
48 await doAfterLastJob({ video, previousVideoState: payload.previousVideoState, isNewVideo: payload.isNewVideo }) 48 await doAfterLastJob({ video, previousVideoState: payload.previousVideoState, isNewVideo: payload.isNewVideo })
49 } 49 }
50 } catch (err) { 50 } catch (err) {
51 logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTags }) 51 await onMoveToObjectStorageFailure(job, err)
52
53 await moveToFailedMoveToObjectStorageState(video)
54 await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingMove')
55 } 52 }
56 53
57 return payload.videoUUID 54 return payload.videoUUID
58} 55}
59 56
57export async function onMoveToObjectStorageFailure (job: Job, err: any) {
58 const payload = job.data as MoveObjectStoragePayload
59
60 const video = await VideoModel.loadWithFiles(payload.videoUUID)
61 if (!video) return
62
63 logger.error('Cannot move video %s to object storage.', video.url, { err, ...lTagsBase(video.uuid, video.url) })
64
65 await moveToFailedMoveToObjectStorageState(video)
66 await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingMove')
67}
68
60// --------------------------------------------------------------------------- 69// ---------------------------------------------------------------------------
61 70
62async function moveWebTorrentFiles (video: MVideoWithAllFiles) { 71async function moveWebTorrentFiles (video: MVideoWithAllFiles) {
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index f339e9135..ce24763f1 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -33,7 +33,7 @@ import { refreshAPObject } from './handlers/activitypub-refresher'
33import { processActorKeys } from './handlers/actor-keys' 33import { processActorKeys } from './handlers/actor-keys'
34import { processEmail } from './handlers/email' 34import { processEmail } from './handlers/email'
35import { processManageVideoTorrent } from './handlers/manage-video-torrent' 35import { processManageVideoTorrent } from './handlers/manage-video-torrent'
36import { processMoveToObjectStorage } from './handlers/move-to-object-storage' 36import { onMoveToObjectStorageFailure, processMoveToObjectStorage } from './handlers/move-to-object-storage'
37import { processVideoFileImport } from './handlers/video-file-import' 37import { processVideoFileImport } from './handlers/video-file-import'
38import { processVideoImport } from './handlers/video-import' 38import { processVideoImport } from './handlers/video-import'
39import { processVideoLiveEnding } from './handlers/video-live-ending' 39import { processVideoLiveEnding } from './handlers/video-live-ending'
@@ -88,6 +88,10 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = {
88 'video-studio-edition': processVideoStudioEdition 88 'video-studio-edition': processVideoStudioEdition
89} 89}
90 90
91const errorHandlers: { [id in JobType]?: (job: Job, err: any) => Promise<any> } = {
92 'move-to-object-storage': onMoveToObjectStorageFailure
93}
94
91const jobTypes: JobType[] = [ 95const jobTypes: JobType[] = [
92 'activitypub-follow', 96 'activitypub-follow',
93 'activitypub-http-broadcast', 97 'activitypub-http-broadcast',
@@ -162,6 +166,11 @@ class JobQueue {
162 : 'error' 166 : 'error'
163 167
164 logger.log(logLevel, 'Cannot execute job %d in queue %s.', job.id, handlerName, { payload: job.data, err }) 168 logger.log(logLevel, 'Cannot execute job %d in queue %s.', job.id, handlerName, { payload: job.data, err })
169
170 if (errorHandlers[job.name]) {
171 errorHandlers[job.name](job, err)
172 .catch(err => logger.error('Cannot run error handler for job failure %d in queue %s.', job.id, handlerName, { err }))
173 }
165 }) 174 })
166 175
167 queue.on('error', err => { 176 queue.on('error', err => {
diff --git a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
index daefa25bd..a7292de69 100644
--- a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
+++ b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
@@ -5,7 +5,7 @@ import { MAbuseFull, MAbuseMessage, MAccountDefault, MUserWithNotificationSettin
5import { UserNotificationType } from '@shared/models' 5import { UserNotificationType } from '@shared/models'
6import { AbstractNotification } from '../common/abstract-notification' 6import { AbstractNotification } from '../common/abstract-notification'
7 7
8export type NewAbuseMessagePayload = { 8type NewAbuseMessagePayload = {
9 abuse: MAbuseFull 9 abuse: MAbuseFull
10 message: MAbuseMessage 10 message: MAbuseMessage
11} 11}
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index d052de786..d6d053d2f 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -1,4 +1,4 @@
1import { createClient, RedisClientOptions, RedisModules, RedisScripts } from 'redis' 1import { createClient, RedisClientOptions, RedisModules } from 'redis'
2import { exists } from '@server/helpers/custom-validators/misc' 2import { exists } from '@server/helpers/custom-validators/misc'
3import { sha256 } from '@shared/extra-utils' 3import { sha256 } from '@shared/extra-utils'
4import { logger } from '../helpers/logger' 4import { logger } from '../helpers/logger'
@@ -16,16 +16,12 @@ import {
16 WEBSERVER 16 WEBSERVER
17} from '../initializers/constants' 17} from '../initializers/constants'
18 18
19// Only used for typings
20// TODO: remove when https://github.com/microsoft/TypeScript/issues/37181 is fixed
21const redisClientWrapperForType = () => createClient<{}, RedisScripts>()
22
23class Redis { 19class Redis {
24 20
25 private static instance: Redis 21 private static instance: Redis
26 private initialized = false 22 private initialized = false
27 private connected = false 23 private connected = false
28 private client: ReturnType<typeof redisClientWrapperForType> 24 private client: ReturnType<typeof createClient>
29 private prefix: string 25 private prefix: string
30 26
31 private constructor () { 27 private constructor () {
@@ -308,7 +304,7 @@ class Redis {
308 return this.deleteKey('resumable-upload-' + uploadId) 304 return this.deleteKey('resumable-upload-' + uploadId)
309 } 305 }
310 306
311 /* ************ AP ressource unavailability ************ */ 307 /* ************ AP resource unavailability ************ */
312 308
313 async addAPUnavailability (url: string) { 309 async addAPUnavailability (url: string) {
314 const key = this.generateAPUnavailabilityKey(url) 310 const key = this.generateAPUnavailabilityKey(url)
diff --git a/server/lib/schedulers/geo-ip-update-scheduler.ts b/server/lib/schedulers/geo-ip-update-scheduler.ts
index 9dda6d76c..b06f5a9b5 100644
--- a/server/lib/schedulers/geo-ip-update-scheduler.ts
+++ b/server/lib/schedulers/geo-ip-update-scheduler.ts
@@ -6,7 +6,7 @@ export class GeoIPUpdateScheduler extends AbstractScheduler {
6 6
7 private static instance: AbstractScheduler 7 private static instance: AbstractScheduler
8 8
9 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.YOUTUBE_DL_UPDATE 9 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.GEO_IP_UPDATE
10 10
11 private constructor () { 11 private constructor () {
12 super() 12 super()
diff --git a/server/lib/signup.ts b/server/lib/signup.ts
index 3c1397a12..f094531eb 100644
--- a/server/lib/signup.ts
+++ b/server/lib/signup.ts
@@ -26,7 +26,7 @@ function isSignupAllowedForCurrentIP (ip: string) {
26 const excludeList = [ 'blacklist' ] 26 const excludeList = [ 'blacklist' ]
27 let matched = '' 27 let matched = ''
28 28
29 // if there is a valid, non-empty whitelist, we exclude all unknown adresses too 29 // if there is a valid, non-empty whitelist, we exclude all unknown addresses too
30 if (CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr(cidr)).length > 0) { 30 if (CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr(cidr)).length > 0) {
31 excludeList.push('unknown') 31 excludeList.push('unknown')
32 } 32 }
diff --git a/server/lib/video.ts b/server/lib/video.ts
index a98e45c60..86718abbe 100644
--- a/server/lib/video.ts
+++ b/server/lib/video.ts
@@ -1,6 +1,6 @@
1import { UploadFiles } from 'express' 1import { UploadFiles } from 'express'
2import { Transaction } from 'sequelize/types' 2import { Transaction } from 'sequelize/types'
3import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' 3import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY, MEMOIZE_LENGTH, MEMOIZE_TTL } from '@server/initializers/constants'
4import { TagModel } from '@server/models/video/tag' 4import { TagModel } from '@server/models/video/tag'
5import { VideoModel } from '@server/models/video/video' 5import { VideoModel } from '@server/models/video/video'
6import { VideoJobInfoModel } from '@server/models/video/video-job-info' 6import { VideoJobInfoModel } from '@server/models/video/video-job-info'
@@ -10,6 +10,7 @@ import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingP
10import { CreateJobOptions, JobQueue } from './job-queue/job-queue' 10import { CreateJobOptions, JobQueue } from './job-queue/job-queue'
11import { updateVideoMiniatureFromExisting } from './thumbnail' 11import { updateVideoMiniatureFromExisting } from './thumbnail'
12import { CONFIG } from '@server/initializers/config' 12import { CONFIG } from '@server/initializers/config'
13import memoizee from 'memoizee'
13 14
14function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { 15function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> {
15 return { 16 return {
@@ -150,6 +151,24 @@ async function addMoveToObjectStorageJob (options: {
150 151
151// --------------------------------------------------------------------------- 152// ---------------------------------------------------------------------------
152 153
154async function getVideoDuration (videoId: number | string) {
155 const video = await VideoModel.load(videoId)
156
157 const duration = video.isLive
158 ? undefined
159 : video.duration
160
161 return { duration, isLive: video.isLive }
162}
163
164const getCachedVideoDuration = memoizee(getVideoDuration, {
165 promise: true,
166 max: MEMOIZE_LENGTH.VIDEO_DURATION,
167 maxAge: MEMOIZE_TTL.VIDEO_DURATION
168})
169
170// ---------------------------------------------------------------------------
171
153export { 172export {
154 buildLocalVideoFromReq, 173 buildLocalVideoFromReq,
155 buildVideoThumbnailsFromReq, 174 buildVideoThumbnailsFromReq,
@@ -157,5 +176,6 @@ export {
157 addOptimizeOrMergeAudioJob, 176 addOptimizeOrMergeAudioJob,
158 addTranscodingJob, 177 addTranscodingJob,
159 addMoveToObjectStorageJob, 178 addMoveToObjectStorageJob,
160 getTranscodingJobPriority 179 getTranscodingJobPriority,
180 getCachedVideoDuration
161} 181}
diff --git a/server/lib/views/shared/video-viewer-counters.ts b/server/lib/views/shared/video-viewer-counters.ts
index 5158f8f93..cf3fa5882 100644
--- a/server/lib/views/shared/video-viewer-counters.ts
+++ b/server/lib/views/shared/video-viewer-counters.ts
@@ -5,7 +5,7 @@ import { sendView } from '@server/lib/activitypub/send/send-view'
5import { PeerTubeSocket } from '@server/lib/peertube-socket' 5import { PeerTubeSocket } from '@server/lib/peertube-socket'
6import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
7import { VideoModel } from '@server/models/video/video' 7import { VideoModel } from '@server/models/video/video'
8import { MVideo } from '@server/types/models' 8import { MVideo, MVideoImmutable } from '@server/types/models'
9import { buildUUID, sha256 } from '@shared/extra-utils' 9import { buildUUID, sha256 } from '@shared/extra-utils'
10 10
11const lTags = loggerTagsFactory('views') 11const lTags = loggerTagsFactory('views')
@@ -33,7 +33,7 @@ export class VideoViewerCounters {
33 // --------------------------------------------------------------------------- 33 // ---------------------------------------------------------------------------
34 34
35 async addLocalViewer (options: { 35 async addLocalViewer (options: {
36 video: MVideo 36 video: MVideoImmutable
37 ip: string 37 ip: string
38 }) { 38 }) {
39 const { video, ip } = options 39 const { video, ip } = options
@@ -86,7 +86,7 @@ export class VideoViewerCounters {
86 // --------------------------------------------------------------------------- 86 // ---------------------------------------------------------------------------
87 87
88 private async addViewerToVideo (options: { 88 private async addViewerToVideo (options: {
89 video: MVideo 89 video: MVideoImmutable
90 viewerId: string 90 viewerId: string
91 viewerExpires?: Date 91 viewerExpires?: Date
92 }) { 92 }) {
@@ -162,7 +162,7 @@ export class VideoViewerCounters {
162 return sha256(this.salt + '-' + ip + '-' + videoUUID) 162 return sha256(this.salt + '-' + ip + '-' + videoUUID)
163 } 163 }
164 164
165 private async federateViewerIfNeeded (video: MVideo, viewer: Viewer) { 165 private async federateViewerIfNeeded (video: MVideoImmutable, viewer: Viewer) {
166 // Federate the viewer if it's been a "long" time we did not 166 // Federate the viewer if it's been a "long" time we did not
167 const now = new Date().getTime() 167 const now = new Date().getTime()
168 const federationLimit = now - (VIEW_LIFETIME.VIEWER_COUNTER * 0.75) 168 const federationLimit = now - (VIEW_LIFETIME.VIEWER_COUNTER * 0.75)
diff --git a/server/lib/views/shared/video-viewer-stats.ts b/server/lib/views/shared/video-viewer-stats.ts
index a9ba25b47..a56c20559 100644
--- a/server/lib/views/shared/video-viewer-stats.ts
+++ b/server/lib/views/shared/video-viewer-stats.ts
@@ -10,7 +10,7 @@ import { Redis } from '@server/lib/redis'
10import { VideoModel } from '@server/models/video/video' 10import { VideoModel } from '@server/models/video/video'
11import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' 11import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
12import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section' 12import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section'
13import { MVideo } from '@server/types/models' 13import { MVideo, MVideoImmutable } from '@server/types/models'
14import { VideoViewEvent } from '@shared/models' 14import { VideoViewEvent } from '@shared/models'
15 15
16const lTags = loggerTagsFactory('views') 16const lTags = loggerTagsFactory('views')
@@ -41,7 +41,7 @@ export class VideoViewerStats {
41 // --------------------------------------------------------------------------- 41 // ---------------------------------------------------------------------------
42 42
43 async addLocalViewer (options: { 43 async addLocalViewer (options: {
44 video: MVideo 44 video: MVideoImmutable
45 currentTime: number 45 currentTime: number
46 ip: string 46 ip: string
47 viewEvent?: VideoViewEvent 47 viewEvent?: VideoViewEvent
@@ -64,7 +64,7 @@ export class VideoViewerStats {
64 // --------------------------------------------------------------------------- 64 // ---------------------------------------------------------------------------
65 65
66 private async updateLocalViewerStats (options: { 66 private async updateLocalViewerStats (options: {
67 video: MVideo 67 video: MVideoImmutable
68 ip: string 68 ip: string
69 currentTime: number 69 currentTime: number
70 viewEvent?: VideoViewEvent 70 viewEvent?: VideoViewEvent
diff --git a/server/lib/views/shared/video-views.ts b/server/lib/views/shared/video-views.ts
index 275f7a014..e563287e1 100644
--- a/server/lib/views/shared/video-views.ts
+++ b/server/lib/views/shared/video-views.ts
@@ -1,7 +1,8 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger' 1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { sendView } from '@server/lib/activitypub/send/send-view' 2import { sendView } from '@server/lib/activitypub/send/send-view'
3import { getCachedVideoDuration } from '@server/lib/video'
3import { getServerActor } from '@server/models/application/application' 4import { getServerActor } from '@server/models/application/application'
4import { MVideo } from '@server/types/models' 5import { MVideo, MVideoImmutable } from '@server/types/models'
5import { buildUUID } from '@shared/extra-utils' 6import { buildUUID } from '@shared/extra-utils'
6import { Redis } from '../../redis' 7import { Redis } from '../../redis'
7 8
@@ -10,7 +11,7 @@ const lTags = loggerTagsFactory('views')
10export class VideoViews { 11export class VideoViews {
11 12
12 async addLocalView (options: { 13 async addLocalView (options: {
13 video: MVideo 14 video: MVideoImmutable
14 ip: string 15 ip: string
15 watchTime: number 16 watchTime: number
16 }) { 17 }) {
@@ -18,7 +19,7 @@ export class VideoViews {
18 19
19 logger.debug('Adding local view to video %s.', video.uuid, { watchTime, ...lTags(video.uuid) }) 20 logger.debug('Adding local view to video %s.', video.uuid, { watchTime, ...lTags(video.uuid) })
20 21
21 if (!this.hasEnoughWatchTime(video, watchTime)) return false 22 if (!await this.hasEnoughWatchTime(video, watchTime)) return false
22 23
23 const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid) 24 const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid)
24 if (viewExists) return false 25 if (viewExists) return false
@@ -46,7 +47,7 @@ export class VideoViews {
46 47
47 // --------------------------------------------------------------------------- 48 // ---------------------------------------------------------------------------
48 49
49 private async addView (video: MVideo) { 50 private async addView (video: MVideoImmutable) {
50 const promises: Promise<any>[] = [] 51 const promises: Promise<any>[] = []
51 52
52 if (video.isOwned()) { 53 if (video.isOwned()) {
@@ -58,10 +59,12 @@ export class VideoViews {
58 await Promise.all(promises) 59 await Promise.all(promises)
59 } 60 }
60 61
61 private hasEnoughWatchTime (video: MVideo, watchTime: number) { 62 private async hasEnoughWatchTime (video: MVideoImmutable, watchTime: number) {
62 if (video.isLive || video.duration >= 30) return watchTime >= 30 63 const { duration, isLive } = await getCachedVideoDuration(video.id)
64
65 if (isLive || duration >= 30) return watchTime >= 30
63 66
64 // Check more than 50% of the video is watched 67 // Check more than 50% of the video is watched
65 return video.duration / watchTime < 2 68 return duration / watchTime < 2
66 } 69 }
67} 70}
diff --git a/server/lib/views/video-views-manager.ts b/server/lib/views/video-views-manager.ts
index ea3b35c6c..86758e8d8 100644
--- a/server/lib/views/video-views-manager.ts
+++ b/server/lib/views/video-views-manager.ts
@@ -1,5 +1,5 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger' 1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { MVideo } from '@server/types/models' 2import { MVideo, MVideoImmutable } from '@server/types/models'
3import { VideoViewEvent } from '@shared/models' 3import { VideoViewEvent } from '@shared/models'
4import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared' 4import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared'
5 5
@@ -41,7 +41,7 @@ export class VideoViewsManager {
41 } 41 }
42 42
43 async processLocalView (options: { 43 async processLocalView (options: {
44 video: MVideo 44 video: MVideoImmutable
45 currentTime: number 45 currentTime: number
46 ip: string | null 46 ip: string | null
47 viewEvent?: VideoViewEvent 47 viewEvent?: VideoViewEvent
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts
index c5424be97..ad3b24ab2 100644
--- a/server/middlewares/auth.ts
+++ b/server/middlewares/auth.ts
@@ -47,7 +47,7 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) {
47 .catch(err => logger.error('Cannot get access token.', { err })) 47 .catch(err => logger.error('Cannot get access token.', { err }))
48} 48}
49 49
50function authenticatePromiseIfNeeded (req: express.Request, res: express.Response, authenticateInQuery = false) { 50function authenticatePromise (req: express.Request, res: express.Response, authenticateInQuery = false) {
51 return new Promise<void>(resolve => { 51 return new Promise<void>(resolve => {
52 // Already authenticated? (or tried to) 52 // Already authenticated? (or tried to)
53 if (res.locals.oauth?.token.User) return resolve() 53 if (res.locals.oauth?.token.User) return resolve()
@@ -76,6 +76,6 @@ function optionalAuthenticate (req: express.Request, res: express.Response, next
76export { 76export {
77 authenticate, 77 authenticate,
78 authenticateSocket, 78 authenticateSocket,
79 authenticatePromiseIfNeeded, 79 authenticatePromise,
80 optionalAuthenticate 80 optionalAuthenticate
81} 81}
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index d2ed079b6..b40f864ce 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -4,6 +4,7 @@ export * from './activitypub'
4export * from './async' 4export * from './async'
5export * from './auth' 5export * from './auth'
6export * from './pagination' 6export * from './pagination'
7export * from './rate-limiter'
7export * from './robots' 8export * from './robots'
8export * from './servers' 9export * from './servers'
9export * from './sort' 10export * from './sort'
diff --git a/server/middlewares/rate-limiter.ts b/server/middlewares/rate-limiter.ts
new file mode 100644
index 000000000..bc9513969
--- /dev/null
+++ b/server/middlewares/rate-limiter.ts
@@ -0,0 +1,31 @@
1import { UserRole } from '@shared/models'
2import RateLimit from 'express-rate-limit'
3import { optionalAuthenticate } from './auth'
4
5const whitelistRoles = new Set([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ])
6
7function buildRateLimiter (options: {
8 windowMs: number
9 max: number
10 skipFailedRequests?: boolean
11}) {
12 return RateLimit({
13 windowMs: options.windowMs,
14 max: options.max,
15 skipFailedRequests: options.skipFailedRequests,
16
17 handler: (req, res, next, options) => {
18 return optionalAuthenticate(req, res, () => {
19 if (res.locals.authenticated === true && whitelistRoles.has(res.locals.oauth.token.User.role)) {
20 return next()
21 }
22
23 return res.status(options.statusCode).send(options.message)
24 })
25 }
26 })
27}
28
29export {
30 buildRateLimiter
31}
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
index f8ebaf6ed..04b4e00c9 100644
--- a/server/middlewares/validators/feeds.ts
+++ b/server/middlewares/validators/feeds.ts
@@ -6,6 +6,7 @@ import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helper
6import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
7import { 7import {
8 areValidationErrors, 8 areValidationErrors,
9 checkCanSeeVideo,
9 doesAccountIdExist, 10 doesAccountIdExist,
10 doesAccountNameWithHostExist, 11 doesAccountNameWithHostExist,
11 doesUserFeedTokenCorrespond, 12 doesUserFeedTokenCorrespond,
@@ -112,7 +113,10 @@ const videoCommentsFeedsValidator = [
112 return res.fail({ message: 'videoId cannot be mixed with a channel filter' }) 113 return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
113 } 114 }
114 115
115 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return 116 if (req.query.videoId) {
117 if (!await doesVideoExist(req.query.videoId, res)) return
118 if (!await checkCanSeeVideo({ req, res, paramId: req.query.videoId, video: res.locals.videoAll })) return
119 }
116 120
117 return next() 121 return next()
118 } 122 }
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts
index 8807435f6..2c2ae3811 100644
--- a/server/middlewares/validators/shared/videos.ts
+++ b/server/middlewares/validators/shared/videos.ts
@@ -1,7 +1,8 @@
1import { Request, Response } from 'express' 1import { Request, Response } from 'express'
2import { isUUIDValid } from '@server/helpers/custom-validators/misc'
2import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' 3import { loadVideo, VideoLoadType } from '@server/lib/model-loaders'
3import { isAbleToUploadVideo } from '@server/lib/user' 4import { isAbleToUploadVideo } from '@server/lib/user'
4import { authenticatePromiseIfNeeded } from '@server/middlewares/auth' 5import { authenticatePromise } from '@server/middlewares/auth'
5import { VideoModel } from '@server/models/video/video' 6import { VideoModel } from '@server/models/video/video'
6import { VideoChannelModel } from '@server/models/video/video-channel' 7import { VideoChannelModel } from '@server/models/video/video-channel'
7import { VideoFileModel } from '@server/models/video/video-file' 8import { VideoFileModel } from '@server/models/video/video-file'
@@ -18,18 +19,19 @@ import {
18 MVideoThumbnail, 19 MVideoThumbnail,
19 MVideoWithRights 20 MVideoWithRights
20} from '@server/types/models' 21} from '@server/types/models'
21import { HttpStatusCode, ServerErrorCode, UserRight } from '@shared/models' 22import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models'
22 23
23async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { 24async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
24 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 25 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
25 26
26 const video = await loadVideo(id, fetchType, userId) 27 const video = await loadVideo(id, fetchType, userId)
27 28
28 if (video === null) { 29 if (!video) {
29 res.fail({ 30 res.fail({
30 status: HttpStatusCode.NOT_FOUND_404, 31 status: HttpStatusCode.NOT_FOUND_404,
31 message: 'Video not found' 32 message: 'Video not found'
32 }) 33 })
34
33 return false 35 return false
34 } 36 }
35 37
@@ -58,6 +60,8 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
58 return true 60 return true
59} 61}
60 62
63// ---------------------------------------------------------------------------
64
61async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { 65async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
62 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { 66 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
63 res.fail({ 67 res.fail({
@@ -70,6 +74,8 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
70 return true 74 return true
71} 75}
72 76
77// ---------------------------------------------------------------------------
78
73async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { 79async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
74 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) 80 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
75 81
@@ -95,32 +101,78 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
95 return true 101 return true
96} 102}
97 103
98async function checkCanSeeVideoIfPrivate (req: Request, res: Response, video: MVideo, authenticateInQuery = false) { 104// ---------------------------------------------------------------------------
99 if (!video.requiresAuth()) return true
100 105
101 const videoWithRights = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id) 106async function checkCanSeeVideo (options: {
107 req: Request
108 res: Response
109 paramId: string
110 video: MVideo
111 authenticateInQuery?: boolean // default false
112}) {
113 const { req, res, video, paramId, authenticateInQuery = false } = options
114
115 if (video.requiresAuth()) {
116 return checkCanSeeAuthVideo(req, res, video, authenticateInQuery)
117 }
102 118
103 return checkCanSeePrivateVideo(req, res, videoWithRights, authenticateInQuery) 119 if (video.privacy === VideoPrivacy.UNLISTED) {
104} 120 if (isUUIDValid(paramId)) return true
105 121
106async function checkCanSeePrivateVideo (req: Request, res: Response, video: MVideoWithRights, authenticateInQuery = false) { 122 return checkCanSeeAuthVideo(req, res, video, authenticateInQuery)
107 await authenticatePromiseIfNeeded(req, res, authenticateInQuery) 123 }
124
125 if (video.privacy === VideoPrivacy.PUBLIC) return true
108 126
109 const user = res.locals.oauth ? res.locals.oauth.token.User : null 127 throw new Error('Fatal error when checking video right ' + video.url)
128}
110 129
111 // Only the owner or a user that have blocklist rights can see the video 130async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights, authenticateInQuery = false) {
112 if (!user || !user.canGetVideo(video)) { 131 const fail = () => {
113 res.fail({ 132 res.fail({
114 status: HttpStatusCode.FORBIDDEN_403, 133 status: HttpStatusCode.FORBIDDEN_403,
115 message: 'Cannot fetch information of private/internal/blocklisted video' 134 message: 'Cannot fetch information of private/internal/blocked video'
116 }) 135 })
117 136
118 return false 137 return false
119 } 138 }
120 139
121 return true 140 await authenticatePromise(req, res, authenticateInQuery)
141
142 const user = res.locals.oauth?.token.User
143 if (!user) return fail()
144
145 const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId
146 ? video as MVideoWithRights
147 : await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
148
149 const privacy = videoWithRights.privacy
150
151 if (privacy === VideoPrivacy.INTERNAL) {
152 // We know we have a user
153 return true
154 }
155
156 const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id
157
158 if (videoWithRights.isBlacklisted()) {
159 if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true
160
161 return fail()
162 }
163
164 if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
165 if (isOwnedByUser || user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true
166
167 return fail()
168 }
169
170 // Should not happen
171 return fail()
122} 172}
123 173
174// ---------------------------------------------------------------------------
175
124function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { 176function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
125 // Retrieve the user who did the request 177 // Retrieve the user who did the request
126 if (onlyOwned && video.isOwned() === false) { 178 if (onlyOwned && video.isOwned() === false) {
@@ -146,6 +198,8 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
146 return true 198 return true
147} 199}
148 200
201// ---------------------------------------------------------------------------
202
149async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) { 203async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
150 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { 204 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
151 res.fail({ 205 res.fail({
@@ -167,7 +221,6 @@ export {
167 doesVideoFileOfVideoExist, 221 doesVideoFileOfVideoExist,
168 222
169 checkUserCanManageVideo, 223 checkUserCanManageVideo,
170 checkCanSeeVideoIfPrivate, 224 checkCanSeeVideo,
171 checkCanSeePrivateVideo,
172 checkUserQuota 225 checkUserQuota
173} 226}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 3ba668460..c9978e3b4 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -28,7 +28,7 @@ function createSortableColumns (sortableColumns: string[]) {
28 return sortableColumns.concat(sortableColumnDesc) 28 return sortableColumns.concat(sortableColumnDesc)
29} 29}
30 30
31const usersSortValidator = checkSortFactory(SORTABLE_COLUMNS.USERS) 31const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) 32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) 33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) 34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
@@ -59,7 +59,7 @@ const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CH
59// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
60 60
61export { 61export {
62 usersSortValidator, 62 adminUsersSortValidator,
63 abusesSortValidator, 63 abusesSortValidator,
64 videoChannelsSortValidator, 64 videoChannelsSortValidator,
65 videoImportsSortValidator, 65 videoImportsSortValidator,
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index bc6007c6d..6d306121e 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -486,7 +486,7 @@ const ensureAuthUserOwnsAccountValidator = [
486 if (res.locals.account.id !== user.Account.id) { 486 if (res.locals.account.id !== user.Account.id) {
487 return res.fail({ 487 return res.fail({
488 status: HttpStatusCode.FORBIDDEN_403, 488 status: HttpStatusCode.FORBIDDEN_403,
489 message: 'Only owner of this account can access this ressource.' 489 message: 'Only owner of this account can access this resource.'
490 }) 490 })
491 } 491 }
492 492
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
index bd2590bc5..1dd7b5d2e 100644
--- a/server/middlewares/validators/videos/index.ts
+++ b/server/middlewares/validators/videos/index.ts
@@ -9,6 +9,7 @@ export * from './video-ownership-changes'
9export * from './video-view' 9export * from './video-view'
10export * from './video-rates' 10export * from './video-rates'
11export * from './video-shares' 11export * from './video-shares'
12export * from './video-source'
12export * from './video-stats' 13export * from './video-stats'
13export * from './video-studio' 14export * from './video-studio'
14export * from './video-transcoding' 15export * from './video-transcoding'
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 441c6b4be..dfb8fefc5 100644
--- a/server/middlewares/validators/videos/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -7,7 +7,7 @@ import { logger } from '../../../helpers/logger'
7import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' 7import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants'
8import { 8import {
9 areValidationErrors, 9 areValidationErrors,
10 checkCanSeeVideoIfPrivate, 10 checkCanSeeVideo,
11 checkUserCanManageVideo, 11 checkUserCanManageVideo,
12 doesVideoCaptionExist, 12 doesVideoCaptionExist,
13 doesVideoExist, 13 doesVideoExist,
@@ -74,7 +74,7 @@ const listVideoCaptionsValidator = [
74 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 74 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
75 75
76 const video = res.locals.onlyVideo 76 const video = res.locals.onlyVideo
77 if (!await checkCanSeeVideoIfPrivate(req, res, video)) return 77 if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return
78 78
79 return next() 79 return next()
80 } 80 }
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 698afdbd1..b22a4e3b7 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -10,7 +10,7 @@ import { Hooks } from '../../../lib/plugins/hooks'
10import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' 10import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
11import { 11import {
12 areValidationErrors, 12 areValidationErrors,
13 checkCanSeeVideoIfPrivate, 13 checkCanSeeVideo,
14 doesVideoCommentExist, 14 doesVideoCommentExist,
15 doesVideoCommentThreadExist, 15 doesVideoCommentThreadExist,
16 doesVideoExist, 16 doesVideoExist,
@@ -54,7 +54,7 @@ const listVideoCommentThreadsValidator = [
54 if (areValidationErrors(req, res)) return 54 if (areValidationErrors(req, res)) return
55 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 55 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
56 56
57 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return 57 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
58 58
59 return next() 59 return next()
60 } 60 }
@@ -73,7 +73,7 @@ const listVideoThreadCommentsValidator = [
73 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 73 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
74 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return 74 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
75 75
76 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return 76 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
77 77
78 return next() 78 return next()
79 } 79 }
@@ -91,7 +91,7 @@ const addVideoCommentThreadValidator = [
91 if (areValidationErrors(req, res)) return 91 if (areValidationErrors(req, res)) return
92 if (!await doesVideoExist(req.params.videoId, res)) return 92 if (!await doesVideoExist(req.params.videoId, res)) return
93 93
94 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return 94 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
95 95
96 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return 96 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
97 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return 97 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
@@ -113,7 +113,7 @@ const addVideoCommentReplyValidator = [
113 if (areValidationErrors(req, res)) return 113 if (areValidationErrors(req, res)) return
114 if (!await doesVideoExist(req.params.videoId, res)) return 114 if (!await doesVideoExist(req.params.videoId, res)) return
115 115
116 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return 116 if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
117 117
118 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return 118 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
119 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return 119 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 241b9ed7b..d514ae0ad 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -33,7 +33,7 @@ import { logger } from '../../../helpers/logger'
33import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 33import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
34import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' 34import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
35import { MVideoPlaylist } from '../../../types/models/video/video-playlist' 35import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
36import { authenticatePromiseIfNeeded } from '../../auth' 36import { authenticatePromise } from '../../auth'
37import { 37import {
38 areValidationErrors, 38 areValidationErrors,
39 doesVideoChannelIdExist, 39 doesVideoChannelIdExist,
@@ -161,7 +161,7 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
161 } 161 }
162 162
163 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { 163 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
164 await authenticatePromiseIfNeeded(req, res) 164 await authenticatePromise(req, res)
165 165
166 const user = res.locals.oauth ? res.locals.oauth.token.User : null 166 const user = res.locals.oauth ? res.locals.oauth.token.User : null
167 167
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
index 1a9736034..8b8eeedb6 100644
--- a/server/middlewares/validators/videos/video-rates.ts
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -8,7 +8,7 @@ import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
8import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' 8import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
9import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
10import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 10import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
11import { areValidationErrors, checkCanSeeVideoIfPrivate, doesVideoExist, isValidVideoIdParam } from '../shared' 11import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
12 12
13const videoUpdateRateValidator = [ 13const videoUpdateRateValidator = [
14 isValidVideoIdParam('id'), 14 isValidVideoIdParam('id'),
@@ -21,7 +21,7 @@ const videoUpdateRateValidator = [
21 if (areValidationErrors(req, res)) return 21 if (areValidationErrors(req, res)) return
22 if (!await doesVideoExist(req.params.id, res)) return 22 if (!await doesVideoExist(req.params.id, res)) return
23 23
24 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.videoAll)) return 24 if (!await checkCanSeeVideo({ req, res, paramId: req.params.id, video: res.locals.videoAll })) return
25 25
26 return next() 26 return next()
27 } 27 }
diff --git a/server/middlewares/validators/videos/video-source.ts b/server/middlewares/validators/videos/video-source.ts
new file mode 100644
index 000000000..31a2f16b3
--- /dev/null
+++ b/server/middlewares/validators/videos/video-source.ts
@@ -0,0 +1,37 @@
1import express from 'express'
2import { getVideoWithAttributes } from '@server/helpers/video'
3import { VideoSourceModel } from '@server/models/video/video-source'
4import { MVideoFullLight } from '@server/types/models'
5import { HttpStatusCode, UserRight } from '@shared/models'
6import { logger } from '../../../helpers/logger'
7import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared'
8
9const videoSourceGetValidator = [
10 isValidVideoIdParam('id'),
11
12 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
13 logger.debug('Checking videoSourceGet parameters', { parameters: req.params })
14
15 if (areValidationErrors(req, res)) return
16 if (!await doesVideoExist(req.params.id, res, 'for-api')) return
17
18 const video = getVideoWithAttributes(res) as MVideoFullLight
19
20 res.locals.videoSource = await VideoSourceModel.loadByVideoId(video.id)
21 if (!res.locals.videoSource) {
22 return res.fail({
23 status: HttpStatusCode.NOT_FOUND_404,
24 message: 'Video source not found'
25 })
26 }
27
28 const user = res.locals.oauth.token.User
29 if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
30
31 return next()
32 }
33]
34
35export {
36 videoSourceGetValidator
37}
diff --git a/server/middlewares/validators/videos/video-view.ts b/server/middlewares/validators/videos/video-view.ts
index 7a4994e8a..2edcd140f 100644
--- a/server/middlewares/validators/videos/video-view.ts
+++ b/server/middlewares/validators/videos/video-view.ts
@@ -6,6 +6,7 @@ import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
6import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' 6import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' 8import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
9import { getCachedVideoDuration } from '@server/lib/video'
9 10
10const getVideoLocalViewerValidator = [ 11const getVideoLocalViewerValidator = [
11 param('localViewerId') 12 param('localViewerId')
@@ -42,20 +43,18 @@ const videoViewValidator = [
42 logger.debug('Checking videoView parameters', { parameters: req.body }) 43 logger.debug('Checking videoView parameters', { parameters: req.body })
43 44
44 if (areValidationErrors(req, res)) return 45 if (areValidationErrors(req, res)) return
45 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 46 if (!await doesVideoExist(req.params.videoId, res, 'only-immutable-attributes')) return
46 47
47 const video = res.locals.onlyVideo 48 const video = res.locals.onlyImmutableVideo
48 const videoDuration = video.isLive 49 const { duration } = await getCachedVideoDuration(video.id)
49 ? undefined
50 : video.duration
51 50
52 if (!exists(req.body.currentTime)) { // TODO: remove in a few versions, introduced in 4.2 51 if (!exists(req.body.currentTime)) { // TODO: remove in a few versions, introduced in 4.2
53 req.body.currentTime = Math.min(videoDuration ?? 0, 30) 52 req.body.currentTime = Math.min(duration ?? 0, 30)
54 } 53 }
55 54
56 const currentTime: number = req.body.currentTime 55 const currentTime: number = req.body.currentTime
57 56
58 if (!isVideoTimeValid(currentTime, videoDuration)) { 57 if (!isVideoTimeValid(currentTime, duration)) {
59 return res.fail({ 58 return res.fail({
60 status: HttpStatusCode.BAD_REQUEST_400, 59 status: HttpStatusCode.BAD_REQUEST_400,
61 message: 'Current time is invalid' 60 message: 'Current time is invalid'
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 0b6b8bfe5..c6d31f8f0 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -7,14 +7,13 @@ import { getServerActor } from '@server/models/application/application'
7import { ExpressPromiseHandler } from '@server/types/express-handler' 7import { ExpressPromiseHandler } from '@server/types/express-handler'
8import { MUserAccountId, MVideoFullLight } from '@server/types/models' 8import { MUserAccountId, MVideoFullLight } from '@server/types/models'
9import { getAllPrivacies } from '@shared/core-utils' 9import { getAllPrivacies } from '@shared/core-utils'
10import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoPrivacy } from '@shared/models' 10import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude } from '@shared/models'
11import { 11import {
12 exists, 12 exists,
13 isBooleanValid, 13 isBooleanValid,
14 isDateValid, 14 isDateValid,
15 isFileValid, 15 isFileValid,
16 isIdValid, 16 isIdValid,
17 isUUIDValid,
18 toArray, 17 toArray,
19 toBooleanOrNull, 18 toBooleanOrNull,
20 toIntOrNull, 19 toIntOrNull,
@@ -50,7 +49,7 @@ import { Hooks } from '../../../lib/plugins/hooks'
50import { VideoModel } from '../../../models/video/video' 49import { VideoModel } from '../../../models/video/video'
51import { 50import {
52 areValidationErrors, 51 areValidationErrors,
53 checkCanSeePrivateVideo, 52 checkCanSeeVideo,
54 checkUserCanManageVideo, 53 checkUserCanManageVideo,
55 checkUserQuota, 54 checkUserQuota,
56 doesVideoChannelOfAccountExist, 55 doesVideoChannelOfAccountExist,
@@ -152,7 +151,7 @@ const videosAddResumableValidator = [
152 151
153 if (!await isVideoAccepted(req, res, file)) return cleanup() 152 if (!await isVideoAccepted(req, res, file)) return cleanup()
154 153
155 res.locals.videoFileResumable = file 154 res.locals.videoFileResumable = { ...file, originalname: file.filename }
156 155
157 return next() 156 return next()
158 } 157 }
@@ -297,28 +296,9 @@ const videosCustomGetValidator = (
297 296
298 const video = getVideoWithAttributes(res) as MVideoFullLight 297 const video = getVideoWithAttributes(res) as MVideoFullLight
299 298
300 // Video private or blacklisted 299 if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id, authenticateInQuery })) return
301 if (video.requiresAuth()) {
302 if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) {
303 return next()
304 }
305 300
306 return 301 return next()
307 }
308
309 // Video is public, anyone can access it
310 if (video.privacy === VideoPrivacy.PUBLIC) return next()
311
312 // Video is unlisted, check we used the uuid to fetch it
313 if (video.privacy === VideoPrivacy.UNLISTED) {
314 if (isUUIDValid(req.params.id)) return next()
315
316 // Don't leak this unlisted video
317 return res.fail({
318 status: HttpStatusCode.NOT_FOUND_404,
319 message: 'Video not found'
320 })
321 }
322 } 302 }
323 ] 303 ]
324} 304}
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts
index 025e6ba55..cfc924ba4 100644
--- a/server/models/abuse/abuse-query-builder.ts
+++ b/server/models/abuse/abuse-query-builder.ts
@@ -13,7 +13,7 @@ export type BuildAbusesQueryOptions = {
13 searchReporter?: string 13 searchReporter?: string
14 searchReportee?: string 14 searchReportee?: string
15 15
16 // video releated 16 // video related
17 searchVideo?: string 17 searchVideo?: string
18 searchVideoChannel?: string 18 searchVideoChannel?: string
19 videoIs?: AbuseVideoIs 19 videoIs?: AbuseVideoIs
diff --git a/server/models/shared/abstract-run-query.ts b/server/models/shared/abstract-run-query.ts
index f1182c7be..7f27a0c4b 100644
--- a/server/models/shared/abstract-run-query.ts
+++ b/server/models/shared/abstract-run-query.ts
@@ -2,7 +2,7 @@ import { QueryTypes, Sequelize, Transaction } from 'sequelize'
2 2
3/** 3/**
4 * 4 *
5 * Abstact builder to run video SQL queries 5 * Abstract builder to run video SQL queries
6 * 6 *
7 */ 7 */
8 8
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 68b2bf523..dc260e512 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -29,12 +29,11 @@ import {
29 MUserDefault, 29 MUserDefault,
30 MUserFormattable, 30 MUserFormattable,
31 MUserNotifSettingChannelDefault, 31 MUserNotifSettingChannelDefault,
32 MUserWithNotificationSetting, 32 MUserWithNotificationSetting
33 MVideoWithRights
34} from '@server/types/models' 33} from '@server/types/models'
35import { AttributesOnly } from '@shared/typescript-utils' 34import { AttributesOnly } from '@shared/typescript-utils'
36import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' 35import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users'
37import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' 36import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models'
38import { User, UserRole } from '../../../shared/models/users' 37import { User, UserRole } from '../../../shared/models/users'
39import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' 38import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
40import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' 39import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
@@ -66,7 +65,7 @@ import { ActorModel } from '../actor/actor'
66import { ActorFollowModel } from '../actor/actor-follow' 65import { ActorFollowModel } from '../actor/actor-follow'
67import { ActorImageModel } from '../actor/actor-image' 66import { ActorImageModel } from '../actor/actor-image'
68import { OAuthTokenModel } from '../oauth/oauth-token' 67import { OAuthTokenModel } from '../oauth/oauth-token'
69import { getSort, throwIfNotValid } from '../utils' 68import { getAdminUsersSort, throwIfNotValid } from '../utils'
70import { VideoModel } from '../video/video' 69import { VideoModel } from '../video/video'
71import { VideoChannelModel } from '../video/video-channel' 70import { VideoChannelModel } from '../video/video-channel'
72import { VideoImportModel } from '../video/video-import' 71import { VideoImportModel } from '../video/video-import'
@@ -461,7 +460,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
461 return this.count() 460 return this.count()
462 } 461 }
463 462
464 static listForApi (parameters: { 463 static listForAdminApi (parameters: {
465 start: number 464 start: number
466 count: number 465 count: number
467 sort: string 466 sort: string
@@ -497,7 +496,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
497 const query: FindOptions = { 496 const query: FindOptions = {
498 offset: start, 497 offset: start,
499 limit: count, 498 limit: count,
500 order: getSort(sort), 499 order: getAdminUsersSort(sort),
501 where 500 where
502 } 501 }
503 502
@@ -851,22 +850,6 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
851 .then(u => u.map(u => u.username)) 850 .then(u => u.map(u => u.username))
852 } 851 }
853 852
854 canGetVideo (video: MVideoWithRights) {
855 const videoUserId = video.VideoChannel.Account.userId
856
857 if (video.isBlacklisted()) {
858 return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
859 }
860
861 if (video.privacy === VideoPrivacy.PRIVATE) {
862 return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
863 }
864
865 if (video.privacy === VideoPrivacy.INTERNAL) return true
866
867 return false
868 }
869
870 hasRight (right: UserRight) { 853 hasRight (right: UserRight) {
871 return hasUserRight(this.role, right) 854 return hasUserRight(this.role, right)
872 } 855 }
diff --git a/server/models/utils.ts b/server/models/utils.ts
index b57290aff..c468f748d 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -11,8 +11,6 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
11 11
12 if (field.toLowerCase() === 'match') { // Search 12 if (field.toLowerCase() === 'match') { // Search
13 finalField = Sequelize.col('similarity') 13 finalField = Sequelize.col('similarity')
14 } else if (field === 'videoQuotaUsed') { // Users list
15 finalField = Sequelize.col('videoQuotaUsed')
16 } else { 14 } else {
17 finalField = field 15 finalField = field
18 } 16 }
@@ -20,6 +18,25 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
20 return [ [ finalField, direction ], lastSort ] 18 return [ [ finalField, direction ], lastSort ]
21} 19}
22 20
21function getAdminUsersSort (value: string): OrderItem[] {
22 const { direction, field } = buildDirectionAndField(value)
23
24 let finalField: string | ReturnType<typeof Sequelize.col>
25
26 if (field === 'videoQuotaUsed') { // Users list
27 finalField = Sequelize.col('videoQuotaUsed')
28 } else {
29 finalField = field
30 }
31
32 const nullPolicy = direction === 'ASC'
33 ? 'NULLS FIRST'
34 : 'NULLS LAST'
35
36 // FIXME: typings
37 return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ]
38}
39
23function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { 40function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
24 const { direction, field } = buildDirectionAndField(value) 41 const { direction, field } = buildDirectionAndField(value)
25 42
@@ -102,7 +119,7 @@ function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'A
102 119
103function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { 120function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
104 if (!model.createdAt || !model.updatedAt) { 121 if (!model.createdAt || !model.updatedAt) {
105 throw new Error('Miss createdAt & updatedAt attribuets to model') 122 throw new Error('Miss createdAt & updatedAt attributes to model')
106 } 123 }
107 124
108 const now = Date.now() 125 const now = Date.now()
@@ -260,6 +277,7 @@ export {
260 buildLocalAccountIdsIn, 277 buildLocalAccountIdsIn,
261 getSort, 278 getSort,
262 getCommentSort, 279 getCommentSort,
280 getAdminUsersSort,
263 getVideoSort, 281 getVideoSort,
264 getBlacklistSort, 282 getBlacklistSort,
265 createSimilarityAttribute, 283 createSimilarityAttribute,
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index d6dd1b8bb..91dafbcf1 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -311,6 +311,16 @@ export type SummaryOptions = {
311 ')' 311 ')'
312 ), 312 ),
313 'viewsPerDay' 313 'viewsPerDay'
314 ],
315 [
316 literal(
317 '(' +
318 'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' +
319 'FROM "video" ' +
320 'WHERE "video"."channelId" = "VideoChannelModel"."id"' +
321 ')'
322 ),
323 'totalViews'
314 ] 324 ]
315 ] 325 ]
316 } 326 }
@@ -766,6 +776,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
766 }) 776 })
767 } 777 }
768 778
779 const totalViews = this.get('totalViews') as number
780
769 const actor = this.Actor.toFormattedJSON() 781 const actor = this.Actor.toFormattedJSON()
770 const videoChannel = { 782 const videoChannel = {
771 id: this.id, 783 id: this.id,
@@ -779,6 +791,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
779 791
780 videosCount, 792 videosCount,
781 viewsPerDay, 793 viewsPerDay,
794 totalViews,
782 795
783 avatars: actor.avatars, 796 avatars: actor.avatars,
784 797
diff --git a/server/models/video/video-live-session.ts b/server/models/video/video-live-session.ts
index 2b4cde9f8..836620872 100644
--- a/server/models/video/video-live-session.ts
+++ b/server/models/video/video-live-session.ts
@@ -110,7 +110,7 @@ export class VideoLiveSessionModel extends Model<Partial<AttributesOnly<VideoLiv
110 static listSessionsOfLiveForAPI (options: { videoId: number }) { 110 static listSessionsOfLiveForAPI (options: { videoId: number }) {
111 const { videoId } = options 111 const { videoId } = options
112 112
113 const query: FindOptions<VideoLiveSessionModel> = { 113 const query: FindOptions<AttributesOnly<VideoLiveSessionModel>> = {
114 where: { 114 where: {
115 liveVideoId: videoId 115 liveVideoId: videoId
116 }, 116 },
diff --git a/server/models/video/video-source.ts b/server/models/video/video-source.ts
new file mode 100644
index 000000000..e306b160d
--- /dev/null
+++ b/server/models/video/video-source.ts
@@ -0,0 +1,55 @@
1import { Op } from 'sequelize'
2import {
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 ForeignKey,
8 Model,
9 Table,
10 UpdatedAt
11} from 'sequelize-typescript'
12import { AttributesOnly } from '@shared/typescript-utils'
13import { VideoModel } from './video'
14
15@Table({
16 tableName: 'videoSource',
17 indexes: [
18 {
19 fields: [ 'videoId' ],
20 where: {
21 videoId: {
22 [Op.ne]: null
23 }
24 }
25 }
26 ]
27})
28export class VideoSourceModel extends Model<Partial<AttributesOnly<VideoSourceModel>>> {
29 @CreatedAt
30 createdAt: Date
31
32 @UpdatedAt
33 updatedAt: Date
34
35 @AllowNull(false)
36 @Column
37 filename: string
38
39 @ForeignKey(() => VideoModel)
40 @Column
41 videoId: number
42
43 @BelongsTo(() => VideoModel)
44 Video: VideoModel
45
46 static loadByVideoId (videoId) {
47 return VideoSourceModel.findOne({ where: { videoId } })
48 }
49
50 toFormattedJSON () {
51 return {
52 filename: this.filename
53 }
54 }
55}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index e6a8d3f95..08adbced6 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -136,6 +136,7 @@ import { VideoPlaylistElementModel } from './video-playlist-element'
136import { VideoShareModel } from './video-share' 136import { VideoShareModel } from './video-share'
137import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 137import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
138import { VideoTagModel } from './video-tag' 138import { VideoTagModel } from './video-tag'
139import { VideoSourceModel } from './video-source'
139 140
140export enum ScopeNames { 141export enum ScopeNames {
141 FOR_API = 'FOR_API', 142 FOR_API = 'FOR_API',
@@ -597,6 +598,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
597 }) 598 })
598 VideoPlaylistElements: VideoPlaylistElementModel[] 599 VideoPlaylistElements: VideoPlaylistElementModel[]
599 600
601 @HasOne(() => VideoSourceModel, {
602 foreignKey: {
603 name: 'videoId',
604 allowNull: true
605 },
606 onDelete: 'CASCADE'
607 })
608 VideoSource: VideoSourceModel
609
600 @HasMany(() => VideoAbuseModel, { 610 @HasMany(() => VideoAbuseModel, {
601 foreignKey: { 611 foreignKey: {
602 name: 'videoId', 612 name: 'videoId',
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts
index c4b051723..bc2cc640f 100644
--- a/server/tests/api/check-params/abuses.ts
+++ b/server/tests/api/check-params/abuses.ts
@@ -269,7 +269,7 @@ describe('Test abuses API validators', function () {
269 await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) 269 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
270 }) 270 })
271 271
272 it('Should succeed with the corret parameters (advanced)', async function () { 272 it('Should succeed with the correct parameters (advanced)', async function () {
273 const fields: AbuseCreate = { 273 const fields: AbuseCreate = {
274 video: { 274 video: {
275 id: server.store.videoCreated.id, 275 id: server.store.videoCreated.id,
@@ -333,7 +333,7 @@ describe('Test abuses API validators', function () {
333 await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 333 await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
334 }) 334 })
335 335
336 it('Should suceed with the correct params', async function () { 336 it('Should succeed with the correct params', async function () {
337 const res = await command.addMessage({ token: userToken, abuseId, message }) 337 const res = await command.addMessage({ token: userToken, abuseId, message })
338 messageId = res.body.abuseMessage.id 338 messageId = res.body.abuseMessage.id
339 }) 339 })
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 259d7e783..a27bc8509 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -3,14 +3,14 @@ import './accounts'
3import './blocklist' 3import './blocklist'
4import './bulk' 4import './bulk'
5import './config' 5import './config'
6import './custom-pages'
7import './contact-form' 6import './contact-form'
7import './custom-pages'
8import './debug' 8import './debug'
9import './follows' 9import './follows'
10import './jobs' 10import './jobs'
11import './live'
11import './logs' 12import './logs'
12import './my-user' 13import './my-user'
13import './live'
14import './plugins' 14import './plugins'
15import './redundancy' 15import './redundancy'
16import './search' 16import './search'
@@ -25,12 +25,13 @@ import './video-blacklist'
25import './video-captions' 25import './video-captions'
26import './video-channels' 26import './video-channels'
27import './video-comments' 27import './video-comments'
28import './video-studio' 28import './video-files'
29import './video-imports' 29import './video-imports'
30import './video-playlists' 30import './video-playlists'
31import './videos' 31import './video-source'
32import './video-studio'
32import './videos-common-filters' 33import './videos-common-filters'
33import './video-files'
34import './videos-history' 34import './videos-history'
35import './videos-overviews' 35import './videos-overviews'
36import './videos'
36import './views' 37import './views'
diff --git a/server/tests/api/check-params/video-source.ts b/server/tests/api/check-params/video-source.ts
new file mode 100644
index 000000000..ca324bb9d
--- /dev/null
+++ b/server/tests/api/check-params/video-source.ts
@@ -0,0 +1,44 @@
1import { HttpStatusCode } from '@shared/models'
2import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
3
4describe('Test video sources API validator', function () {
5 let server: PeerTubeServer = null
6 let uuid: string
7 let userToken: string
8
9 before(async function () {
10 this.timeout(30000)
11
12 server = await createSingleServer(1)
13 await setAccessTokensToServers([ server ])
14
15 const created = await server.videos.quickUpload({ name: 'video' })
16 uuid = created.uuid
17
18 userToken = await server.users.generateUserAndToken('user')
19 })
20
21 it('Should fail without a valid uuid', async function () {
22 await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
23 })
24
25 it('Should receive 404 when passing a non existing video id', async function () {
26 await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
27 })
28
29 it('Should not get the source as unauthenticated', async function () {
30 await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null })
31 })
32
33 it('Should not get the source with another user', async function () {
34 await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken })
35 })
36
37 it('Should succeed with the correct parameters get the source as another user', async function () {
38 await server.videos.getSource({ id: uuid })
39 })
40
41 after(async function () {
42 await cleanupTests([ server ])
43 })
44})
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index 41064d2ff..5ff51d1ff 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -39,10 +39,7 @@ describe('Test videos API validator', function () {
39 39
40 await setAccessTokensToServers([ server ]) 40 await setAccessTokensToServers([ server ])
41 41
42 const username = 'user1' 42 userAccessToken = await server.users.generateUserAndToken('user1')
43 const password = 'my super password'
44 await server.users.create({ username: username, password: password })
45 userAccessToken = await server.login.getAccessToken({ username, password })
46 43
47 { 44 {
48 const body = await server.users.getMyInfo() 45 const body = await server.users.getMyInfo()
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index 5d354aad1..2d47c131b 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -610,7 +610,7 @@ describe('Test live', function () {
610 } 610 }
611 611
612 before(async function () { 612 before(async function () {
613 this.timeout(160000) 613 this.timeout(300000)
614 614
615 liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false }) 615 liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false })
616 liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false }) 616 liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false })
@@ -654,7 +654,7 @@ describe('Test live', function () {
654 }) 654 })
655 655
656 it('Should save a non permanent live replay', async function () { 656 it('Should save a non permanent live replay', async function () {
657 this.timeout(120000) 657 this.timeout(240000)
658 658
659 await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) 659 await commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
660 660
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts
index 7bf49c7ec..568fbefcf 100644
--- a/server/tests/api/moderation/abuses.ts
+++ b/server/tests/api/moderation/abuses.ts
@@ -168,7 +168,7 @@ describe('Test abuses', function () {
168 expect(abuse2.reporterAccount.name).to.equal('root') 168 expect(abuse2.reporterAccount.name).to.equal('root')
169 expect(abuse2.reporterAccount.host).to.equal(servers[0].host) 169 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
170 170
171 expect(abuse2.video.id).to.equal(servers[1].store.videoCreated.id) 171 expect(abuse2.video.uuid).to.equal(servers[1].store.videoCreated.uuid)
172 172
173 expect(abuse2.comment).to.be.null 173 expect(abuse2.comment).to.be.null
174 174
@@ -530,7 +530,7 @@ describe('Test abuses', function () {
530 it('Should keep the comment abuse when deleting the comment', async function () { 530 it('Should keep the comment abuse when deleting the comment', async function () {
531 this.timeout(10000) 531 this.timeout(10000)
532 532
533 const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.id) 533 const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.uuid)
534 534
535 await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id }) 535 await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id })
536 536
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index a7cc529f8..a11289236 100644
--- a/server/tests/api/notifications/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -545,7 +545,7 @@ describe('Test user notifications', function () {
545 await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port }) 545 await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port })
546 }) 546 })
547 547
548 // PeerTube does not support accout -> account follows 548 // PeerTube does not support account -> account follows
549 // it('Should notify when a local account is following one of our channel', async function () { 549 // it('Should notify when a local account is following one of our channel', async function () {
550 // this.timeout(50000) 550 // this.timeout(50000)
551 // 551 //
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts
index 4f01f6fd5..d6165b293 100644
--- a/server/tests/api/server/contact-form.ts
+++ b/server/tests/api/server/contact-form.ts
@@ -61,7 +61,7 @@ describe('Test contact form', function () {
61 expect(email['text']).contains('my super message') 61 expect(email['text']).contains('my super message')
62 }) 62 })
63 63
64 it('Should not have duplicated email adress in text message', async function () { 64 it('Should not have duplicated email address in text message', async function () {
65 const text = emails[0]['text'] as string 65 const text = emails[0]['text'] as string
66 66
67 const matches = text.match(/toto@example.com/g) 67 const matches = text.match(/toto@example.com/g)
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts
index fa2063536..0a1565faf 100644
--- a/server/tests/api/server/reverse-proxy.ts
+++ b/server/tests/api/server/reverse-proxy.ts
@@ -7,6 +7,7 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ
7 7
8describe('Test application behind a reverse proxy', function () { 8describe('Test application behind a reverse proxy', function () {
9 let server: PeerTubeServer 9 let server: PeerTubeServer
10 let userAccessToken: string
10 let videoId: string 11 let videoId: string
11 12
12 before(async function () { 13 before(async function () {
@@ -34,6 +35,8 @@ describe('Test application behind a reverse proxy', function () {
34 server = await createSingleServer(1, config) 35 server = await createSingleServer(1, config)
35 await setAccessTokensToServers([ server ]) 36 await setAccessTokensToServers([ server ])
36 37
38 userAccessToken = await server.users.generateUserAndToken('user')
39
37 const { uuid } = await server.videos.upload() 40 const { uuid } = await server.videos.upload()
38 videoId = uuid 41 videoId = uuid
39 }) 42 })
@@ -93,7 +96,7 @@ describe('Test application behind a reverse proxy', function () {
93 it('Should rate limit logins', async function () { 96 it('Should rate limit logins', async function () {
94 const user = { username: 'root', password: 'fail' } 97 const user = { username: 'root', password: 'fail' }
95 98
96 for (let i = 0; i < 19; i++) { 99 for (let i = 0; i < 18; i++) {
97 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 100 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
98 } 101 }
99 102
@@ -141,6 +144,12 @@ describe('Test application behind a reverse proxy', function () {
141 await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) 144 await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
142 }) 145 })
143 146
147 it('Should rate limit API calls with a user but not with an admin', async function () {
148 await server.videos.get({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
149
150 await server.videos.get({ id: videoId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
151 })
152
144 after(async function () { 153 after(async function () {
145 await cleanupTests([ server ]) 154 await cleanupTests([ server ])
146 }) 155 })
diff --git a/server/tests/api/server/services.ts b/server/tests/api/server/services.ts
index 5fd2abda4..2f95f953b 100644
--- a/server/tests/api/server/services.ts
+++ b/server/tests/api/server/services.ts
@@ -13,6 +13,21 @@ describe('Test services', function () {
13 let playlistDisplayName: string 13 let playlistDisplayName: string
14 let video: Video 14 let video: Video
15 15
16 const urlSuffixes = [
17 {
18 input: '',
19 output: ''
20 },
21 {
22 input: '?param=1',
23 output: ''
24 },
25 {
26 input: '?muted=1&warningTitle=0&toto=1',
27 output: '?muted=1&warningTitle=0'
28 }
29 ]
30
16 before(async function () { 31 before(async function () {
17 this.timeout(30000) 32 this.timeout(30000)
18 33
@@ -52,14 +67,15 @@ describe('Test services', function () {
52 67
53 it('Should have a valid oEmbed video response', async function () { 68 it('Should have a valid oEmbed video response', async function () {
54 for (const basePath of [ '/videos/watch/', '/w/' ]) { 69 for (const basePath of [ '/videos/watch/', '/w/' ]) {
55 for (const suffix of [ '', '?param=1' ]) { 70 for (const suffix of urlSuffixes) {
56 const oembedUrl = server.url + basePath + video.uuid + suffix 71 const oembedUrl = server.url + basePath + video.uuid + suffix.input
57 72
58 const res = await server.services.getOEmbed({ oembedUrl }) 73 const res = await server.services.getOEmbed({ oembedUrl })
59 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + 74 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
60 `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + 75 `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}${suffix.output}" ` +
61 'frameborder="0" allowfullscreen></iframe>' 76 'frameborder="0" allowfullscreen></iframe>'
62 const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath 77
78 const expectedThumbnailUrl = 'http://' + server.host + video.previewPath
63 79
64 expect(res.body.html).to.equal(expectedHtml) 80 expect(res.body.html).to.equal(expectedHtml)
65 expect(res.body.title).to.equal(video.name) 81 expect(res.body.title).to.equal(video.name)
@@ -75,12 +91,12 @@ describe('Test services', function () {
75 91
76 it('Should have a valid playlist oEmbed response', async function () { 92 it('Should have a valid playlist oEmbed response', async function () {
77 for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) { 93 for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) {
78 for (const suffix of [ '', '?param=1' ]) { 94 for (const suffix of urlSuffixes) {
79 const oembedUrl = server.url + basePath + playlistUUID + suffix 95 const oembedUrl = server.url + basePath + playlistUUID + suffix.input
80 96
81 const res = await server.services.getOEmbed({ oembedUrl }) 97 const res = await server.services.getOEmbed({ oembedUrl })
82 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' + 98 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
83 `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + 99 `title="${playlistDisplayName}" src="http://${server.host}/video-playlists/embed/${playlistUUID}${suffix.output}" ` +
84 'frameborder="0" allowfullscreen></iframe>' 100 'frameborder="0" allowfullscreen></iframe>'
85 101
86 expect(res.body.html).to.equal(expectedHtml) 102 expect(res.body.html).to.equal(expectedHtml)
@@ -97,14 +113,14 @@ describe('Test services', function () {
97 113
98 it('Should have a valid oEmbed response with small max height query', async function () { 114 it('Should have a valid oEmbed response with small max height query', async function () {
99 for (const basePath of [ '/videos/watch/', '/w/' ]) { 115 for (const basePath of [ '/videos/watch/', '/w/' ]) {
100 const oembedUrl = 'http://localhost:' + server.port + basePath + video.uuid 116 const oembedUrl = 'http://' + server.host + basePath + video.uuid
101 const format = 'json' 117 const format = 'json'
102 const maxHeight = 50 118 const maxHeight = 50
103 const maxWidth = 50 119 const maxWidth = 50
104 120
105 const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth }) 121 const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth })
106 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' + 122 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' +
107 `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + 123 `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}" ` +
108 'frameborder="0" allowfullscreen></iframe>' 124 'frameborder="0" allowfullscreen></iframe>'
109 125
110 expect(res.body.html).to.equal(expectedHtml) 126 expect(res.body.html).to.equal(expectedHtml)
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 01b4c2eab..d15daeba5 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -380,7 +380,7 @@ describe('Test users', function () {
380 }) 380 })
381 381
382 it('Should disable webtorrent, enable HLS, and update my quota', async function () { 382 it('Should disable webtorrent, enable HLS, and update my quota', async function () {
383 this.timeout(60000) 383 this.timeout(160000)
384 384
385 { 385 {
386 const config = await server.config.getCustomConfig() 386 const config = await server.config.getCustomConfig()
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index 27b119f30..a0b6b01cf 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -16,3 +16,4 @@ import './video-schedule-update'
16import './videos-common-filters' 16import './videos-common-filters'
17import './videos-history' 17import './videos-history'
18import './videos-overview' 18import './videos-overview'
19import './video-source'
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 6f495c42d..42e0cf431 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -478,6 +478,25 @@ describe('Test video channels', function () {
478 } 478 }
479 }) 479 })
480 480
481 it('Should report correct total views count', async function () {
482 // check if there's the property
483 {
484 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
485
486 for (const channel of data) {
487 expect(channel).to.haveOwnProperty('totalViews')
488 expect(channel.totalViews).to.be.a('number')
489 }
490 }
491
492 // Check if the totalViews count can be updated
493 {
494 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
495 const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id)
496 expect(channelWithView.totalViews).to.equal(2)
497 }
498 })
499
481 it('Should report correct videos count', async function () { 500 it('Should report correct videos count', async function () {
482 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) 501 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
483 502
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts
index 3051a443d..1073aee8c 100644
--- a/server/tests/api/videos/video-privacy.ts
+++ b/server/tests/api/videos/video-privacy.ts
@@ -162,7 +162,7 @@ describe('Test video privacy', function () {
162 }) 162 })
163 163
164 it('Should not be able to get this unlisted video using its id', async function () { 164 it('Should not be able to get this unlisted video using its id', async function () {
165 await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) 165 await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
166 }) 166 })
167 167
168 it('Should be able to get this unlisted video using its uuid/shortUUID', async function () { 168 it('Should be able to get this unlisted video using its uuid/shortUUID', async function () {
diff --git a/server/tests/api/videos/video-source.ts b/server/tests/api/videos/video-source.ts
new file mode 100644
index 000000000..e34642300
--- /dev/null
+++ b/server/tests/api/videos/video-source.ts
@@ -0,0 +1,39 @@
1import 'mocha'
2import * as chai from 'chai'
3import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
4
5const expect = chai.expect
6
7describe('Test video source', () => {
8 let server: PeerTubeServer = null
9 const fixture = 'video_short.webm'
10
11 before(async function () {
12 this.timeout(30000)
13
14 server = await createSingleServer(1)
15 await setAccessTokensToServers([ server ])
16 })
17
18 it('Should get the source filename with legacy upload', async function () {
19 this.timeout(30000)
20
21 const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' })
22
23 const source = await server.videos.getSource({ id: uuid })
24 expect(source.filename).to.equal(fixture)
25 })
26
27 it('Should get the source filename with resumable upload', async function () {
28 this.timeout(30000)
29
30 const { uuid } = await server.videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' })
31
32 const source = await server.videos.getSource({ id: uuid })
33 expect(source.filename).to.equal(fixture)
34 })
35
36 after(async function () {
37 await cleanupTests([ server ])
38 })
39})
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts
index fa0a71341..e3e5eb45c 100644
--- a/server/tests/helpers/core-utils.ts
+++ b/server/tests/helpers/core-utils.ts
@@ -6,47 +6,64 @@ import { snakeCase } from 'lodash'
6import validator from 'validator' 6import validator from 'validator'
7import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils' 7import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils'
8import { VideoResolution } from '@shared/models' 8import { VideoResolution } from '@shared/models'
9import { objectConverter, parseBytes } from '../../helpers/core-utils' 9import { objectConverter, parseBytes, parseDurationToMs } from '../../helpers/core-utils'
10 10
11const expect = chai.expect 11const expect = chai.expect
12 12
13describe('Parse Bytes', function () { 13describe('Parse Bytes', function () {
14 14
15 it('Should pass when given valid value', async function () { 15 it('Should pass on valid value', async function () {
16 // just return it 16 // just return it
17 expect(parseBytes(1024)).to.be.eq(1024) 17 expect(parseBytes(-1024)).to.equal(-1024)
18 expect(parseBytes(1048576)).to.be.eq(1048576) 18 expect(parseBytes(1024)).to.equal(1024)
19 expect(parseBytes('1024')).to.be.eq(1024) 19 expect(parseBytes(1048576)).to.equal(1048576)
20 expect(parseBytes('1048576')).to.be.eq(1048576) 20 expect(parseBytes('1024')).to.equal(1024)
21 expect(parseBytes('1048576')).to.equal(1048576)
21 22
22 // sizes 23 // sizes
23 expect(parseBytes('1B')).to.be.eq(1024) 24 expect(parseBytes('1B')).to.equal(1024)
24 expect(parseBytes('1MB')).to.be.eq(1048576) 25 expect(parseBytes('1MB')).to.equal(1048576)
25 expect(parseBytes('1GB')).to.be.eq(1073741824) 26 expect(parseBytes('1GB')).to.equal(1073741824)
26 expect(parseBytes('1TB')).to.be.eq(1099511627776) 27 expect(parseBytes('1TB')).to.equal(1099511627776)
27 28
28 expect(parseBytes('5GB')).to.be.eq(5368709120) 29 expect(parseBytes('5GB')).to.equal(5368709120)
29 expect(parseBytes('5TB')).to.be.eq(5497558138880) 30 expect(parseBytes('5TB')).to.equal(5497558138880)
30 31
31 expect(parseBytes('1024B')).to.be.eq(1048576) 32 expect(parseBytes('1024B')).to.equal(1048576)
32 expect(parseBytes('1024MB')).to.be.eq(1073741824) 33 expect(parseBytes('1024MB')).to.equal(1073741824)
33 expect(parseBytes('1024GB')).to.be.eq(1099511627776) 34 expect(parseBytes('1024GB')).to.equal(1099511627776)
34 expect(parseBytes('1024TB')).to.be.eq(1125899906842624) 35 expect(parseBytes('1024TB')).to.equal(1125899906842624)
35 36
36 // with whitespace 37 // with whitespace
37 expect(parseBytes('1 GB')).to.be.eq(1073741824) 38 expect(parseBytes('1 GB')).to.equal(1073741824)
38 expect(parseBytes('1\tGB')).to.be.eq(1073741824) 39 expect(parseBytes('1\tGB')).to.equal(1073741824)
39 40
40 // sum value 41 // sum value
41 expect(parseBytes('1TB 1024MB')).to.be.eq(1100585369600) 42 expect(parseBytes('1TB 1024MB')).to.equal(1100585369600)
42 expect(parseBytes('4GB 1024MB')).to.be.eq(5368709120) 43 expect(parseBytes('4GB 1024MB')).to.equal(5368709120)
43 expect(parseBytes('4TB 1024GB')).to.be.eq(5497558138880) 44 expect(parseBytes('4TB 1024GB')).to.equal(5497558138880)
44 expect(parseBytes('4TB 1024GB 0MB')).to.be.eq(5497558138880) 45 expect(parseBytes('4TB 1024GB 0MB')).to.equal(5497558138880)
45 expect(parseBytes('1024TB 1024GB 1024MB')).to.be.eq(1127000492212224) 46 expect(parseBytes('1024TB 1024GB 1024MB')).to.equal(1127000492212224)
47 })
48
49 it('Should be invalid when given invalid value', async function () {
50 expect(parseBytes('6GB 1GB')).to.equal(6)
51 })
52})
53
54describe('Parse duration', function () {
55
56 it('Should pass when given valid value', async function () {
57 expect(parseDurationToMs(35)).to.equal(35)
58 expect(parseDurationToMs(-35)).to.equal(-35)
59 expect(parseDurationToMs('35 seconds')).to.equal(35 * 1000)
60 expect(parseDurationToMs('1 minute')).to.equal(60 * 1000)
61 expect(parseDurationToMs('1 hour')).to.equal(3600 * 1000)
62 expect(parseDurationToMs('35 hours')).to.equal(3600 * 35 * 1000)
46 }) 63 })
47 64
48 it('Should be invalid when given invalid value', async function () { 65 it('Should be invalid when given invalid value', async function () {
49 expect(parseBytes('6GB 1GB')).to.be.eq(6) 66 expect(parseBytes('35m 5s')).to.equal(35)
50 }) 67 })
51}) 68})
52 69
diff --git a/server/types/express.d.ts b/server/types/express.d.ts
index 7cc13f21d..27e532c31 100644
--- a/server/types/express.d.ts
+++ b/server/types/express.d.ts
@@ -42,6 +42,7 @@ import {
42 MVideoThumbnail 42 MVideoThumbnail
43} from './models' 43} from './models'
44import { Writable } from 'stream' 44import { Writable } from 'stream'
45import { MVideoSource } from './models/video/video-source'
45 46
46declare module 'express' { 47declare module 'express' {
47 export interface Request { 48 export interface Request {
@@ -68,7 +69,7 @@ declare module 'express' {
68 } | UploadFileForCheck[] 69 } | UploadFileForCheck[]
69 70
70 // Upload file with a duration added by our middleware 71 // Upload file with a duration added by our middleware
71 export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size'> & { 72 export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size', 'originalname'> & {
72 duration: number 73 duration: number
73 } 74 }
74 75
@@ -85,6 +86,7 @@ declare module 'express' {
85 duration: number 86 duration: number
86 path: string 87 path: string
87 filename: string 88 filename: string
89 originalname: string
88 } 90 }
89 91
90 // Extends Response with added functions and potential variables passed by middlewares 92 // Extends Response with added functions and potential variables passed by middlewares
@@ -123,6 +125,8 @@ declare module 'express' {
123 125
124 videoShare?: MVideoShareActor 126 videoShare?: MVideoShareActor
125 127
128 videoSource?: MVideoSource
129
126 videoFile?: MVideoFile 130 videoFile?: MVideoFile
127 131
128 videoFileResumable?: EnhancedUploadXFile 132 videoFileResumable?: EnhancedUploadXFile
diff --git a/server/types/models/video/video-source.ts b/server/types/models/video/video-source.ts
new file mode 100644
index 000000000..0948f3b2e
--- /dev/null
+++ b/server/types/models/video/video-source.ts
@@ -0,0 +1,3 @@
1import { VideoSourceModel } from '@server/models/video/video-source'
2
3export type MVideoSource = Omit<VideoSourceModel, 'Video'>