diff options
Diffstat (limited to 'server')
112 files changed, 3046 insertions, 1750 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 1a4e28dc8..31c0a5fbd 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -3,22 +3,18 @@ import * as express from 'express' | |||
3 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' | 3 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' |
4 | import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' | 4 | import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' |
5 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' | 5 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' |
6 | import { buildAnnounceWithVideoAudience, buildDislikeActivity, buildLikeActivity } from '../../lib/activitypub/send' | 6 | import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send' |
7 | import { audiencify, getAudience } from '../../lib/activitypub/audience' | 7 | import { audiencify, getAudience } from '../../lib/activitypub/audience' |
8 | import { buildCreateActivity } from '../../lib/activitypub/send/send-create' | 8 | import { buildCreateActivity } from '../../lib/activitypub/send/send-create' |
9 | import { | 9 | import { |
10 | asyncMiddleware, | 10 | asyncMiddleware, |
11 | videosShareValidator, | ||
12 | executeIfActivityPub, | 11 | executeIfActivityPub, |
13 | localAccountValidator, | 12 | localAccountValidator, |
14 | localVideoChannelValidator, | 13 | localVideoChannelValidator, |
15 | videosCustomGetValidator | 14 | videosCustomGetValidator, |
15 | videosShareValidator | ||
16 | } from '../../middlewares' | 16 | } from '../../middlewares' |
17 | import { | 17 | import { getAccountVideoRateValidator, videoCommentGetValidator, videosGetValidator } from '../../middlewares/validators' |
18 | getAccountVideoRateValidator, | ||
19 | videoCommentGetValidator, | ||
20 | videosGetValidator | ||
21 | } from '../../middlewares/validators' | ||
22 | import { AccountModel } from '../../models/account/account' | 18 | import { AccountModel } from '../../models/account/account' |
23 | import { ActorModel } from '../../models/activitypub/actor' | 19 | import { ActorModel } from '../../models/activitypub/actor' |
24 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 20 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' |
@@ -37,9 +33,10 @@ import { | |||
37 | getVideoSharesActivityPubUrl | 33 | getVideoSharesActivityPubUrl |
38 | } from '../../lib/activitypub' | 34 | } from '../../lib/activitypub' |
39 | import { VideoCaptionModel } from '../../models/video/video-caption' | 35 | import { VideoCaptionModel } from '../../models/video/video-caption' |
40 | import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' | 36 | import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy' |
41 | import { getServerActor } from '../../helpers/utils' | 37 | import { getServerActor } from '../../helpers/utils' |
42 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 38 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
39 | import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike' | ||
43 | 40 | ||
44 | const activityPubClientRouter = express.Router() | 41 | const activityPubClientRouter = express.Router() |
45 | 42 | ||
@@ -66,11 +63,11 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', | |||
66 | 63 | ||
67 | activityPubClientRouter.get('/videos/watch/:id', | 64 | activityPubClientRouter.get('/videos/watch/:id', |
68 | executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), | 65 | executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), |
69 | executeIfActivityPub(asyncMiddleware(videosGetValidator)), | 66 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))), |
70 | executeIfActivityPub(asyncMiddleware(videoController)) | 67 | executeIfActivityPub(asyncMiddleware(videoController)) |
71 | ) | 68 | ) |
72 | activityPubClientRouter.get('/videos/watch/:id/activity', | 69 | activityPubClientRouter.get('/videos/watch/:id/activity', |
73 | executeIfActivityPub(asyncMiddleware(videosGetValidator)), | 70 | executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))), |
74 | executeIfActivityPub(asyncMiddleware(videoController)) | 71 | executeIfActivityPub(asyncMiddleware(videoController)) |
75 | ) | 72 | ) |
76 | activityPubClientRouter.get('/videos/watch/:id/announces', | 73 | activityPubClientRouter.get('/videos/watch/:id/announces', |
@@ -116,7 +113,11 @@ activityPubClientRouter.get('/video-channels/:name/following', | |||
116 | ) | 113 | ) |
117 | 114 | ||
118 | activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', | 115 | activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', |
119 | executeIfActivityPub(asyncMiddleware(videoRedundancyGetValidator)), | 116 | executeIfActivityPub(asyncMiddleware(videoFileRedundancyGetValidator)), |
117 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) | ||
118 | ) | ||
119 | activityPubClientRouter.get('/redundancy/video-playlists/:streamingPlaylistType/:videoId', | ||
120 | executeIfActivityPub(asyncMiddleware(videoPlaylistRedundancyGetValidator)), | ||
120 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) | 121 | executeIfActivityPub(asyncMiddleware(videoRedundancyController)) |
121 | ) | 122 | ) |
122 | 123 | ||
@@ -156,14 +157,15 @@ function getAccountVideoRate (rateType: VideoRateType) { | |||
156 | const url = getRateUrl(rateType, byActor, accountVideoRate.Video) | 157 | const url = getRateUrl(rateType, byActor, accountVideoRate.Video) |
157 | const APObject = rateType === 'like' | 158 | const APObject = rateType === 'like' |
158 | ? buildLikeActivity(url, byActor, accountVideoRate.Video) | 159 | ? buildLikeActivity(url, byActor, accountVideoRate.Video) |
159 | : buildCreateActivity(url, byActor, buildDislikeActivity(url, byActor, accountVideoRate.Video)) | 160 | : buildDislikeActivity(url, byActor, accountVideoRate.Video) |
160 | 161 | ||
161 | return activityPubResponse(activityPubContextify(APObject), res) | 162 | return activityPubResponse(activityPubContextify(APObject), res) |
162 | } | 163 | } |
163 | } | 164 | } |
164 | 165 | ||
165 | async function videoController (req: express.Request, res: express.Response) { | 166 | async function videoController (req: express.Request, res: express.Response) { |
166 | const video: VideoModel = res.locals.video | 167 | // We need more attributes |
168 | const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id) | ||
167 | 169 | ||
168 | if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url) | 170 | if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url) |
169 | 171 | ||
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index a69a83acf..8c0237203 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -14,6 +14,8 @@ import { AccountModel } from '../../models/account/account' | |||
14 | import { VideoModel } from '../../models/video/video' | 14 | import { VideoModel } from '../../models/video/video' |
15 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 15 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
16 | import { VideoChannelModel } from '../../models/video/video-channel' | 16 | import { VideoChannelModel } from '../../models/video/video-channel' |
17 | import { JobQueue } from '../../lib/job-queue' | ||
18 | import { logger } from '../../helpers/logger' | ||
17 | 19 | ||
18 | const accountsRouter = express.Router() | 20 | const accountsRouter = express.Router() |
19 | 21 | ||
@@ -57,6 +59,11 @@ export { | |||
57 | function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { | 59 | function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { |
58 | const account: AccountModel = res.locals.account | 60 | const account: AccountModel = res.locals.account |
59 | 61 | ||
62 | if (account.isOutdated()) { | ||
63 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } }) | ||
64 | .catch(err => logger.error('Cannot create AP refresher job for actor %s.', account.Actor.url, { err })) | ||
65 | } | ||
66 | |||
60 | return res.json(account.toFormattedJSON()) | 67 | return res.json(account.toFormattedJSON()) |
61 | } | 68 | } |
62 | 69 | ||
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index dd06a0597..1f3341bc0 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { omit, snakeCase } from 'lodash' | 2 | import { snakeCase } from 'lodash' |
3 | import { ServerConfig, UserRight } from '../../../shared' | 3 | import { ServerConfig, UserRight } from '../../../shared' |
4 | import { About } from '../../../shared/models/server/about.model' | 4 | import { About } from '../../../shared/models/server/about.model' |
5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
@@ -78,6 +78,9 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
78 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | 78 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION |
79 | }, | 79 | }, |
80 | transcoding: { | 80 | transcoding: { |
81 | hls: { | ||
82 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
83 | }, | ||
81 | enabledResolutions | 84 | enabledResolutions |
82 | }, | 85 | }, |
83 | import: { | 86 | import: { |
@@ -120,6 +123,11 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
120 | user: { | 123 | user: { |
121 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | 124 | videoQuota: CONFIG.USER.VIDEO_QUOTA, |
122 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | 125 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY |
126 | }, | ||
127 | trending: { | ||
128 | videos: { | ||
129 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | ||
130 | } | ||
123 | } | 131 | } |
124 | } | 132 | } |
125 | 133 | ||
@@ -241,6 +249,9 @@ function customConfig (): CustomConfig { | |||
241 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], | 249 | '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], |
242 | '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], | 250 | '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], |
243 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] | 251 | '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] |
252 | }, | ||
253 | hls: { | ||
254 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
244 | } | 255 | } |
245 | }, | 256 | }, |
246 | import: { | 257 | import: { |
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts index 85803f69e..89ffd1717 100644 --- a/server/controllers/api/server/stats.ts +++ b/server/controllers/api/server/stats.ts | |||
@@ -8,6 +8,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment' | |||
8 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 8 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
9 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' | 9 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' |
10 | import { cacheRoute } from '../../../middlewares/cache' | 10 | import { cacheRoute } from '../../../middlewares/cache' |
11 | import { VideoFileModel } from '../../../models/video/video-file' | ||
11 | 12 | ||
12 | const statsRouter = express.Router() | 13 | const statsRouter = express.Router() |
13 | 14 | ||
@@ -16,11 +17,12 @@ statsRouter.get('/stats', | |||
16 | asyncMiddleware(getStats) | 17 | asyncMiddleware(getStats) |
17 | ) | 18 | ) |
18 | 19 | ||
19 | async function getStats (req: express.Request, res: express.Response, next: express.NextFunction) { | 20 | async function getStats (req: express.Request, res: express.Response) { |
20 | const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() | 21 | const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() |
21 | const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() | 22 | const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() |
22 | const { totalUsers } = await UserModel.getStats() | 23 | const { totalUsers } = await UserModel.getStats() |
23 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() | 24 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() |
25 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() | ||
24 | 26 | ||
25 | const videosRedundancyStats = await Promise.all( | 27 | const videosRedundancyStats = await Promise.all( |
26 | CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => { | 28 | CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => { |
@@ -32,8 +34,9 @@ async function getStats (req: express.Request, res: express.Response, next: expr | |||
32 | const data: ServerStats = { | 34 | const data: ServerStats = { |
33 | totalLocalVideos, | 35 | totalLocalVideos, |
34 | totalLocalVideoViews, | 36 | totalLocalVideoViews, |
35 | totalVideos, | 37 | totalLocalVideoFilesSize, |
36 | totalLocalVideoComments, | 38 | totalLocalVideoComments, |
39 | totalVideos, | ||
37 | totalVideoComments, | 40 | totalVideoComments, |
38 | totalUsers, | 41 | totalUsers, |
39 | totalInstanceFollowers, | 42 | totalInstanceFollowers, |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 9e6a019f6..e3533a7f6 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -41,6 +41,7 @@ import { myBlocklistRouter } from './my-blocklist' | |||
41 | import { myVideosHistoryRouter } from './my-history' | 41 | import { myVideosHistoryRouter } from './my-history' |
42 | import { myNotificationsRouter } from './my-notifications' | 42 | import { myNotificationsRouter } from './my-notifications' |
43 | import { Notifier } from '../../../lib/notifier' | 43 | import { Notifier } from '../../../lib/notifier' |
44 | import { mySubscriptionsRouter } from './my-subscriptions' | ||
44 | 45 | ||
45 | const auditLogger = auditLoggerFactory('users') | 46 | const auditLogger = auditLoggerFactory('users') |
46 | 47 | ||
@@ -58,6 +59,7 @@ const askSendEmailLimiter = new RateLimit({ | |||
58 | 59 | ||
59 | const usersRouter = express.Router() | 60 | const usersRouter = express.Router() |
60 | usersRouter.use('/', myNotificationsRouter) | 61 | usersRouter.use('/', myNotificationsRouter) |
62 | usersRouter.use('/', mySubscriptionsRouter) | ||
61 | usersRouter.use('/', myBlocklistRouter) | 63 | usersRouter.use('/', myBlocklistRouter) |
62 | usersRouter.use('/', myVideosHistoryRouter) | 64 | usersRouter.use('/', myVideosHistoryRouter) |
63 | usersRouter.use('/', meRouter) | 65 | usersRouter.use('/', meRouter) |
@@ -227,7 +229,7 @@ async function unblockUser (req: express.Request, res: express.Response, next: e | |||
227 | return res.status(204).end() | 229 | return res.status(204).end() |
228 | } | 230 | } |
229 | 231 | ||
230 | async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 232 | async function blockUser (req: express.Request, res: express.Response) { |
231 | const user: UserModel = res.locals.user | 233 | const user: UserModel = res.locals.user |
232 | const reason = req.body.reason | 234 | const reason = req.body.reason |
233 | 235 | ||
@@ -236,23 +238,23 @@ async function blockUser (req: express.Request, res: express.Response, next: exp | |||
236 | return res.status(204).end() | 238 | return res.status(204).end() |
237 | } | 239 | } |
238 | 240 | ||
239 | function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 241 | function getUser (req: express.Request, res: express.Response) { |
240 | return res.json((res.locals.user as UserModel).toFormattedJSON()) | 242 | return res.json((res.locals.user as UserModel).toFormattedJSON()) |
241 | } | 243 | } |
242 | 244 | ||
243 | async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 245 | async function autocompleteUsers (req: express.Request, res: express.Response) { |
244 | const resultList = await UserModel.autoComplete(req.query.search as string) | 246 | const resultList = await UserModel.autoComplete(req.query.search as string) |
245 | 247 | ||
246 | return res.json(resultList) | 248 | return res.json(resultList) |
247 | } | 249 | } |
248 | 250 | ||
249 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 251 | async function listUsers (req: express.Request, res: express.Response) { |
250 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) | 252 | const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) |
251 | 253 | ||
252 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 254 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
253 | } | 255 | } |
254 | 256 | ||
255 | async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 257 | async function removeUser (req: express.Request, res: express.Response) { |
256 | const user: UserModel = res.locals.user | 258 | const user: UserModel = res.locals.user |
257 | 259 | ||
258 | await user.destroy() | 260 | await user.destroy() |
@@ -262,12 +264,13 @@ async function removeUser (req: express.Request, res: express.Response, next: ex | |||
262 | return res.sendStatus(204) | 264 | return res.sendStatus(204) |
263 | } | 265 | } |
264 | 266 | ||
265 | async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 267 | async function updateUser (req: express.Request, res: express.Response) { |
266 | const body: UserUpdate = req.body | 268 | const body: UserUpdate = req.body |
267 | const userToUpdate = res.locals.user as UserModel | 269 | const userToUpdate = res.locals.user as UserModel |
268 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) | 270 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) |
269 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role | 271 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role |
270 | 272 | ||
273 | if (body.password !== undefined) userToUpdate.password = body.password | ||
271 | if (body.email !== undefined) userToUpdate.email = body.email | 274 | if (body.email !== undefined) userToUpdate.email = body.email |
272 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified | 275 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified |
273 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota | 276 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota |
@@ -277,11 +280,11 @@ async function updateUser (req: express.Request, res: express.Response, next: ex | |||
277 | const user = await userToUpdate.save() | 280 | const user = await userToUpdate.save() |
278 | 281 | ||
279 | // Destroy user token to refresh rights | 282 | // Destroy user token to refresh rights |
280 | if (roleChanged) await deleteUserToken(userToUpdate.id) | 283 | if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) |
281 | 284 | ||
282 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) | 285 | auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) |
283 | 286 | ||
284 | // Don't need to send this update to followers, these attributes are not propagated | 287 | // Don't need to send this update to followers, these attributes are not federated |
285 | 288 | ||
286 | return res.sendStatus(204) | 289 | return res.sendStatus(204) |
287 | } | 290 | } |
@@ -291,7 +294,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response | |||
291 | 294 | ||
292 | const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) | 295 | const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) |
293 | const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString | 296 | const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString |
294 | await Emailer.Instance.addForgetPasswordEmailJob(user.email, url) | 297 | await Emailer.Instance.addPasswordResetEmailJob(user.email, url) |
295 | 298 | ||
296 | return res.status(204).end() | 299 | return res.status(204).end() |
297 | } | 300 | } |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 8a3208160..d5e154869 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -8,36 +8,23 @@ import { | |||
8 | asyncMiddleware, | 8 | asyncMiddleware, |
9 | asyncRetryTransactionMiddleware, | 9 | asyncRetryTransactionMiddleware, |
10 | authenticate, | 10 | authenticate, |
11 | commonVideosFiltersValidator, | ||
12 | paginationValidator, | 11 | paginationValidator, |
13 | setDefaultPagination, | 12 | setDefaultPagination, |
14 | setDefaultSort, | 13 | setDefaultSort, |
15 | userSubscriptionAddValidator, | ||
16 | userSubscriptionGetValidator, | ||
17 | usersUpdateMeValidator, | 14 | usersUpdateMeValidator, |
18 | usersVideoRatingValidator | 15 | usersVideoRatingValidator |
19 | } from '../../../middlewares' | 16 | } from '../../../middlewares' |
20 | import { | 17 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' |
21 | areSubscriptionsExistValidator, | ||
22 | deleteMeValidator, | ||
23 | userSubscriptionsSortValidator, | ||
24 | videoImportsSortValidator, | ||
25 | videosSortValidator | ||
26 | } from '../../../middlewares/validators' | ||
27 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 18 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
28 | import { UserModel } from '../../../models/account/user' | 19 | import { UserModel } from '../../../models/account/user' |
29 | import { VideoModel } from '../../../models/video/video' | 20 | import { VideoModel } from '../../../models/video/video' |
30 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' | 21 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' |
31 | import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils' | 22 | import { createReqFiles } from '../../../helpers/express-utils' |
32 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' | 23 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' |
33 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' | 24 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' |
34 | import { updateActorAvatarFile } from '../../../lib/avatar' | 25 | import { updateActorAvatarFile } from '../../../lib/avatar' |
35 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 26 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
36 | import { VideoImportModel } from '../../../models/video/video-import' | 27 | import { VideoImportModel } from '../../../models/video/video-import' |
37 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
38 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
39 | import { JobQueue } from '../../../lib/job-queue' | ||
40 | import { logger } from '../../../helpers/logger' | ||
41 | import { AccountModel } from '../../../models/account/account' | 28 | import { AccountModel } from '../../../models/account/account' |
42 | 29 | ||
43 | const auditLogger = auditLoggerFactory('users-me') | 30 | const auditLogger = auditLoggerFactory('users-me') |
@@ -98,51 +85,6 @@ meRouter.post('/me/avatar/pick', | |||
98 | asyncRetryTransactionMiddleware(updateMyAvatar) | 85 | asyncRetryTransactionMiddleware(updateMyAvatar) |
99 | ) | 86 | ) |
100 | 87 | ||
101 | // ##### Subscriptions part ##### | ||
102 | |||
103 | meRouter.get('/me/subscriptions/videos', | ||
104 | authenticate, | ||
105 | paginationValidator, | ||
106 | videosSortValidator, | ||
107 | setDefaultSort, | ||
108 | setDefaultPagination, | ||
109 | commonVideosFiltersValidator, | ||
110 | asyncMiddleware(getUserSubscriptionVideos) | ||
111 | ) | ||
112 | |||
113 | meRouter.get('/me/subscriptions/exist', | ||
114 | authenticate, | ||
115 | areSubscriptionsExistValidator, | ||
116 | asyncMiddleware(areSubscriptionsExist) | ||
117 | ) | ||
118 | |||
119 | meRouter.get('/me/subscriptions', | ||
120 | authenticate, | ||
121 | paginationValidator, | ||
122 | userSubscriptionsSortValidator, | ||
123 | setDefaultSort, | ||
124 | setDefaultPagination, | ||
125 | asyncMiddleware(getUserSubscriptions) | ||
126 | ) | ||
127 | |||
128 | meRouter.post('/me/subscriptions', | ||
129 | authenticate, | ||
130 | userSubscriptionAddValidator, | ||
131 | asyncMiddleware(addUserSubscription) | ||
132 | ) | ||
133 | |||
134 | meRouter.get('/me/subscriptions/:uri', | ||
135 | authenticate, | ||
136 | userSubscriptionGetValidator, | ||
137 | getUserSubscription | ||
138 | ) | ||
139 | |||
140 | meRouter.delete('/me/subscriptions/:uri', | ||
141 | authenticate, | ||
142 | userSubscriptionGetValidator, | ||
143 | asyncRetryTransactionMiddleware(deleteUserSubscription) | ||
144 | ) | ||
145 | |||
146 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
147 | 89 | ||
148 | export { | 90 | export { |
@@ -151,100 +93,6 @@ export { | |||
151 | 93 | ||
152 | // --------------------------------------------------------------------------- | 94 | // --------------------------------------------------------------------------- |
153 | 95 | ||
154 | async function areSubscriptionsExist (req: express.Request, res: express.Response) { | ||
155 | const uris = req.query.uris as string[] | ||
156 | const user = res.locals.oauth.token.User as UserModel | ||
157 | |||
158 | const handles = uris.map(u => { | ||
159 | let [ name, host ] = u.split('@') | ||
160 | if (host === CONFIG.WEBSERVER.HOST) host = null | ||
161 | |||
162 | return { name, host, uri: u } | ||
163 | }) | ||
164 | |||
165 | const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles) | ||
166 | |||
167 | const existObject: { [id: string ]: boolean } = {} | ||
168 | for (const handle of handles) { | ||
169 | const obj = results.find(r => { | ||
170 | const server = r.ActorFollowing.Server | ||
171 | |||
172 | return r.ActorFollowing.preferredUsername === handle.name && | ||
173 | ( | ||
174 | (!server && !handle.host) || | ||
175 | (server.host === handle.host) | ||
176 | ) | ||
177 | }) | ||
178 | |||
179 | existObject[handle.uri] = obj !== undefined | ||
180 | } | ||
181 | |||
182 | return res.json(existObject) | ||
183 | } | ||
184 | |||
185 | async function addUserSubscription (req: express.Request, res: express.Response) { | ||
186 | const user = res.locals.oauth.token.User as UserModel | ||
187 | const [ name, host ] = req.body.uri.split('@') | ||
188 | |||
189 | const payload = { | ||
190 | name, | ||
191 | host, | ||
192 | followerActorId: user.Account.Actor.id | ||
193 | } | ||
194 | |||
195 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | ||
196 | .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err)) | ||
197 | |||
198 | return res.status(204).end() | ||
199 | } | ||
200 | |||
201 | function getUserSubscription (req: express.Request, res: express.Response) { | ||
202 | const subscription: ActorFollowModel = res.locals.subscription | ||
203 | |||
204 | return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) | ||
205 | } | ||
206 | |||
207 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | ||
208 | const subscription: ActorFollowModel = res.locals.subscription | ||
209 | |||
210 | await sequelizeTypescript.transaction(async t => { | ||
211 | return subscription.destroy({ transaction: t }) | ||
212 | }) | ||
213 | |||
214 | return res.type('json').status(204).end() | ||
215 | } | ||
216 | |||
217 | async function getUserSubscriptions (req: express.Request, res: express.Response) { | ||
218 | const user = res.locals.oauth.token.User as UserModel | ||
219 | const actorId = user.Account.Actor.id | ||
220 | |||
221 | const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) | ||
222 | |||
223 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
224 | } | ||
225 | |||
226 | async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
227 | const user = res.locals.oauth.token.User as UserModel | ||
228 | const resultList = await VideoModel.listForApi({ | ||
229 | start: req.query.start, | ||
230 | count: req.query.count, | ||
231 | sort: req.query.sort, | ||
232 | includeLocalVideos: false, | ||
233 | categoryOneOf: req.query.categoryOneOf, | ||
234 | licenceOneOf: req.query.licenceOneOf, | ||
235 | languageOneOf: req.query.languageOneOf, | ||
236 | tagsOneOf: req.query.tagsOneOf, | ||
237 | tagsAllOf: req.query.tagsAllOf, | ||
238 | nsfw: buildNSFWFilter(res, req.query.nsfw), | ||
239 | filter: req.query.filter as VideoFilter, | ||
240 | withFiles: false, | ||
241 | followerActorId: user.Account.Actor.id, | ||
242 | user | ||
243 | }) | ||
244 | |||
245 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
246 | } | ||
247 | |||
248 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 96 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
249 | const user = res.locals.oauth.token.User as UserModel | 97 | const user = res.locals.oauth.token.User as UserModel |
250 | const resultList = await VideoModel.listUserVideosForApi( | 98 | const resultList = await VideoModel.listUserVideosForApi( |
@@ -319,7 +167,7 @@ async function deleteMe (req: express.Request, res: express.Response) { | |||
319 | return res.sendStatus(204) | 167 | return res.sendStatus(204) |
320 | } | 168 | } |
321 | 169 | ||
322 | async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { | 170 | async function updateMe (req: express.Request, res: express.Response) { |
323 | const body: UserUpdateMe = req.body | 171 | const body: UserUpdateMe = req.body |
324 | 172 | ||
325 | const user: UserModel = res.locals.oauth.token.user | 173 | const user: UserModel = res.locals.oauth.token.user |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts new file mode 100644 index 000000000..accca6d52 --- /dev/null +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -0,0 +1,170 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { getFormattedObjects } from '../../../helpers/utils' | ||
4 | import { CONFIG, sequelizeTypescript } from '../../../initializers' | ||
5 | import { | ||
6 | asyncMiddleware, | ||
7 | asyncRetryTransactionMiddleware, | ||
8 | authenticate, | ||
9 | commonVideosFiltersValidator, | ||
10 | paginationValidator, | ||
11 | setDefaultPagination, | ||
12 | setDefaultSort, | ||
13 | userSubscriptionAddValidator, | ||
14 | userSubscriptionGetValidator | ||
15 | } from '../../../middlewares' | ||
16 | import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators' | ||
17 | import { UserModel } from '../../../models/account/user' | ||
18 | import { VideoModel } from '../../../models/video/video' | ||
19 | import { buildNSFWFilter } from '../../../helpers/express-utils' | ||
20 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
21 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
22 | import { JobQueue } from '../../../lib/job-queue' | ||
23 | import { logger } from '../../../helpers/logger' | ||
24 | |||
25 | const mySubscriptionsRouter = express.Router() | ||
26 | |||
27 | mySubscriptionsRouter.get('/me/subscriptions/videos', | ||
28 | authenticate, | ||
29 | paginationValidator, | ||
30 | videosSortValidator, | ||
31 | setDefaultSort, | ||
32 | setDefaultPagination, | ||
33 | commonVideosFiltersValidator, | ||
34 | asyncMiddleware(getUserSubscriptionVideos) | ||
35 | ) | ||
36 | |||
37 | mySubscriptionsRouter.get('/me/subscriptions/exist', | ||
38 | authenticate, | ||
39 | areSubscriptionsExistValidator, | ||
40 | asyncMiddleware(areSubscriptionsExist) | ||
41 | ) | ||
42 | |||
43 | mySubscriptionsRouter.get('/me/subscriptions', | ||
44 | authenticate, | ||
45 | paginationValidator, | ||
46 | userSubscriptionsSortValidator, | ||
47 | setDefaultSort, | ||
48 | setDefaultPagination, | ||
49 | asyncMiddleware(getUserSubscriptions) | ||
50 | ) | ||
51 | |||
52 | mySubscriptionsRouter.post('/me/subscriptions', | ||
53 | authenticate, | ||
54 | userSubscriptionAddValidator, | ||
55 | asyncMiddleware(addUserSubscription) | ||
56 | ) | ||
57 | |||
58 | mySubscriptionsRouter.get('/me/subscriptions/:uri', | ||
59 | authenticate, | ||
60 | userSubscriptionGetValidator, | ||
61 | getUserSubscription | ||
62 | ) | ||
63 | |||
64 | mySubscriptionsRouter.delete('/me/subscriptions/:uri', | ||
65 | authenticate, | ||
66 | userSubscriptionGetValidator, | ||
67 | asyncRetryTransactionMiddleware(deleteUserSubscription) | ||
68 | ) | ||
69 | |||
70 | // --------------------------------------------------------------------------- | ||
71 | |||
72 | export { | ||
73 | mySubscriptionsRouter | ||
74 | } | ||
75 | |||
76 | // --------------------------------------------------------------------------- | ||
77 | |||
78 | async function areSubscriptionsExist (req: express.Request, res: express.Response) { | ||
79 | const uris = req.query.uris as string[] | ||
80 | const user = res.locals.oauth.token.User as UserModel | ||
81 | |||
82 | const handles = uris.map(u => { | ||
83 | let [ name, host ] = u.split('@') | ||
84 | if (host === CONFIG.WEBSERVER.HOST) host = null | ||
85 | |||
86 | return { name, host, uri: u } | ||
87 | }) | ||
88 | |||
89 | const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles) | ||
90 | |||
91 | const existObject: { [id: string ]: boolean } = {} | ||
92 | for (const handle of handles) { | ||
93 | const obj = results.find(r => { | ||
94 | const server = r.ActorFollowing.Server | ||
95 | |||
96 | return r.ActorFollowing.preferredUsername === handle.name && | ||
97 | ( | ||
98 | (!server && !handle.host) || | ||
99 | (server.host === handle.host) | ||
100 | ) | ||
101 | }) | ||
102 | |||
103 | existObject[handle.uri] = obj !== undefined | ||
104 | } | ||
105 | |||
106 | return res.json(existObject) | ||
107 | } | ||
108 | |||
109 | async function addUserSubscription (req: express.Request, res: express.Response) { | ||
110 | const user = res.locals.oauth.token.User as UserModel | ||
111 | const [ name, host ] = req.body.uri.split('@') | ||
112 | |||
113 | const payload = { | ||
114 | name, | ||
115 | host, | ||
116 | followerActorId: user.Account.Actor.id | ||
117 | } | ||
118 | |||
119 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | ||
120 | .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err)) | ||
121 | |||
122 | return res.status(204).end() | ||
123 | } | ||
124 | |||
125 | function getUserSubscription (req: express.Request, res: express.Response) { | ||
126 | const subscription: ActorFollowModel = res.locals.subscription | ||
127 | |||
128 | return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) | ||
129 | } | ||
130 | |||
131 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | ||
132 | const subscription: ActorFollowModel = res.locals.subscription | ||
133 | |||
134 | await sequelizeTypescript.transaction(async t => { | ||
135 | return subscription.destroy({ transaction: t }) | ||
136 | }) | ||
137 | |||
138 | return res.type('json').status(204).end() | ||
139 | } | ||
140 | |||
141 | async function getUserSubscriptions (req: express.Request, res: express.Response) { | ||
142 | const user = res.locals.oauth.token.User as UserModel | ||
143 | const actorId = user.Account.Actor.id | ||
144 | |||
145 | const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) | ||
146 | |||
147 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
148 | } | ||
149 | |||
150 | async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
151 | const user = res.locals.oauth.token.User as UserModel | ||
152 | const resultList = await VideoModel.listForApi({ | ||
153 | start: req.query.start, | ||
154 | count: req.query.count, | ||
155 | sort: req.query.sort, | ||
156 | includeLocalVideos: false, | ||
157 | categoryOneOf: req.query.categoryOneOf, | ||
158 | licenceOneOf: req.query.licenceOneOf, | ||
159 | languageOneOf: req.query.languageOneOf, | ||
160 | tagsOneOf: req.query.tagsOneOf, | ||
161 | tagsAllOf: req.query.tagsAllOf, | ||
162 | nsfw: buildNSFWFilter(res, req.query.nsfw), | ||
163 | filter: req.query.filter as VideoFilter, | ||
164 | withFiles: false, | ||
165 | followerActorId: user.Account.Actor.id, | ||
166 | user | ||
167 | }) | ||
168 | |||
169 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
170 | } | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 3d6a6af7f..db7602139 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -30,6 +30,7 @@ import { updateActorAvatarFile } from '../../lib/avatar' | |||
30 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 30 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
31 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 31 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
32 | import { UserModel } from '../../models/account/user' | 32 | import { UserModel } from '../../models/account/user' |
33 | import { JobQueue } from '../../lib/job-queue' | ||
33 | 34 | ||
34 | const auditLogger = auditLoggerFactory('channels') | 35 | const auditLogger = auditLoggerFactory('channels') |
35 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) | 36 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
@@ -197,6 +198,11 @@ async function removeVideoChannel (req: express.Request, res: express.Response) | |||
197 | async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { | 198 | async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { |
198 | const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) | 199 | const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) |
199 | 200 | ||
201 | if (videoChannelWithVideos.isOutdated()) { | ||
202 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) | ||
203 | .catch(err => logger.error('Cannot create AP refresher job for actor %s.', videoChannelWithVideos.Actor.url, { err })) | ||
204 | } | ||
205 | |||
200 | return res.json(videoChannelWithVideos.toFormattedJSON()) | 206 | return res.json(videoChannelWithVideos.toFormattedJSON()) |
201 | } | 207 | } |
202 | 208 | ||
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index fe0a95cd5..32f9c4793 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts | |||
@@ -3,7 +3,6 @@ import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared | |||
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { getFormattedObjects } from '../../../helpers/utils' | 4 | import { getFormattedObjects } from '../../../helpers/utils' |
5 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers' |
6 | import { sendVideoAbuse } from '../../../lib/activitypub/send' | ||
7 | import { | 6 | import { |
8 | asyncMiddleware, | 7 | asyncMiddleware, |
9 | asyncRetryTransactionMiddleware, | 8 | asyncRetryTransactionMiddleware, |
@@ -23,6 +22,7 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse' | |||
23 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' | 22 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' |
24 | import { UserModel } from '../../../models/account/user' | 23 | import { UserModel } from '../../../models/account/user' |
25 | import { Notifier } from '../../../lib/notifier' | 24 | import { Notifier } from '../../../lib/notifier' |
25 | import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag' | ||
26 | 26 | ||
27 | const auditLogger = auditLoggerFactory('abuse') | 27 | const auditLogger = auditLoggerFactory('abuse') |
28 | const abuseVideoRouter = express.Router() | 28 | const abuseVideoRouter = express.Router() |
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts index 9ef08812b..43b0516e7 100644 --- a/server/controllers/api/videos/blacklist.ts +++ b/server/controllers/api/videos/blacklist.ts | |||
@@ -18,6 +18,8 @@ import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | |||
18 | import { sequelizeTypescript } from '../../../initializers' | 18 | import { sequelizeTypescript } from '../../../initializers' |
19 | import { Notifier } from '../../../lib/notifier' | 19 | import { Notifier } from '../../../lib/notifier' |
20 | import { VideoModel } from '../../../models/video/video' | 20 | import { VideoModel } from '../../../models/video/video' |
21 | import { sendCreateVideo, sendDeleteVideo, sendUpdateVideo } from '../../../lib/activitypub/send' | ||
22 | import { federateVideoIfNeeded } from '../../../lib/activitypub' | ||
21 | 23 | ||
22 | const blacklistRouter = express.Router() | 24 | const blacklistRouter = express.Router() |
23 | 25 | ||
@@ -66,12 +68,17 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response) | |||
66 | 68 | ||
67 | const toCreate = { | 69 | const toCreate = { |
68 | videoId: videoInstance.id, | 70 | videoId: videoInstance.id, |
71 | unfederated: body.unfederate === true, | ||
69 | reason: body.reason | 72 | reason: body.reason |
70 | } | 73 | } |
71 | 74 | ||
72 | const blacklist = await VideoBlacklistModel.create(toCreate) | 75 | const blacklist = await VideoBlacklistModel.create(toCreate) |
73 | blacklist.Video = videoInstance | 76 | blacklist.Video = videoInstance |
74 | 77 | ||
78 | if (body.unfederate === true) { | ||
79 | await sendDeleteVideo(videoInstance, undefined) | ||
80 | } | ||
81 | |||
75 | Notifier.Instance.notifyOnVideoBlacklist(blacklist) | 82 | Notifier.Instance.notifyOnVideoBlacklist(blacklist) |
76 | 83 | ||
77 | logger.info('Video %s blacklisted.', res.locals.video.uuid) | 84 | logger.info('Video %s blacklisted.', res.locals.video.uuid) |
@@ -101,8 +108,14 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex | |||
101 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel | 108 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel |
102 | const video: VideoModel = res.locals.video | 109 | const video: VideoModel = res.locals.video |
103 | 110 | ||
104 | await sequelizeTypescript.transaction(t => { | 111 | await sequelizeTypescript.transaction(async t => { |
105 | return videoBlacklist.destroy({ transaction: t }) | 112 | const unfederated = videoBlacklist.unfederated |
113 | await videoBlacklist.destroy({ transaction: t }) | ||
114 | |||
115 | // Re federate the video | ||
116 | if (unfederated === true) { | ||
117 | await federateVideoIfNeeded(video, true, t) | ||
118 | } | ||
106 | }) | 119 | }) |
107 | 120 | ||
108 | Notifier.Instance.notifyOnVideoUnblacklist(video) | 121 | Notifier.Instance.notifyOnVideoUnblacklist(video) |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 98366cd82..7053d5253 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -164,6 +164,7 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You | |||
164 | licence: body.licence || importData.licence, | 164 | licence: body.licence || importData.licence, |
165 | language: body.language || undefined, | 165 | language: body.language || undefined, |
166 | commentsEnabled: body.commentsEnabled || true, | 166 | commentsEnabled: body.commentsEnabled || true, |
167 | downloadEnabled: body.downloadEnabled || true, | ||
167 | waitTranscoding: body.waitTranscoding || false, | 168 | waitTranscoding: body.waitTranscoding || false, |
168 | state: VideoState.TO_IMPORT, | 169 | state: VideoState.TO_IMPORT, |
169 | nsfw: body.nsfw || importData.nsfw || false, | 170 | nsfw: body.nsfw || importData.nsfw || false, |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index b26dcabe1..6ac13e6a4 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -23,7 +23,6 @@ import { | |||
23 | fetchRemoteVideoDescription, | 23 | fetchRemoteVideoDescription, |
24 | getVideoActivityPubUrl | 24 | getVideoActivityPubUrl |
25 | } from '../../../lib/activitypub' | 25 | } from '../../../lib/activitypub' |
26 | import { sendCreateView } from '../../../lib/activitypub/send' | ||
27 | import { JobQueue } from '../../../lib/job-queue' | 26 | import { JobQueue } from '../../../lib/job-queue' |
28 | import { Redis } from '../../../lib/redis' | 27 | import { Redis } from '../../../lib/redis' |
29 | import { | 28 | import { |
@@ -37,6 +36,7 @@ import { | |||
37 | setDefaultPagination, | 36 | setDefaultPagination, |
38 | setDefaultSort, | 37 | setDefaultSort, |
39 | videosAddValidator, | 38 | videosAddValidator, |
39 | videosCustomGetValidator, | ||
40 | videosGetValidator, | 40 | videosGetValidator, |
41 | videosRemoveValidator, | 41 | videosRemoveValidator, |
42 | videosSortValidator, | 42 | videosSortValidator, |
@@ -59,6 +59,7 @@ import { resetSequelizeInstance } from '../../../helpers/database-utils' | |||
59 | import { move } from 'fs-extra' | 59 | import { move } from 'fs-extra' |
60 | import { watchingRouter } from './watching' | 60 | import { watchingRouter } from './watching' |
61 | import { Notifier } from '../../../lib/notifier' | 61 | import { Notifier } from '../../../lib/notifier' |
62 | import { sendView } from '../../../lib/activitypub/send/send-view' | ||
62 | 63 | ||
63 | const auditLogger = auditLoggerFactory('videos') | 64 | const auditLogger = auditLoggerFactory('videos') |
64 | const videosRouter = express.Router() | 65 | const videosRouter = express.Router() |
@@ -123,9 +124,9 @@ videosRouter.get('/:id/description', | |||
123 | ) | 124 | ) |
124 | videosRouter.get('/:id', | 125 | videosRouter.get('/:id', |
125 | optionalAuthenticate, | 126 | optionalAuthenticate, |
126 | asyncMiddleware(videosGetValidator), | 127 | asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), |
127 | asyncMiddleware(checkVideoFollowConstraints), | 128 | asyncMiddleware(checkVideoFollowConstraints), |
128 | getVideo | 129 | asyncMiddleware(getVideo) |
129 | ) | 130 | ) |
130 | videosRouter.post('/:id/views', | 131 | videosRouter.post('/:id/views', |
131 | asyncMiddleware(videosGetValidator), | 132 | asyncMiddleware(videosGetValidator), |
@@ -181,6 +182,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
181 | licence: videoInfo.licence, | 182 | licence: videoInfo.licence, |
182 | language: videoInfo.language, | 183 | language: videoInfo.language, |
183 | commentsEnabled: videoInfo.commentsEnabled || false, | 184 | commentsEnabled: videoInfo.commentsEnabled || false, |
185 | downloadEnabled: videoInfo.downloadEnabled || true, | ||
184 | waitTranscoding: videoInfo.waitTranscoding || false, | 186 | waitTranscoding: videoInfo.waitTranscoding || false, |
185 | state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED, | 187 | state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED, |
186 | nsfw: videoInfo.nsfw || false, | 188 | nsfw: videoInfo.nsfw || false, |
@@ -326,8 +328,9 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
326 | if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support) | 328 | if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support) |
327 | if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) | 329 | if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) |
328 | if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled) | 330 | if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled) |
329 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && | 331 | if (videoInfoToUpdate.downloadEnabled !== undefined) videoInstance.set('downloadEnabled', videoInfoToUpdate.downloadEnabled) |
330 | videoInfoToUpdate.originallyPublishedAt !== null) { | 332 | |
333 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { | ||
331 | videoInstance.set('originallyPublishedAt', videoInfoToUpdate.originallyPublishedAt) | 334 | videoInstance.set('originallyPublishedAt', videoInfoToUpdate.originallyPublishedAt) |
332 | } | 335 | } |
333 | 336 | ||
@@ -370,7 +373,11 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
370 | } | 373 | } |
371 | 374 | ||
372 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE | 375 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE |
373 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | 376 | |
377 | // Don't send update if the video was unfederated | ||
378 | if (!videoInstanceUpdated.VideoBlacklist || videoInstanceUpdated.VideoBlacklist.unfederated === false) { | ||
379 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | ||
380 | } | ||
374 | 381 | ||
375 | auditLogger.update( | 382 | auditLogger.update( |
376 | getAuditIdFromRes(res), | 383 | getAuditIdFromRes(res), |
@@ -397,15 +404,17 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
397 | return res.type('json').status(204).end() | 404 | return res.type('json').status(204).end() |
398 | } | 405 | } |
399 | 406 | ||
400 | function getVideo (req: express.Request, res: express.Response) { | 407 | async function getVideo (req: express.Request, res: express.Response) { |
401 | const videoInstance = res.locals.video | 408 | // We need more attributes |
409 | const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null | ||
410 | const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId) | ||
402 | 411 | ||
403 | if (videoInstance.isOutdated()) { | 412 | if (video.isOutdated()) { |
404 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoInstance.url } }) | 413 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) |
405 | .catch(err => logger.error('Cannot create AP refresher job for video %s.', videoInstance.url, { err })) | 414 | .catch(err => logger.error('Cannot create AP refresher job for video %s.', video.url, { err })) |
406 | } | 415 | } |
407 | 416 | ||
408 | return res.json(videoInstance.toFormattedDetailsJSON()) | 417 | return res.json(video.toFormattedDetailsJSON()) |
409 | } | 418 | } |
410 | 419 | ||
411 | async function viewVideo (req: express.Request, res: express.Response) { | 420 | async function viewVideo (req: express.Request, res: express.Response) { |
@@ -424,7 +433,7 @@ async function viewVideo (req: express.Request, res: express.Response) { | |||
424 | ]) | 433 | ]) |
425 | 434 | ||
426 | const serverActor = await getServerActor() | 435 | const serverActor = await getServerActor() |
427 | await sendCreateView(serverActor, videoInstance, undefined) | 436 | await sendView(serverActor, videoInstance, undefined) |
428 | 437 | ||
429 | return res.status(204).end() | 438 | return res.status(204).end() |
430 | } | 439 | } |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 4fd58f70c..b21f9da00 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { CONFIG, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' | 3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' |
4 | import { VideosPreviewCache } from '../lib/cache' | 4 | import { VideosPreviewCache } from '../lib/cache' |
5 | import { cacheRoute } from '../middlewares/cache' | 5 | import { cacheRoute } from '../middlewares/cache' |
6 | import { asyncMiddleware, videosGetValidator } from '../middlewares' | 6 | import { asyncMiddleware, videosGetValidator } from '../middlewares' |
@@ -51,6 +51,13 @@ staticRouter.use( | |||
51 | asyncMiddleware(downloadVideoFile) | 51 | asyncMiddleware(downloadVideoFile) |
52 | ) | 52 | ) |
53 | 53 | ||
54 | // HLS | ||
55 | staticRouter.use( | ||
56 | STATIC_PATHS.PLAYLISTS.HLS, | ||
57 | cors(), | ||
58 | express.static(HLS_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist | ||
59 | ) | ||
60 | |||
54 | // Thumbnails path for express | 61 | // Thumbnails path for express |
55 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR | 62 | const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR |
56 | staticRouter.use( | 63 | staticRouter.use( |
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index 1deb8c402..8b77d9de7 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts | |||
@@ -7,6 +7,7 @@ import { Server as WebSocketServer } from 'ws' | |||
7 | import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' | 7 | import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' |
8 | import { VideoFileModel } from '../models/video/video-file' | 8 | import { VideoFileModel } from '../models/video/video-file' |
9 | import { parse } from 'url' | 9 | import { parse } from 'url' |
10 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
10 | 11 | ||
11 | const TrackerServer = bitTorrentTracker.Server | 12 | const TrackerServer = bitTorrentTracker.Server |
12 | 13 | ||
@@ -21,7 +22,7 @@ const trackerServer = new TrackerServer({ | |||
21 | udp: false, | 22 | udp: false, |
22 | ws: false, | 23 | ws: false, |
23 | dht: false, | 24 | dht: false, |
24 | filter: function (infoHash, params, cb) { | 25 | filter: async function (infoHash, params, cb) { |
25 | let ip: string | 26 | let ip: string |
26 | 27 | ||
27 | if (params.type === 'ws') { | 28 | if (params.type === 'ws') { |
@@ -32,19 +33,25 @@ const trackerServer = new TrackerServer({ | |||
32 | 33 | ||
33 | const key = ip + '-' + infoHash | 34 | const key = ip + '-' + infoHash |
34 | 35 | ||
35 | peersIps[ip] = peersIps[ip] ? peersIps[ip] + 1 : 1 | 36 | peersIps[ ip ] = peersIps[ ip ] ? peersIps[ ip ] + 1 : 1 |
36 | peersIpInfoHash[key] = peersIpInfoHash[key] ? peersIpInfoHash[key] + 1 : 1 | 37 | peersIpInfoHash[ key ] = peersIpInfoHash[ key ] ? peersIpInfoHash[ key ] + 1 : 1 |
37 | 38 | ||
38 | if (peersIpInfoHash[key] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { | 39 | if (peersIpInfoHash[ key ] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { |
39 | return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) | 40 | return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) |
40 | } | 41 | } |
41 | 42 | ||
42 | VideoFileModel.isInfohashExists(infoHash) | 43 | try { |
43 | .then(exists => { | 44 | const videoFileExists = await VideoFileModel.doesInfohashExist(infoHash) |
44 | if (exists === false) return cb(new Error(`Unknown infoHash ${infoHash}`)) | 45 | if (videoFileExists === true) return cb() |
45 | 46 | ||
46 | return cb() | 47 | const playlistExists = await VideoStreamingPlaylistModel.doesInfohashExist(infoHash) |
47 | }) | 48 | if (playlistExists === true) return cb() |
49 | |||
50 | return cb(new Error(`Unknown infoHash ${infoHash}`)) | ||
51 | } catch (err) { | ||
52 | logger.error('Error in tracker filter.', { err }) | ||
53 | return cb(err) | ||
54 | } | ||
48 | } | 55 | } |
49 | }) | 56 | }) |
50 | 57 | ||
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 79b76fa0b..62d78373e 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -15,7 +15,7 @@ function activityPubContextify <T> (data: T) { | |||
15 | 'https://w3id.org/security/v1', | 15 | 'https://w3id.org/security/v1', |
16 | { | 16 | { |
17 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', | 17 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', |
18 | pt: 'https://joinpeertube.org/ns', | 18 | pt: 'https://joinpeertube.org/ns#', |
19 | sc: 'http://schema.org#', | 19 | sc: 'http://schema.org#', |
20 | Hashtag: 'as:Hashtag', | 20 | Hashtag: 'as:Hashtag', |
21 | uuid: 'sc:identifier', | 21 | uuid: 'sc:identifier', |
@@ -29,10 +29,12 @@ function activityPubContextify <T> (data: T) { | |||
29 | size: 'sc:Number', | 29 | size: 'sc:Number', |
30 | fps: 'sc:Number', | 30 | fps: 'sc:Number', |
31 | commentsEnabled: 'sc:Boolean', | 31 | commentsEnabled: 'sc:Boolean', |
32 | downloadEnabled: 'sc:Boolean', | ||
32 | waitTranscoding: 'sc:Boolean', | 33 | waitTranscoding: 'sc:Boolean', |
33 | expires: 'sc:expires', | 34 | expires: 'sc:expires', |
34 | support: 'sc:Text', | 35 | support: 'sc:Text', |
35 | CacheFile: 'pt:CacheFile' | 36 | CacheFile: 'pt:CacheFile', |
37 | Infohash: 'pt:Infohash' | ||
36 | }, | 38 | }, |
37 | { | 39 | { |
38 | likes: { | 40 | likes: { |
@@ -106,7 +108,7 @@ function buildSignedActivity (byActor: ActorModel, data: Object) { | |||
106 | return signJsonLDObject(byActor, activity) as Promise<Activity> | 108 | return signJsonLDObject(byActor, activity) as Promise<Activity> |
107 | } | 109 | } |
108 | 110 | ||
109 | function getAPUrl (activity: string | { id: string }) { | 111 | function getAPId (activity: string | { id: string }) { |
110 | if (typeof activity === 'string') return activity | 112 | if (typeof activity === 'string') return activity |
111 | 113 | ||
112 | return activity.id | 114 | return activity.id |
@@ -123,7 +125,7 @@ function checkUrlsSameHost (url1: string, url2: string) { | |||
123 | 125 | ||
124 | export { | 126 | export { |
125 | checkUrlsSameHost, | 127 | checkUrlsSameHost, |
126 | getAPUrl, | 128 | getAPId, |
127 | activityPubContextify, | 129 | activityPubContextify, |
128 | activityPubCollectionPagination, | 130 | activityPubCollectionPagination, |
129 | buildSignedActivity | 131 | buildSignedActivity |
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index 00311fce1..a121f0b8a 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts | |||
@@ -117,7 +117,8 @@ const videoKeysToKeep = [ | |||
117 | 'channel-uuid', | 117 | 'channel-uuid', |
118 | 'channel-name', | 118 | 'channel-name', |
119 | 'support', | 119 | 'support', |
120 | 'commentsEnabled' | 120 | 'commentsEnabled', |
121 | 'downloadEnabled' | ||
121 | ] | 122 | ] |
122 | class VideoAuditView extends EntityAuditView { | 123 | class VideoAuditView extends EntityAuditView { |
123 | constructor (private video: VideoDetails) { | 124 | constructor (private video: VideoDetails) { |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 3fb824e36..f38b82d97 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -193,10 +193,14 @@ function peertubeTruncate (str: string, maxLength: number) { | |||
193 | return truncate(str, options) | 193 | return truncate(str, options) |
194 | } | 194 | } |
195 | 195 | ||
196 | function sha256 (str: string, encoding: HexBase64Latin1Encoding = 'hex') { | 196 | function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { |
197 | return createHash('sha256').update(str).digest(encoding) | 197 | return createHash('sha256').update(str).digest(encoding) |
198 | } | 198 | } |
199 | 199 | ||
200 | function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { | ||
201 | return createHash('sha1').update(str).digest(encoding) | ||
202 | } | ||
203 | |||
200 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | 204 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { |
201 | return function promisified (): Promise<A> { | 205 | return function promisified (): Promise<A> { |
202 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | 206 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { |
@@ -262,7 +266,9 @@ export { | |||
262 | sanitizeHost, | 266 | sanitizeHost, |
263 | buildPath, | 267 | buildPath, |
264 | peertubeTruncate, | 268 | peertubeTruncate, |
269 | |||
265 | sha256, | 270 | sha256, |
271 | sha1, | ||
266 | 272 | ||
267 | promisify0, | 273 | promisify0, |
268 | promisify1, | 274 | promisify1, |
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 2562ead9b..b24590d9d 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -1,26 +1,14 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
3 | import { | 3 | import { sanitizeAndCheckActorObject } from './actor' |
4 | isActorAcceptActivityValid, | 4 | import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' |
5 | isActorDeleteActivityValid, | 5 | import { isDislikeActivityValid } from './rate' |
6 | isActorFollowActivityValid, | 6 | import { sanitizeAndCheckVideoCommentObject } from './video-comments' |
7 | isActorRejectActivityValid, | 7 | import { sanitizeAndCheckVideoTorrentObject } from './videos' |
8 | isActorUpdateActivityValid | ||
9 | } from './actor' | ||
10 | import { isAnnounceActivityValid } from './announce' | ||
11 | import { isActivityPubUrlValid } from './misc' | ||
12 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
13 | import { isUndoActivityValid } from './undo' | ||
14 | import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' | ||
15 | import { | ||
16 | isVideoFlagValid, | ||
17 | isVideoTorrentDeleteActivityValid, | ||
18 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
19 | sanitizeAndCheckVideoTorrentUpdateActivity | ||
20 | } from './videos' | ||
21 | import { isViewActivityValid } from './view' | 8 | import { isViewActivityValid } from './view' |
22 | import { exists } from '../misc' | 9 | import { exists } from '../misc' |
23 | import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file' | 10 | import { isCacheFileObjectValid } from './cache-file' |
11 | import { isFlagActivityValid } from './flag' | ||
24 | 12 | ||
25 | function isRootActivityValid (activity: any) { | 13 | function isRootActivityValid (activity: any) { |
26 | return Array.isArray(activity['@context']) && ( | 14 | return Array.isArray(activity['@context']) && ( |
@@ -46,7 +34,10 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean | |||
46 | Reject: checkRejectActivity, | 34 | Reject: checkRejectActivity, |
47 | Announce: checkAnnounceActivity, | 35 | Announce: checkAnnounceActivity, |
48 | Undo: checkUndoActivity, | 36 | Undo: checkUndoActivity, |
49 | Like: checkLikeActivity | 37 | Like: checkLikeActivity, |
38 | View: checkViewActivity, | ||
39 | Flag: checkFlagActivity, | ||
40 | Dislike: checkDislikeActivity | ||
50 | } | 41 | } |
51 | 42 | ||
52 | function isActivityValid (activity: any) { | 43 | function isActivityValid (activity: any) { |
@@ -66,47 +57,79 @@ export { | |||
66 | 57 | ||
67 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
68 | 59 | ||
60 | function checkViewActivity (activity: any) { | ||
61 | return isBaseActivityValid(activity, 'View') && | ||
62 | isViewActivityValid(activity) | ||
63 | } | ||
64 | |||
65 | function checkFlagActivity (activity: any) { | ||
66 | return isBaseActivityValid(activity, 'Flag') && | ||
67 | isFlagActivityValid(activity) | ||
68 | } | ||
69 | |||
70 | function checkDislikeActivity (activity: any) { | ||
71 | return isBaseActivityValid(activity, 'Dislike') && | ||
72 | isDislikeActivityValid(activity) | ||
73 | } | ||
74 | |||
69 | function checkCreateActivity (activity: any) { | 75 | function checkCreateActivity (activity: any) { |
70 | return isViewActivityValid(activity) || | 76 | return isBaseActivityValid(activity, 'Create') && |
71 | isDislikeActivityValid(activity) || | 77 | ( |
72 | sanitizeAndCheckVideoTorrentCreateActivity(activity) || | 78 | isViewActivityValid(activity.object) || |
73 | isVideoFlagValid(activity) || | 79 | isDislikeActivityValid(activity.object) || |
74 | isVideoCommentCreateActivityValid(activity) || | 80 | isFlagActivityValid(activity.object) || |
75 | isCacheFileCreateActivityValid(activity) | 81 | |
82 | isCacheFileObjectValid(activity.object) || | ||
83 | sanitizeAndCheckVideoCommentObject(activity.object) || | ||
84 | sanitizeAndCheckVideoTorrentObject(activity.object) | ||
85 | ) | ||
76 | } | 86 | } |
77 | 87 | ||
78 | function checkUpdateActivity (activity: any) { | 88 | function checkUpdateActivity (activity: any) { |
79 | return isCacheFileUpdateActivityValid(activity) || | 89 | return isBaseActivityValid(activity, 'Update') && |
80 | sanitizeAndCheckVideoTorrentUpdateActivity(activity) || | 90 | ( |
81 | isActorUpdateActivityValid(activity) | 91 | isCacheFileObjectValid(activity.object) || |
92 | sanitizeAndCheckVideoTorrentObject(activity.object) || | ||
93 | sanitizeAndCheckActorObject(activity.object) | ||
94 | ) | ||
82 | } | 95 | } |
83 | 96 | ||
84 | function checkDeleteActivity (activity: any) { | 97 | function checkDeleteActivity (activity: any) { |
85 | return isVideoTorrentDeleteActivityValid(activity) || | 98 | // We don't really check objects |
86 | isActorDeleteActivityValid(activity) || | 99 | return isBaseActivityValid(activity, 'Delete') && |
87 | isVideoCommentDeleteActivityValid(activity) | 100 | isObjectValid(activity.object) |
88 | } | 101 | } |
89 | 102 | ||
90 | function checkFollowActivity (activity: any) { | 103 | function checkFollowActivity (activity: any) { |
91 | return isActorFollowActivityValid(activity) | 104 | return isBaseActivityValid(activity, 'Follow') && |
105 | isObjectValid(activity.object) | ||
92 | } | 106 | } |
93 | 107 | ||
94 | function checkAcceptActivity (activity: any) { | 108 | function checkAcceptActivity (activity: any) { |
95 | return isActorAcceptActivityValid(activity) | 109 | return isBaseActivityValid(activity, 'Accept') |
96 | } | 110 | } |
97 | 111 | ||
98 | function checkRejectActivity (activity: any) { | 112 | function checkRejectActivity (activity: any) { |
99 | return isActorRejectActivityValid(activity) | 113 | return isBaseActivityValid(activity, 'Reject') |
100 | } | 114 | } |
101 | 115 | ||
102 | function checkAnnounceActivity (activity: any) { | 116 | function checkAnnounceActivity (activity: any) { |
103 | return isAnnounceActivityValid(activity) | 117 | return isBaseActivityValid(activity, 'Announce') && |
118 | isObjectValid(activity.object) | ||
104 | } | 119 | } |
105 | 120 | ||
106 | function checkUndoActivity (activity: any) { | 121 | function checkUndoActivity (activity: any) { |
107 | return isUndoActivityValid(activity) | 122 | return isBaseActivityValid(activity, 'Undo') && |
123 | ( | ||
124 | checkFollowActivity(activity.object) || | ||
125 | checkLikeActivity(activity.object) || | ||
126 | checkDislikeActivity(activity.object) || | ||
127 | checkAnnounceActivity(activity.object) || | ||
128 | checkCreateActivity(activity.object) | ||
129 | ) | ||
108 | } | 130 | } |
109 | 131 | ||
110 | function checkLikeActivity (activity: any) { | 132 | function checkLikeActivity (activity: any) { |
111 | return isLikeActivityValid(activity) | 133 | return isBaseActivityValid(activity, 'Like') && |
134 | isObjectValid(activity.object) | ||
112 | } | 135 | } |
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 070632a20..c05f60f14 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts | |||
@@ -73,24 +73,10 @@ function isActorDeleteActivityValid (activity: any) { | |||
73 | return isBaseActivityValid(activity, 'Delete') | 73 | return isBaseActivityValid(activity, 'Delete') |
74 | } | 74 | } |
75 | 75 | ||
76 | function isActorFollowActivityValid (activity: any) { | 76 | function sanitizeAndCheckActorObject (object: any) { |
77 | return isBaseActivityValid(activity, 'Follow') && | 77 | normalizeActor(object) |
78 | isActivityPubUrlValid(activity.object) | ||
79 | } | ||
80 | |||
81 | function isActorAcceptActivityValid (activity: any) { | ||
82 | return isBaseActivityValid(activity, 'Accept') | ||
83 | } | ||
84 | |||
85 | function isActorRejectActivityValid (activity: any) { | ||
86 | return isBaseActivityValid(activity, 'Reject') | ||
87 | } | ||
88 | |||
89 | function isActorUpdateActivityValid (activity: any) { | ||
90 | normalizeActor(activity.object) | ||
91 | 78 | ||
92 | return isBaseActivityValid(activity, 'Update') && | 79 | return isActorObjectValid(object) |
93 | isActorObjectValid(activity.object) | ||
94 | } | 80 | } |
95 | 81 | ||
96 | function normalizeActor (actor: any) { | 82 | function normalizeActor (actor: any) { |
@@ -139,10 +125,7 @@ export { | |||
139 | isActorObjectValid, | 125 | isActorObjectValid, |
140 | isActorFollowingCountValid, | 126 | isActorFollowingCountValid, |
141 | isActorFollowersCountValid, | 127 | isActorFollowersCountValid, |
142 | isActorFollowActivityValid, | ||
143 | isActorAcceptActivityValid, | ||
144 | isActorRejectActivityValid, | ||
145 | isActorDeleteActivityValid, | 128 | isActorDeleteActivityValid, |
146 | isActorUpdateActivityValid, | 129 | sanitizeAndCheckActorObject, |
147 | isValidActorHandle | 130 | isValidActorHandle |
148 | } | 131 | } |
diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts deleted file mode 100644 index 0519c6026..000000000 --- a/server/helpers/custom-validators/activitypub/announce.ts +++ /dev/null | |||
@@ -1,13 +0,0 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | ||
2 | |||
3 | function isAnnounceActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Announce') && | ||
5 | ( | ||
6 | isActivityPubUrlValid(activity.object) || | ||
7 | (activity.object && isActivityPubUrlValid(activity.object.id)) | ||
8 | ) | ||
9 | } | ||
10 | |||
11 | export { | ||
12 | isAnnounceActivityValid | ||
13 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts index bd70934c8..21d5c53ca 100644 --- a/server/helpers/custom-validators/activitypub/cache-file.ts +++ b/server/helpers/custom-validators/activitypub/cache-file.ts | |||
@@ -1,28 +1,26 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid } from './misc' |
2 | import { isRemoteVideoUrlValid } from './videos' | 2 | import { isRemoteVideoUrlValid } from './videos' |
3 | import { isDateValid, exists } from '../misc' | 3 | import { exists, isDateValid } from '../misc' |
4 | import { CacheFileObject } from '../../../../shared/models/activitypub/objects' | 4 | import { CacheFileObject } from '../../../../shared/models/activitypub/objects' |
5 | 5 | ||
6 | function isCacheFileCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | isCacheFileObjectValid(activity.object) | ||
9 | } | ||
10 | |||
11 | function isCacheFileUpdateActivityValid (activity: any) { | ||
12 | return isBaseActivityValid(activity, 'Update') && | ||
13 | isCacheFileObjectValid(activity.object) | ||
14 | } | ||
15 | |||
16 | function isCacheFileObjectValid (object: CacheFileObject) { | 6 | function isCacheFileObjectValid (object: CacheFileObject) { |
17 | return exists(object) && | 7 | return exists(object) && |
18 | object.type === 'CacheFile' && | 8 | object.type === 'CacheFile' && |
19 | isDateValid(object.expires) && | 9 | isDateValid(object.expires) && |
20 | isActivityPubUrlValid(object.object) && | 10 | isActivityPubUrlValid(object.object) && |
21 | isRemoteVideoUrlValid(object.url) | 11 | (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) |
22 | } | 12 | } |
23 | 13 | ||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
24 | export { | 16 | export { |
25 | isCacheFileUpdateActivityValid, | ||
26 | isCacheFileCreateActivityValid, | ||
27 | isCacheFileObjectValid | 17 | isCacheFileObjectValid |
28 | } | 18 | } |
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | function isPlaylistRedundancyUrlValid (url: any) { | ||
23 | return url.type === 'Link' && | ||
24 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
25 | isActivityPubUrlValid(url.href) | ||
26 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts new file mode 100644 index 000000000..6452e297c --- /dev/null +++ b/server/helpers/custom-validators/activitypub/flag.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | ||
2 | import { isVideoAbuseReasonValid } from '../video-abuses' | ||
3 | |||
4 | function isFlagActivityValid (activity: any) { | ||
5 | return activity.type === 'Flag' && | ||
6 | isVideoAbuseReasonValid(activity.content) && | ||
7 | isActivityPubUrlValid(activity.object) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | isFlagActivityValid | ||
14 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 4e2c57f04..f1762d11c 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts | |||
@@ -28,15 +28,20 @@ function isBaseActivityValid (activity: any, type: string) { | |||
28 | return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && | 28 | return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && |
29 | activity.type === type && | 29 | activity.type === type && |
30 | isActivityPubUrlValid(activity.id) && | 30 | isActivityPubUrlValid(activity.id) && |
31 | exists(activity.actor) && | 31 | isObjectValid(activity.actor) && |
32 | (isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) && | 32 | isUrlCollectionValid(activity.to) && |
33 | ( | 33 | isUrlCollectionValid(activity.cc) |
34 | activity.to === undefined || | 34 | } |
35 | (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t))) | 35 | |
36 | ) && | 36 | function isUrlCollectionValid (collection: any) { |
37 | return collection === undefined || | ||
38 | (Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t))) | ||
39 | } | ||
40 | |||
41 | function isObjectValid (object: any) { | ||
42 | return exists(object) && | ||
37 | ( | 43 | ( |
38 | activity.cc === undefined || | 44 | isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id) |
39 | (Array.isArray(activity.cc) && activity.cc.every(t => isActivityPubUrlValid(t))) | ||
40 | ) | 45 | ) |
41 | } | 46 | } |
42 | 47 | ||
@@ -57,5 +62,6 @@ export { | |||
57 | isUrlValid, | 62 | isUrlValid, |
58 | isActivityPubUrlValid, | 63 | isActivityPubUrlValid, |
59 | isBaseActivityValid, | 64 | isBaseActivityValid, |
60 | setValidAttributedTo | 65 | setValidAttributedTo, |
66 | isObjectValid | ||
61 | } | 67 | } |
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts index e70bd94b8..ba68e8074 100644 --- a/server/helpers/custom-validators/activitypub/rate.ts +++ b/server/helpers/custom-validators/activitypub/rate.ts | |||
@@ -1,20 +1,13 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid, isObjectValid } from './misc' |
2 | |||
3 | function isLikeActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Like') && | ||
5 | isActivityPubUrlValid(activity.object) | ||
6 | } | ||
7 | 2 | ||
8 | function isDislikeActivityValid (activity: any) { | 3 | function isDislikeActivityValid (activity: any) { |
9 | return isBaseActivityValid(activity, 'Create') && | 4 | return activity.type === 'Dislike' && |
10 | activity.object.type === 'Dislike' && | 5 | isActivityPubUrlValid(activity.actor) && |
11 | isActivityPubUrlValid(activity.object.actor) && | 6 | isObjectValid(activity.object) |
12 | isActivityPubUrlValid(activity.object.object) | ||
13 | } | 7 | } |
14 | 8 | ||
15 | // --------------------------------------------------------------------------- | 9 | // --------------------------------------------------------------------------- |
16 | 10 | ||
17 | export { | 11 | export { |
18 | isLikeActivityValid, | ||
19 | isDislikeActivityValid | 12 | isDislikeActivityValid |
20 | } | 13 | } |
diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts deleted file mode 100644 index 578035893..000000000 --- a/server/helpers/custom-validators/activitypub/undo.ts +++ /dev/null | |||
@@ -1,20 +0,0 @@ | |||
1 | import { isActorFollowActivityValid } from './actor' | ||
2 | import { isBaseActivityValid } from './misc' | ||
3 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
4 | import { isAnnounceActivityValid } from './announce' | ||
5 | import { isCacheFileCreateActivityValid } from './cache-file' | ||
6 | |||
7 | function isUndoActivityValid (activity: any) { | ||
8 | return isBaseActivityValid(activity, 'Undo') && | ||
9 | ( | ||
10 | isActorFollowActivityValid(activity.object) || | ||
11 | isLikeActivityValid(activity.object) || | ||
12 | isDislikeActivityValid(activity.object) || | ||
13 | isAnnounceActivityValid(activity.object) || | ||
14 | isCacheFileCreateActivityValid(activity.object) | ||
15 | ) | ||
16 | } | ||
17 | |||
18 | export { | ||
19 | isUndoActivityValid | ||
20 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts index 051c4565a..0415db21c 100644 --- a/server/helpers/custom-validators/activitypub/video-comments.ts +++ b/server/helpers/custom-validators/activitypub/video-comments.ts | |||
@@ -3,11 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' | |||
3 | import { exists, isArray, isDateValid } from '../misc' | 3 | import { exists, isArray, isDateValid } from '../misc' |
4 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 4 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' |
5 | 5 | ||
6 | function isVideoCommentCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | sanitizeAndCheckVideoCommentObject(activity.object) | ||
9 | } | ||
10 | |||
11 | function sanitizeAndCheckVideoCommentObject (comment: any) { | 6 | function sanitizeAndCheckVideoCommentObject (comment: any) { |
12 | if (!comment || comment.type !== 'Note') return false | 7 | if (!comment || comment.type !== 'Note') return false |
13 | 8 | ||
@@ -25,15 +20,9 @@ function sanitizeAndCheckVideoCommentObject (comment: any) { | |||
25 | ) // Only accept public comments | 20 | ) // Only accept public comments |
26 | } | 21 | } |
27 | 22 | ||
28 | function isVideoCommentDeleteActivityValid (activity: any) { | ||
29 | return isBaseActivityValid(activity, 'Delete') | ||
30 | } | ||
31 | |||
32 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
33 | 24 | ||
34 | export { | 25 | export { |
35 | isVideoCommentCreateActivityValid, | ||
36 | isVideoCommentDeleteActivityValid, | ||
37 | sanitizeAndCheckVideoCommentObject | 26 | sanitizeAndCheckVideoCommentObject |
38 | } | 27 | } |
39 | 28 | ||
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 95fe824b9..53ad0588d 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' | 2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' |
3 | import { peertubeTruncate } from '../../core-utils' | 3 | import { peertubeTruncate } from '../../core-utils' |
4 | import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' | 4 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' |
5 | import { | 5 | import { |
6 | isVideoDurationValid, | 6 | isVideoDurationValid, |
7 | isVideoNameValid, | 7 | isVideoNameValid, |
@@ -12,29 +12,12 @@ import { | |||
12 | } from '../videos' | 12 | } from '../videos' |
13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
14 | import { VideoState } from '../../../../shared/models/videos' | 14 | import { VideoState } from '../../../../shared/models/videos' |
15 | import { isVideoAbuseReasonValid } from '../video-abuses' | ||
16 | |||
17 | function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { | ||
18 | return isBaseActivityValid(activity, 'Create') && | ||
19 | sanitizeAndCheckVideoTorrentObject(activity.object) | ||
20 | } | ||
21 | 15 | ||
22 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { | 16 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { |
23 | return isBaseActivityValid(activity, 'Update') && | 17 | return isBaseActivityValid(activity, 'Update') && |
24 | sanitizeAndCheckVideoTorrentObject(activity.object) | 18 | sanitizeAndCheckVideoTorrentObject(activity.object) |
25 | } | 19 | } |
26 | 20 | ||
27 | function isVideoTorrentDeleteActivityValid (activity: any) { | ||
28 | return isBaseActivityValid(activity, 'Delete') | ||
29 | } | ||
30 | |||
31 | function isVideoFlagValid (activity: any) { | ||
32 | return isBaseActivityValid(activity, 'Create') && | ||
33 | activity.object.type === 'Flag' && | ||
34 | isVideoAbuseReasonValid(activity.object.content) && | ||
35 | isActivityPubUrlValid(activity.object.object) | ||
36 | } | ||
37 | |||
38 | function isActivityPubVideoDurationValid (value: string) { | 21 | function isActivityPubVideoDurationValid (value: string) { |
39 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | 22 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration |
40 | return exists(value) && | 23 | return exists(value) && |
@@ -56,6 +39,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
56 | // Default attributes | 39 | // Default attributes |
57 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED | 40 | if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED |
58 | if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false | 41 | if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false |
42 | if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true | ||
59 | 43 | ||
60 | return isActivityPubUrlValid(video.id) && | 44 | return isActivityPubUrlValid(video.id) && |
61 | isVideoNameValid(video.name) && | 45 | isVideoNameValid(video.name) && |
@@ -67,6 +51,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
67 | isVideoViewsValid(video.views) && | 51 | isVideoViewsValid(video.views) && |
68 | isBooleanValid(video.sensitive) && | 52 | isBooleanValid(video.sensitive) && |
69 | isBooleanValid(video.commentsEnabled) && | 53 | isBooleanValid(video.commentsEnabled) && |
54 | isBooleanValid(video.downloadEnabled) && | ||
70 | isDateValid(video.published) && | 55 | isDateValid(video.published) && |
71 | isDateValid(video.updated) && | 56 | isDateValid(video.updated) && |
72 | (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && | 57 | (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && |
@@ -97,17 +82,19 @@ function isRemoteVideoUrlValid (url: any) { | |||
97 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && | 82 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && |
98 | validator.isLength(url.href, { min: 5 }) && | 83 | validator.isLength(url.href, { min: 5 }) && |
99 | validator.isInt(url.height + '', { min: 0 }) | 84 | validator.isInt(url.height + '', { min: 0 }) |
85 | ) || | ||
86 | ( | ||
87 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
88 | isActivityPubUrlValid(url.href) && | ||
89 | isArray(url.tag) | ||
100 | ) | 90 | ) |
101 | } | 91 | } |
102 | 92 | ||
103 | // --------------------------------------------------------------------------- | 93 | // --------------------------------------------------------------------------- |
104 | 94 | ||
105 | export { | 95 | export { |
106 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
107 | sanitizeAndCheckVideoTorrentUpdateActivity, | 96 | sanitizeAndCheckVideoTorrentUpdateActivity, |
108 | isVideoTorrentDeleteActivityValid, | ||
109 | isRemoteStringIdentifierValid, | 97 | isRemoteStringIdentifierValid, |
110 | isVideoFlagValid, | ||
111 | sanitizeAndCheckVideoTorrentObject, | 98 | sanitizeAndCheckVideoTorrentObject, |
112 | isRemoteVideoUrlValid | 99 | isRemoteVideoUrlValid |
113 | } | 100 | } |
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts index 7a3aca6f5..41d16469f 100644 --- a/server/helpers/custom-validators/activitypub/view.ts +++ b/server/helpers/custom-validators/activitypub/view.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid } from './misc' |
2 | 2 | ||
3 | function isViewActivityValid (activity: any) { | 3 | function isViewActivityValid (activity: any) { |
4 | return isBaseActivityValid(activity, 'Create') && | 4 | return activity.type === 'View' && |
5 | activity.object.type === 'View' && | 5 | isActivityPubUrlValid(activity.actor) && |
6 | isActivityPubUrlValid(activity.object.actor) && | 6 | isActivityPubUrlValid(activity.object) |
7 | isActivityPubUrlValid(activity.object.object) | ||
8 | } | 7 | } |
8 | |||
9 | // --------------------------------------------------------------------------- | 9 | // --------------------------------------------------------------------------- |
10 | 10 | ||
11 | export { | 11 | export { |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index b6f0ebe6f..76647fea2 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -13,6 +13,10 @@ function isNotEmptyIntArray (value: any) { | |||
13 | return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 | 13 | return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 |
14 | } | 14 | } |
15 | 15 | ||
16 | function isArrayOf (value: any, validator: (value: any) => boolean) { | ||
17 | return isArray(value) && value.every(v => validator(v)) | ||
18 | } | ||
19 | |||
16 | function isDateValid (value: string) { | 20 | function isDateValid (value: string) { |
17 | return exists(value) && validator.isISO8601(value) | 21 | return exists(value) && validator.isISO8601(value) |
18 | } | 22 | } |
@@ -82,6 +86,7 @@ function isFileValid ( | |||
82 | 86 | ||
83 | export { | 87 | export { |
84 | exists, | 88 | exists, |
89 | isArrayOf, | ||
85 | isNotEmptyIntArray, | 90 | isNotEmptyIntArray, |
86 | isArray, | 91 | isArray, |
87 | isIdValid, | 92 | isIdValid, |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index ce4492a30..dd04aa5f6 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -88,8 +88,8 @@ function isVideoFileExtnameValid (value: string) { | |||
88 | 88 | ||
89 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 89 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { |
90 | const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | 90 | const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
91 | .map(m => `(${m})`) | 91 | .map(m => `(${m})`) |
92 | .join('|') | 92 | .join('|') |
93 | 93 | ||
94 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) | 94 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) |
95 | } | 95 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index c7296054d..133b1b03b 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' | 3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
@@ -29,19 +29,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
29 | return resolutionsEnabled | 29 | return resolutionsEnabled |
30 | } | 30 | } |
31 | 31 | ||
32 | async function getVideoFileResolution (path: string) { | 32 | async function getVideoFileSize (path: string) { |
33 | const videoStream = await getVideoFileStream(path) | 33 | const videoStream = await getVideoFileStream(path) |
34 | 34 | ||
35 | return { | 35 | return { |
36 | videoFileResolution: Math.min(videoStream.height, videoStream.width), | 36 | width: videoStream.width, |
37 | isPortraitMode: videoStream.height > videoStream.width | 37 | height: videoStream.height |
38 | } | ||
39 | } | ||
40 | |||
41 | async function getVideoFileResolution (path: string) { | ||
42 | const size = await getVideoFileSize(path) | ||
43 | |||
44 | return { | ||
45 | videoFileResolution: Math.min(size.height, size.width), | ||
46 | isPortraitMode: size.height > size.width | ||
38 | } | 47 | } |
39 | } | 48 | } |
40 | 49 | ||
41 | async function getVideoFileFPS (path: string) { | 50 | async function getVideoFileFPS (path: string) { |
42 | const videoStream = await getVideoFileStream(path) | 51 | const videoStream = await getVideoFileStream(path) |
43 | 52 | ||
44 | for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { | 53 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
45 | const valuesText: string = videoStream[key] | 54 | const valuesText: string = videoStream[key] |
46 | if (!valuesText) continue | 55 | if (!valuesText) continue |
47 | 56 | ||
@@ -110,8 +119,12 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
110 | type TranscodeOptions = { | 119 | type TranscodeOptions = { |
111 | inputPath: string | 120 | inputPath: string |
112 | outputPath: string | 121 | outputPath: string |
113 | resolution?: VideoResolution | 122 | resolution: VideoResolution |
114 | isPortraitMode?: boolean | 123 | isPortraitMode?: boolean |
124 | |||
125 | hlsPlaylist?: { | ||
126 | videoFilename: string | ||
127 | } | ||
115 | } | 128 | } |
116 | 129 | ||
117 | function transcode (options: TranscodeOptions) { | 130 | function transcode (options: TranscodeOptions) { |
@@ -150,6 +163,18 @@ function transcode (options: TranscodeOptions) { | |||
150 | command = command.withFPS(fps) | 163 | command = command.withFPS(fps) |
151 | } | 164 | } |
152 | 165 | ||
166 | if (options.hlsPlaylist) { | ||
167 | const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | ||
168 | |||
169 | command = command.outputOption('-hls_time 4') | ||
170 | .outputOption('-hls_list_size 0') | ||
171 | .outputOption('-hls_playlist_type vod') | ||
172 | .outputOption('-hls_segment_filename ' + videoPath) | ||
173 | .outputOption('-hls_segment_type fmp4') | ||
174 | .outputOption('-f hls') | ||
175 | .outputOption('-hls_flags single_file') | ||
176 | } | ||
177 | |||
153 | command | 178 | command |
154 | .on('error', (err, stdout, stderr) => { | 179 | .on('error', (err, stdout, stderr) => { |
155 | logger.error('Error in transcoding job.', { stdout, stderr }) | 180 | logger.error('Error in transcoding job.', { stdout, stderr }) |
@@ -166,6 +191,7 @@ function transcode (options: TranscodeOptions) { | |||
166 | // --------------------------------------------------------------------------- | 191 | // --------------------------------------------------------------------------- |
167 | 192 | ||
168 | export { | 193 | export { |
194 | getVideoFileSize, | ||
169 | getVideoFileResolution, | 195 | getVideoFileResolution, |
170 | getDurationFromVideoFile, | 196 | getDurationFromVideoFile, |
171 | generateImageFromVideoFile, | 197 | generateImageFromVideoFile, |
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 3fc776f1a..5c6dc5e19 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -7,7 +7,7 @@ import { join } from 'path' | |||
7 | 7 | ||
8 | function doRequest <T> ( | 8 | function doRequest <T> ( |
9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } | 9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } |
10 | ): Bluebird<{ response: request.RequestResponse, body: any }> { | 10 | ): Bluebird<{ response: request.RequestResponse, body: T }> { |
11 | if (requestOptions.activityPub === true) { | 11 | if (requestOptions.activityPub === true) { |
12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} | 12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} |
13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 3c3406e38..cb0e823c5 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -7,7 +7,6 @@ import { join } from 'path' | |||
7 | import { Instance as ParseTorrent } from 'parse-torrent' | 7 | import { Instance as ParseTorrent } from 'parse-torrent' |
8 | import { remove } from 'fs-extra' | 8 | import { remove } from 'fs-extra' |
9 | import * as memoizee from 'memoizee' | 9 | import * as memoizee from 'memoizee' |
10 | import { isArray } from './custom-validators/misc' | ||
11 | 10 | ||
12 | function deleteFileAsync (path: string) { | 11 | function deleteFileAsync (path: string) { |
13 | remove(path) | 12 | remove(path) |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index 1bd21467d..c90fe06c7 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { VideoModel } from '../models/video/video' |
2 | 2 | ||
3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' | 3 | type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' |
4 | 4 | ||
5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { | 5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { |
6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) | 6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) |
7 | 7 | ||
8 | if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) | ||
9 | |||
8 | if (fetchType === 'only-video') return VideoModel.load(id) | 10 | if (fetchType === 'only-video') return VideoModel.load(id) |
9 | 11 | ||
10 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) | 12 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 7905d9ffa..29fdb263e 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -12,7 +12,7 @@ function checkMissedConfig () { | |||
12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', | 12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', |
13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', | 14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', |
15 | 'storage.redundancy', 'storage.tmp', | 15 | 'storage.redundancy', 'storage.tmp', 'storage.playlists', |
16 | 'log.level', | 16 | 'log.level', |
17 | 'user.video_quota', 'user.video_quota_daily', | 17 | 'user.video_quota', 'user.video_quota_daily', |
18 | 'cache.previews.size', 'admin.email', 'contact_form.enabled', | 18 | 'cache.previews.size', 'admin.email', 'contact_form.enabled', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 4a88aef87..3656a23f9 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -16,7 +16,7 @@ let config: IConfig = require('config') | |||
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
18 | 18 | ||
19 | const LAST_MIGRATION_VERSION = 315 | 19 | const LAST_MIGRATION_VERSION = 340 |
20 | 20 | ||
21 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
22 | 22 | ||
@@ -192,6 +192,7 @@ const CONFIG = { | |||
192 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 192 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), |
193 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 193 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
194 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 194 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
195 | PLAYLISTS_DIR: buildPath(config.get<string>('storage.playlists')), | ||
195 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), | 196 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), |
196 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), | 197 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), |
197 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), | 198 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), |
@@ -259,6 +260,9 @@ const CONFIG = { | |||
259 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, | 260 | get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, |
260 | get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') }, | 261 | get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') }, |
261 | get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') } | 262 | get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') } |
263 | }, | ||
264 | HLS: { | ||
265 | get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } | ||
262 | } | 266 | } |
263 | }, | 267 | }, |
264 | IMPORT: { | 268 | IMPORT: { |
@@ -316,8 +320,8 @@ let CONSTRAINTS_FIELDS = { | |||
316 | BLOCKED_REASON: { min: 3, max: 250 } // Length | 320 | BLOCKED_REASON: { min: 3, max: 250 } // Length |
317 | }, | 321 | }, |
318 | VIDEO_ABUSES: { | 322 | VIDEO_ABUSES: { |
319 | REASON: { min: 2, max: 300 }, // Length | 323 | REASON: { min: 2, max: 3000 }, // Length |
320 | MODERATION_COMMENT: { min: 2, max: 300 } // Length | 324 | MODERATION_COMMENT: { min: 2, max: 3000 } // Length |
321 | }, | 325 | }, |
322 | VIDEO_BLACKLIST: { | 326 | VIDEO_BLACKLIST: { |
323 | REASON: { min: 2, max: 300 } // Length | 327 | REASON: { min: 2, max: 300 } // Length |
@@ -590,6 +594,9 @@ const STATIC_PATHS = { | |||
590 | TORRENTS: '/static/torrents/', | 594 | TORRENTS: '/static/torrents/', |
591 | WEBSEED: '/static/webseed/', | 595 | WEBSEED: '/static/webseed/', |
592 | REDUNDANCY: '/static/redundancy/', | 596 | REDUNDANCY: '/static/redundancy/', |
597 | PLAYLISTS: { | ||
598 | HLS: '/static/playlists/hls' | ||
599 | }, | ||
593 | AVATARS: '/static/avatars/', | 600 | AVATARS: '/static/avatars/', |
594 | VIDEO_CAPTIONS: '/static/video-captions/' | 601 | VIDEO_CAPTIONS: '/static/video-captions/' |
595 | } | 602 | } |
@@ -632,6 +639,9 @@ const CACHE = { | |||
632 | } | 639 | } |
633 | } | 640 | } |
634 | 641 | ||
642 | const HLS_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.PLAYLISTS_DIR, 'hls') | ||
643 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | ||
644 | |||
635 | const MEMOIZE_TTL = { | 645 | const MEMOIZE_TTL = { |
636 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours | 646 | OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours |
637 | } | 647 | } |
@@ -701,6 +711,8 @@ if (isTestInstance() === true) { | |||
701 | CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 | 711 | CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 |
702 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 | 712 | MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 |
703 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' | 713 | ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' |
714 | |||
715 | RATES_LIMIT.LOGIN.MAX = 20 | ||
704 | } | 716 | } |
705 | 717 | ||
706 | updateWebserverUrls() | 718 | updateWebserverUrls() |
@@ -709,6 +721,7 @@ updateWebserverUrls() | |||
709 | 721 | ||
710 | export { | 722 | export { |
711 | API_VERSION, | 723 | API_VERSION, |
724 | HLS_REDUNDANCY_DIRECTORY, | ||
712 | AVATARS_SIZE, | 725 | AVATARS_SIZE, |
713 | ACCEPT_HEADERS, | 726 | ACCEPT_HEADERS, |
714 | BCRYPT_SALT_SIZE, | 727 | BCRYPT_SALT_SIZE, |
@@ -733,6 +746,7 @@ export { | |||
733 | PRIVATE_RSA_KEY_SIZE, | 746 | PRIVATE_RSA_KEY_SIZE, |
734 | ROUTE_CACHE_LIFETIME, | 747 | ROUTE_CACHE_LIFETIME, |
735 | SORTABLE_COLUMNS, | 748 | SORTABLE_COLUMNS, |
749 | HLS_PLAYLIST_DIRECTORY, | ||
736 | FEEDS, | 750 | FEEDS, |
737 | JOB_TTL, | 751 | JOB_TTL, |
738 | NSFW_POLICY_TYPES, | 752 | NSFW_POLICY_TYPES, |
@@ -795,7 +809,9 @@ function buildVideoMimetypeExt () { | |||
795 | 'video/quicktime': '.mov', | 809 | 'video/quicktime': '.mov', |
796 | 'video/x-msvideo': '.avi', | 810 | 'video/x-msvideo': '.avi', |
797 | 'video/x-flv': '.flv', | 811 | 'video/x-flv': '.flv', |
798 | 'video/x-matroska': '.mkv' | 812 | 'video/x-matroska': '.mkv', |
813 | 'application/octet-stream': '.mkv', | ||
814 | 'video/avi': '.avi' | ||
799 | }) | 815 | }) |
800 | } | 816 | } |
801 | 817 | ||
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 84ad2079b..fe296142d 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -33,6 +33,7 @@ import { AccountBlocklistModel } from '../models/account/account-blocklist' | |||
33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | 33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' |
34 | import { UserNotificationModel } from '../models/account/user-notification' | 34 | import { UserNotificationModel } from '../models/account/user-notification' |
35 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | 35 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' |
36 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
36 | 37 | ||
37 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 38 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
38 | 39 | ||
@@ -99,7 +100,8 @@ async function initDatabaseModels (silent: boolean) { | |||
99 | AccountBlocklistModel, | 100 | AccountBlocklistModel, |
100 | ServerBlocklistModel, | 101 | ServerBlocklistModel, |
101 | UserNotificationModel, | 102 | UserNotificationModel, |
102 | UserNotificationSettingModel | 103 | UserNotificationSettingModel, |
104 | VideoStreamingPlaylistModel | ||
103 | ]) | 105 | ]) |
104 | 106 | ||
105 | // Check extensions exist in the database | 107 | // Check extensions exist in the database |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index b9a9da183..2b22e16fe 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -6,7 +6,7 @@ import { UserModel } from '../models/account/user' | |||
6 | import { ApplicationModel } from '../models/application/application' | 6 | import { ApplicationModel } from '../models/application/application' |
7 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 7 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' | 8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' |
9 | import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants' | 9 | import { CACHE, CONFIG, HLS_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants' |
10 | import { sequelizeTypescript } from './database' | 10 | import { sequelizeTypescript } from './database' |
11 | import { remove, ensureDir } from 'fs-extra' | 11 | import { remove, ensureDir } from 'fs-extra' |
12 | 12 | ||
@@ -73,6 +73,9 @@ function createDirectoriesIfNotExist () { | |||
73 | tasks.push(ensureDir(dir)) | 73 | tasks.push(ensureDir(dir)) |
74 | } | 74 | } |
75 | 75 | ||
76 | // Playlist directories | ||
77 | tasks.push(ensureDir(HLS_PLAYLIST_DIRECTORY)) | ||
78 | |||
76 | return Promise.all(tasks) | 79 | return Promise.all(tasks) |
77 | } | 80 | } |
78 | 81 | ||
diff --git a/server/initializers/migrations/0320-blacklist-unfederate.ts b/server/initializers/migrations/0320-blacklist-unfederate.ts new file mode 100644 index 000000000..6fb7bbb90 --- /dev/null +++ b/server/initializers/migrations/0320-blacklist-unfederate.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.BOOLEAN, | ||
12 | allowNull: false, | ||
13 | defaultValue: false | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.addColumn('videoBlacklist', 'unfederated', data) | ||
17 | } | ||
18 | } | ||
19 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { | ||
25 | up, | ||
26 | down | ||
27 | } | ||
diff --git a/server/initializers/migrations/0325-video-abuse-fields.ts b/server/initializers/migrations/0325-video-abuse-fields.ts new file mode 100644 index 000000000..fca6d666f --- /dev/null +++ b/server/initializers/migrations/0325-video-abuse-fields.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.STRING(3000), | ||
12 | allowNull: false, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.changeColumn('videoAbuse', 'reason', data) | ||
17 | } | ||
18 | |||
19 | { | ||
20 | const data = { | ||
21 | type: Sequelize.STRING(3000), | ||
22 | allowNull: true, | ||
23 | defaultValue: null | ||
24 | } | ||
25 | |||
26 | await utils.queryInterface.changeColumn('videoAbuse', 'moderationComment', data) | ||
27 | } | ||
28 | } | ||
29 | |||
30 | function down (options) { | ||
31 | throw new Error('Not implemented.') | ||
32 | } | ||
33 | |||
34 | export { | ||
35 | up, | ||
36 | down | ||
37 | } | ||
diff --git a/server/initializers/migrations/0330-video-streaming-playlist.ts b/server/initializers/migrations/0330-video-streaming-playlist.ts new file mode 100644 index 000000000..c85a762ab --- /dev/null +++ b/server/initializers/migrations/0330-video-streaming-playlist.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const query = ` | ||
11 | CREATE TABLE IF NOT EXISTS "videoStreamingPlaylist" | ||
12 | ( | ||
13 | "id" SERIAL, | ||
14 | "type" INTEGER NOT NULL, | ||
15 | "playlistUrl" VARCHAR(2000) NOT NULL, | ||
16 | "p2pMediaLoaderInfohashes" VARCHAR(255)[] NOT NULL, | ||
17 | "segmentsSha256Url" VARCHAR(255) NOT NULL, | ||
18 | "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
19 | "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
20 | "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, | ||
21 | PRIMARY KEY ("id") | ||
22 | );` | ||
23 | await utils.sequelize.query(query) | ||
24 | } | ||
25 | |||
26 | { | ||
27 | const data = { | ||
28 | type: Sequelize.INTEGER, | ||
29 | allowNull: true, | ||
30 | defaultValue: null | ||
31 | } | ||
32 | |||
33 | await utils.queryInterface.changeColumn('videoRedundancy', 'videoFileId', data) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | const query = 'ALTER TABLE "videoRedundancy" ADD COLUMN "videoStreamingPlaylistId" INTEGER NULL ' + | ||
38 | 'REFERENCES "videoStreamingPlaylist" ("id") ON DELETE CASCADE ON UPDATE CASCADE' | ||
39 | |||
40 | await utils.sequelize.query(query) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | function down (options) { | ||
45 | throw new Error('Not implemented.') | ||
46 | } | ||
47 | |||
48 | export { | ||
49 | up, | ||
50 | down | ||
51 | } | ||
diff --git a/server/initializers/migrations/0335-video-downloading-enabled.ts b/server/initializers/migrations/0335-video-downloading-enabled.ts new file mode 100644 index 000000000..e79466447 --- /dev/null +++ b/server/initializers/migrations/0335-video-downloading-enabled.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { Migration } from '../../models/migrations' | ||
3 | |||
4 | async function up (utils: { | ||
5 | transaction: Sequelize.Transaction, | ||
6 | queryInterface: Sequelize.QueryInterface, | ||
7 | sequelize: Sequelize.Sequelize | ||
8 | }): Promise<void> { | ||
9 | const data = { | ||
10 | type: Sequelize.BOOLEAN, | ||
11 | allowNull: false, | ||
12 | defaultValue: true | ||
13 | } as Migration.Boolean | ||
14 | await utils.queryInterface.addColumn('video', 'downloadEnabled', data) | ||
15 | |||
16 | data.defaultValue = null | ||
17 | return utils.queryInterface.changeColumn('video', 'downloadEnabled', data) | ||
18 | } | ||
19 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { | ||
25 | up, | ||
26 | down | ||
27 | } | ||
diff --git a/server/initializers/migrations/0295-add-originally-published-at.ts b/server/initializers/migrations/0340-add-originally-published-at.ts index e02cca480..ab8d66854 100644 --- a/server/initializers/migrations/0295-add-originally-published-at.ts +++ b/server/initializers/migrations/0340-add-originally-published-at.ts | |||
@@ -19,17 +19,6 @@ async function up (utils: { | |||
19 | const query = 'UPDATE video SET "originallyPublishedAt" = video."publishedAt"' | 19 | const query = 'UPDATE video SET "originallyPublishedAt" = video."publishedAt"' |
20 | await utils.sequelize.query(query) | 20 | await utils.sequelize.query(query) |
21 | } | 21 | } |
22 | |||
23 | // Sequelize does not alter the column with NOW as default value | ||
24 | { | ||
25 | const data = { | ||
26 | type: Sequelize.DATE, | ||
27 | allowNull: false, | ||
28 | defaultValue: Sequelize.NOW | ||
29 | } | ||
30 | await utils.queryInterface.changeColumn('video', 'originallyPublishedAt', data) | ||
31 | } | ||
32 | |||
33 | } | 22 | } |
34 | 23 | ||
35 | function down (options) { | 24 | function down (options) { |
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index f7bf7c65a..a3f379b76 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -4,7 +4,7 @@ import * as url from 'url' | |||
4 | import * as uuidv4 from 'uuid/v4' | 4 | import * as uuidv4 from 'uuid/v4' |
5 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' | 5 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' |
6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
7 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' | 7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
8 | import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' | 8 | import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | 10 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' |
@@ -42,7 +42,7 @@ async function getOrCreateActorAndServerAndModel ( | |||
42 | recurseIfNeeded = true, | 42 | recurseIfNeeded = true, |
43 | updateCollections = false | 43 | updateCollections = false |
44 | ) { | 44 | ) { |
45 | const actorUrl = getAPUrl(activityActor) | 45 | const actorUrl = getAPId(activityActor) |
46 | let created = false | 46 | let created = false |
47 | 47 | ||
48 | let actor = await fetchActorByUrl(actorUrl, fetchType) | 48 | let actor = await fetchActorByUrl(actorUrl, fetchType) |
@@ -201,6 +201,69 @@ async function addFetchOutboxJob (actor: ActorModel) { | |||
201 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | 201 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) |
202 | } | 202 | } |
203 | 203 | ||
204 | async function refreshActorIfNeeded ( | ||
205 | actorArg: ActorModel, | ||
206 | fetchedType: ActorFetchByUrlType | ||
207 | ): Promise<{ actor: ActorModel, refreshed: boolean }> { | ||
208 | if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } | ||
209 | |||
210 | // We need more attributes | ||
211 | const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
212 | |||
213 | try { | ||
214 | let actorUrl: string | ||
215 | try { | ||
216 | actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
217 | } catch (err) { | ||
218 | logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err) | ||
219 | actorUrl = actor.url | ||
220 | } | ||
221 | |||
222 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | ||
223 | |||
224 | if (statusCode === 404) { | ||
225 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
226 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | ||
227 | return { actor: undefined, refreshed: false } | ||
228 | } | ||
229 | |||
230 | if (result === undefined) { | ||
231 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
232 | return { actor, refreshed: false } | ||
233 | } | ||
234 | |||
235 | return sequelizeTypescript.transaction(async t => { | ||
236 | updateInstanceWithAnother(actor, result.actor) | ||
237 | |||
238 | if (result.avatarName !== undefined) { | ||
239 | await updateActorAvatarInstance(actor, result.avatarName, t) | ||
240 | } | ||
241 | |||
242 | // Force update | ||
243 | actor.setDataValue('updatedAt', new Date()) | ||
244 | await actor.save({ transaction: t }) | ||
245 | |||
246 | if (actor.Account) { | ||
247 | actor.Account.set('name', result.name) | ||
248 | actor.Account.set('description', result.summary) | ||
249 | |||
250 | await actor.Account.save({ transaction: t }) | ||
251 | } else if (actor.VideoChannel) { | ||
252 | actor.VideoChannel.set('name', result.name) | ||
253 | actor.VideoChannel.set('description', result.summary) | ||
254 | actor.VideoChannel.set('support', result.support) | ||
255 | |||
256 | await actor.VideoChannel.save({ transaction: t }) | ||
257 | } | ||
258 | |||
259 | return { refreshed: true, actor } | ||
260 | }) | ||
261 | } catch (err) { | ||
262 | logger.warn('Cannot refresh actor.', { err }) | ||
263 | return { actor, refreshed: false } | ||
264 | } | ||
265 | } | ||
266 | |||
204 | export { | 267 | export { |
205 | getOrCreateActorAndServerAndModel, | 268 | getOrCreateActorAndServerAndModel, |
206 | buildActorInstance, | 269 | buildActorInstance, |
@@ -208,6 +271,7 @@ export { | |||
208 | fetchActorTotalItems, | 271 | fetchActorTotalItems, |
209 | fetchAvatarIfExists, | 272 | fetchAvatarIfExists, |
210 | updateActorInstance, | 273 | updateActorInstance, |
274 | refreshActorIfNeeded, | ||
211 | updateActorAvatarInstance, | 275 | updateActorAvatarInstance, |
212 | addFetchOutboxJob | 276 | addFetchOutboxJob |
213 | } | 277 | } |
@@ -291,12 +355,12 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
291 | 355 | ||
292 | logger.info('Fetching remote actor %s.', actorUrl) | 356 | logger.info('Fetching remote actor %s.', actorUrl) |
293 | 357 | ||
294 | const requestResult = await doRequest(options) | 358 | const requestResult = await doRequest<ActivityPubActor>(options) |
295 | normalizeActor(requestResult.body) | 359 | normalizeActor(requestResult.body) |
296 | 360 | ||
297 | const actorJSON: ActivityPubActor = requestResult.body | 361 | const actorJSON = requestResult.body |
298 | if (isActorObjectValid(actorJSON) === false) { | 362 | if (isActorObjectValid(actorJSON) === false) { |
299 | logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) | 363 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) |
300 | return { result: undefined, statusCode: requestResult.response.statusCode } | 364 | return { result: undefined, statusCode: requestResult.response.statusCode } |
301 | } | 365 | } |
302 | 366 | ||
@@ -372,59 +436,3 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu | |||
372 | 436 | ||
373 | return videoChannelCreated | 437 | return videoChannelCreated |
374 | } | 438 | } |
375 | |||
376 | async function refreshActorIfNeeded ( | ||
377 | actorArg: ActorModel, | ||
378 | fetchedType: ActorFetchByUrlType | ||
379 | ): Promise<{ actor: ActorModel, refreshed: boolean }> { | ||
380 | if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } | ||
381 | |||
382 | // We need more attributes | ||
383 | const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
384 | |||
385 | try { | ||
386 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
387 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | ||
388 | |||
389 | if (statusCode === 404) { | ||
390 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
391 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | ||
392 | return { actor: undefined, refreshed: false } | ||
393 | } | ||
394 | |||
395 | if (result === undefined) { | ||
396 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
397 | return { actor, refreshed: false } | ||
398 | } | ||
399 | |||
400 | return sequelizeTypescript.transaction(async t => { | ||
401 | updateInstanceWithAnother(actor, result.actor) | ||
402 | |||
403 | if (result.avatarName !== undefined) { | ||
404 | await updateActorAvatarInstance(actor, result.avatarName, t) | ||
405 | } | ||
406 | |||
407 | // Force update | ||
408 | actor.setDataValue('updatedAt', new Date()) | ||
409 | await actor.save({ transaction: t }) | ||
410 | |||
411 | if (actor.Account) { | ||
412 | actor.Account.set('name', result.name) | ||
413 | actor.Account.set('description', result.summary) | ||
414 | |||
415 | await actor.Account.save({ transaction: t }) | ||
416 | } else if (actor.VideoChannel) { | ||
417 | actor.VideoChannel.set('name', result.name) | ||
418 | actor.VideoChannel.set('description', result.summary) | ||
419 | actor.VideoChannel.set('support', result.support) | ||
420 | |||
421 | await actor.VideoChannel.save({ transaction: t }) | ||
422 | } | ||
423 | |||
424 | return { refreshed: true, actor } | ||
425 | }) | ||
426 | } catch (err) { | ||
427 | logger.warn('Cannot refresh actor.', { err }) | ||
428 | return { actor, refreshed: false } | ||
429 | } | ||
430 | } | ||
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index f6f068b45..9a40414bb 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -1,11 +1,28 @@ | |||
1 | import { CacheFileObject } from '../../../shared/index' | 1 | import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index' |
2 | import { VideoModel } from '../../models/video/video' | 2 | import { VideoModel } from '../../models/video/video' |
3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
4 | import { Transaction } from 'sequelize' | 4 | import { Transaction } from 'sequelize' |
5 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | 6 | ||
6 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { | 7 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { |
7 | const url = cacheFileObject.url | ||
8 | 8 | ||
9 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { | ||
10 | const url = cacheFileObject.url | ||
11 | |||
12 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
14 | |||
15 | return { | ||
16 | expiresOn: new Date(cacheFileObject.expires), | ||
17 | url: cacheFileObject.id, | ||
18 | fileUrl: url.href, | ||
19 | strategy: null, | ||
20 | videoStreamingPlaylistId: playlist.id, | ||
21 | actorId: byActor.id | ||
22 | } | ||
23 | } | ||
24 | |||
25 | const url = cacheFileObject.url | ||
9 | const videoFile = video.VideoFiles.find(f => { | 26 | const videoFile = video.VideoFiles.find(f => { |
10 | return f.resolution === url.height && f.fps === url.fps | 27 | return f.resolution === url.height && f.fps === url.fps |
11 | }) | 28 | }) |
@@ -15,7 +32,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
15 | return { | 32 | return { |
16 | expiresOn: new Date(cacheFileObject.expires), | 33 | expiresOn: new Date(cacheFileObject.expires), |
17 | url: cacheFileObject.id, | 34 | url: cacheFileObject.id, |
18 | fileUrl: cacheFileObject.url.href, | 35 | fileUrl: url.href, |
19 | strategy: null, | 36 | strategy: null, |
20 | videoFileId: videoFile.id, | 37 | videoFileId: videoFile.id, |
21 | actorId: byActor.id | 38 | actorId: byActor.id |
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 605705ad3..ebb275e34 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts | |||
@@ -2,7 +2,6 @@ import { ActivityAccept } from '../../../../shared/models/activitypub' | |||
2 | import { ActorModel } from '../../../models/activitypub/actor' | 2 | import { ActorModel } from '../../../models/activitypub/actor' |
3 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
4 | import { addFetchOutboxJob } from '../actor' | 4 | import { addFetchOutboxJob } from '../actor' |
5 | import { Notifier } from '../../notifier' | ||
6 | 5 | ||
7 | async function processAcceptActivity (activity: ActivityAccept, targetActor: ActorModel, inboxActor?: ActorModel) { | 6 | async function processAcceptActivity (activity: ActivityAccept, targetActor: ActorModel, inboxActor?: ActorModel) { |
8 | if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.') | 7 | if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.') |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 2e04ee843..5f4d793a5 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,36 +1,44 @@ | |||
1 | import { ActivityCreate, CacheFileObject, VideoAbuseState, VideoTorrentObject } from '../../../../shared' | 1 | import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' |
2 | import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' | 2 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' |
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
6 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers' |
7 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
8 | import { ActorModel } from '../../../models/activitypub/actor' | 6 | import { ActorModel } from '../../../models/activitypub/actor' |
9 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
10 | import { addVideoComment, resolveThread } from '../video-comments' | 7 | import { addVideoComment, resolveThread } from '../video-comments' |
11 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 8 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 9 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | import { Redis } from '../../redis' | ||
14 | import { createOrUpdateCacheFile } from '../cache-file' | 10 | import { createOrUpdateCacheFile } from '../cache-file' |
15 | import { getVideoDislikeActivityPubUrl } from '../url' | ||
16 | import { Notifier } from '../../notifier' | 11 | import { Notifier } from '../../notifier' |
12 | import { processViewActivity } from './process-view' | ||
13 | import { processDislikeActivity } from './process-dislike' | ||
14 | import { processFlagActivity } from './process-flag' | ||
17 | 15 | ||
18 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { | 16 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { |
19 | const activityObject = activity.object | 17 | const activityObject = activity.object |
20 | const activityType = activityObject.type | 18 | const activityType = activityObject.type |
21 | 19 | ||
22 | if (activityType === 'View') { | 20 | if (activityType === 'View') { |
23 | return processCreateView(byActor, activity) | 21 | return processViewActivity(activity, byActor) |
24 | } else if (activityType === 'Dislike') { | 22 | } |
25 | return retryTransactionWrapper(processCreateDislike, byActor, activity) | 23 | |
26 | } else if (activityType === 'Video') { | 24 | if (activityType === 'Dislike') { |
25 | return retryTransactionWrapper(processDislikeActivity, activity, byActor) | ||
26 | } | ||
27 | |||
28 | if (activityType === 'Flag') { | ||
29 | return retryTransactionWrapper(processFlagActivity, activity, byActor) | ||
30 | } | ||
31 | |||
32 | if (activityType === 'Video') { | ||
27 | return processCreateVideo(activity) | 33 | return processCreateVideo(activity) |
28 | } else if (activityType === 'Flag') { | 34 | } |
29 | return retryTransactionWrapper(processCreateVideoAbuse, byActor, activityObject as VideoAbuseObject) | 35 | |
30 | } else if (activityType === 'Note') { | 36 | if (activityType === 'Note') { |
31 | return retryTransactionWrapper(processCreateVideoComment, byActor, activity) | 37 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor) |
32 | } else if (activityType === 'CacheFile') { | 38 | } |
33 | return retryTransactionWrapper(processCacheFile, byActor, activity) | 39 | |
40 | if (activityType === 'CacheFile') { | ||
41 | return retryTransactionWrapper(processCacheFile, activity, byActor) | ||
34 | } | 42 | } |
35 | 43 | ||
36 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 44 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
@@ -55,56 +63,7 @@ async function processCreateVideo (activity: ActivityCreate) { | |||
55 | return video | 63 | return video |
56 | } | 64 | } |
57 | 65 | ||
58 | async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) { | 66 | async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { |
59 | const dislike = activity.object as DislikeObject | ||
60 | const byAccount = byActor.Account | ||
61 | |||
62 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
63 | |||
64 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | ||
65 | |||
66 | return sequelizeTypescript.transaction(async t => { | ||
67 | const rate = { | ||
68 | type: 'dislike' as 'dislike', | ||
69 | videoId: video.id, | ||
70 | accountId: byAccount.id | ||
71 | } | ||
72 | |||
73 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | ||
74 | where: rate, | ||
75 | defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), | ||
76 | transaction: t | ||
77 | }) | ||
78 | if (created === true) await video.increment('dislikes', { transaction: t }) | ||
79 | |||
80 | if (video.isOwned() && created === true) { | ||
81 | // Don't resend the activity to the sender | ||
82 | const exceptions = [ byActor ] | ||
83 | |||
84 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
85 | } | ||
86 | }) | ||
87 | } | ||
88 | |||
89 | async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { | ||
90 | const view = activity.object as ViewObject | ||
91 | |||
92 | const options = { | ||
93 | videoObject: view.object, | ||
94 | fetchType: 'only-video' as 'only-video' | ||
95 | } | ||
96 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | ||
97 | |||
98 | await Redis.Instance.addVideoView(video.id) | ||
99 | |||
100 | if (video.isOwned()) { | ||
101 | // Don't resend the activity to the sender | ||
102 | const exceptions = [ byActor ] | ||
103 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
104 | } | ||
105 | } | ||
106 | |||
107 | async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { | ||
108 | const cacheFile = activity.object as CacheFileObject | 67 | const cacheFile = activity.object as CacheFileObject |
109 | 68 | ||
110 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) | 69 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) |
@@ -120,32 +79,7 @@ async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) | |||
120 | } | 79 | } |
121 | } | 80 | } |
122 | 81 | ||
123 | async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) { | 82 | async function processCreateVideoComment (activity: ActivityCreate, byActor: ActorModel) { |
124 | logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) | ||
125 | |||
126 | const account = byActor.Account | ||
127 | if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
128 | |||
129 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoAbuseToCreateData.object }) | ||
130 | |||
131 | return sequelizeTypescript.transaction(async t => { | ||
132 | const videoAbuseData = { | ||
133 | reporterAccountId: account.id, | ||
134 | reason: videoAbuseToCreateData.content, | ||
135 | videoId: video.id, | ||
136 | state: VideoAbuseState.PENDING | ||
137 | } | ||
138 | |||
139 | const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) | ||
140 | videoAbuseInstance.Video = video | ||
141 | |||
142 | Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance) | ||
143 | |||
144 | logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) | ||
145 | }) | ||
146 | } | ||
147 | |||
148 | async function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) { | ||
149 | const commentObject = activity.object as VideoCommentObject | 83 | const commentObject = activity.object as VideoCommentObject |
150 | const byAccount = byActor.Account | 84 | const byAccount = byActor.Account |
151 | 85 | ||
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts new file mode 100644 index 000000000..bfd69e07a --- /dev/null +++ b/server/lib/activitypub/process/process-dislike.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import { ActivityCreate, ActivityDislike } from '../../../../shared' | ||
2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { sequelizeTypescript } from '../../../initializers' | ||
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
6 | import { ActorModel } from '../../../models/activitypub/actor' | ||
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
8 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
9 | import { getVideoDislikeActivityPubUrl } from '../url' | ||
10 | |||
11 | async function processDislikeActivity (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) { | ||
12 | return retryTransactionWrapper(processDislike, activity, byActor) | ||
13 | } | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | processDislikeActivity | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) { | ||
24 | const dislikeObject = activity.type === 'Dislike' ? activity.object : (activity.object as DislikeObject).object | ||
25 | const byAccount = byActor.Account | ||
26 | |||
27 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
28 | |||
29 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislikeObject }) | ||
30 | |||
31 | return sequelizeTypescript.transaction(async t => { | ||
32 | const rate = { | ||
33 | type: 'dislike' as 'dislike', | ||
34 | videoId: video.id, | ||
35 | accountId: byAccount.id | ||
36 | } | ||
37 | |||
38 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | ||
39 | where: rate, | ||
40 | defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), | ||
41 | transaction: t | ||
42 | }) | ||
43 | if (created === true) await video.increment('dislikes', { transaction: t }) | ||
44 | |||
45 | if (video.isOwned() && created === true) { | ||
46 | // Don't resend the activity to the sender | ||
47 | const exceptions = [ byActor ] | ||
48 | |||
49 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
50 | } | ||
51 | }) | ||
52 | } | ||
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts new file mode 100644 index 000000000..79ce6fb41 --- /dev/null +++ b/server/lib/activitypub/process/process-flag.ts | |||
@@ -0,0 +1,49 @@ | |||
1 | import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared' | ||
2 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { sequelizeTypescript } from '../../../initializers' | ||
6 | import { ActorModel } from '../../../models/activitypub/actor' | ||
7 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
8 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
9 | import { Notifier } from '../../notifier' | ||
10 | import { getAPId } from '../../../helpers/activitypub' | ||
11 | |||
12 | async function processFlagActivity (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) { | ||
13 | return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | processFlagActivity | ||
20 | } | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) { | ||
25 | const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) | ||
26 | |||
27 | logger.debug('Reporting remote abuse for video %s.', getAPId(flag.object)) | ||
28 | |||
29 | const account = byActor.Account | ||
30 | if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
31 | |||
32 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: flag.object }) | ||
33 | |||
34 | return sequelizeTypescript.transaction(async t => { | ||
35 | const videoAbuseData = { | ||
36 | reporterAccountId: account.id, | ||
37 | reason: flag.content, | ||
38 | videoId: video.id, | ||
39 | state: VideoAbuseState.PENDING | ||
40 | } | ||
41 | |||
42 | const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) | ||
43 | videoAbuseInstance.Video = video | ||
44 | |||
45 | Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance) | ||
46 | |||
47 | logger.info('Remote abuse for video uuid %s created', flag.object) | ||
48 | }) | ||
49 | } | ||
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index a67892440..0cd537187 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts | |||
@@ -6,9 +6,10 @@ import { ActorModel } from '../../../models/activitypub/actor' | |||
6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
7 | import { sendAccept } from '../send' | 7 | import { sendAccept } from '../send' |
8 | import { Notifier } from '../../notifier' | 8 | import { Notifier } from '../../notifier' |
9 | import { getAPId } from '../../../helpers/activitypub' | ||
9 | 10 | ||
10 | async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { | 11 | async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { |
11 | const activityObject = activity.object | 12 | const activityObject = getAPId(activity.object) |
12 | 13 | ||
13 | return retryTransactionWrapper(processFollow, byActor, activityObject) | 14 | return retryTransactionWrapper(processFollow, byActor, activityObject) |
14 | } | 15 | } |
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index e8e97eece..2a04167d7 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts | |||
@@ -6,6 +6,7 @@ import { ActorModel } from '../../../models/activitypub/actor' | |||
6 | import { forwardVideoRelatedActivity } from '../send/utils' | 6 | import { forwardVideoRelatedActivity } from '../send/utils' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { getVideoLikeActivityPubUrl } from '../url' | 8 | import { getVideoLikeActivityPubUrl } from '../url' |
9 | import { getAPId } from '../../../helpers/activitypub' | ||
9 | 10 | ||
10 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { | 11 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { |
11 | return retryTransactionWrapper(processLikeVideo, byActor, activity) | 12 | return retryTransactionWrapper(processLikeVideo, byActor, activity) |
@@ -20,7 +21,7 @@ export { | |||
20 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
21 | 22 | ||
22 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { | 23 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { |
23 | const videoUrl = activity.object | 24 | const videoUrl = getAPId(activity.object) |
24 | 25 | ||
25 | const byAccount = byActor.Account | 26 | const byAccount = byActor.Account |
26 | if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) | 27 | if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) |
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 438a013b6..ed0177a67 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts | |||
@@ -26,6 +26,10 @@ async function processUndoActivity (activity: ActivityUndo, byActor: ActorModel) | |||
26 | } | 26 | } |
27 | } | 27 | } |
28 | 28 | ||
29 | if (activityToUndo.type === 'Dislike') { | ||
30 | return retryTransactionWrapper(processUndoDislike, byActor, activity) | ||
31 | } | ||
32 | |||
29 | if (activityToUndo.type === 'Follow') { | 33 | if (activityToUndo.type === 'Follow') { |
30 | return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo) | 34 | return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo) |
31 | } | 35 | } |
@@ -72,7 +76,9 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) { | |||
72 | } | 76 | } |
73 | 77 | ||
74 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { | 78 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { |
75 | const dislike = activity.object.object as DislikeObject | 79 | const dislike = activity.object.type === 'Dislike' |
80 | ? activity.object | ||
81 | : activity.object.object as DislikeObject | ||
76 | 82 | ||
77 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | 83 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) |
78 | 84 | ||
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts new file mode 100644 index 000000000..8f66d3630 --- /dev/null +++ b/server/lib/activitypub/process/process-view.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | ||
2 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
3 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
4 | import { Redis } from '../../redis' | ||
5 | import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' | ||
6 | |||
7 | async function processViewActivity (activity: ActivityView | ActivityCreate, byActor: ActorModel) { | ||
8 | return processCreateView(activity, byActor) | ||
9 | } | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
14 | processViewActivity | ||
15 | } | ||
16 | |||
17 | // --------------------------------------------------------------------------- | ||
18 | |||
19 | async function processCreateView (activity: ActivityView | ActivityCreate, byActor: ActorModel) { | ||
20 | const videoObject = activity.type === 'View' ? activity.object : (activity.object as ViewObject).object | ||
21 | |||
22 | const options = { | ||
23 | videoObject: videoObject, | ||
24 | fetchType: 'only-video' as 'only-video' | ||
25 | } | ||
26 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | ||
27 | |||
28 | await Redis.Instance.addVideoView(video.id) | ||
29 | |||
30 | if (video.isOwned()) { | ||
31 | // Don't resend the activity to the sender | ||
32 | const exceptions = [ byActor ] | ||
33 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
34 | } | ||
35 | } | ||
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts index bcc5cac7a..9dd241402 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 1 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
2 | import { checkUrlsSameHost, getAPUrl } from '../../../helpers/activitypub' | 2 | import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 4 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { processAcceptActivity } from './process-accept' | 5 | import { processAcceptActivity } from './process-accept' |
@@ -12,6 +12,9 @@ import { processRejectActivity } from './process-reject' | |||
12 | import { processUndoActivity } from './process-undo' | 12 | import { processUndoActivity } from './process-undo' |
13 | import { processUpdateActivity } from './process-update' | 13 | import { processUpdateActivity } from './process-update' |
14 | import { getOrCreateActorAndServerAndModel } from '../actor' | 14 | import { getOrCreateActorAndServerAndModel } from '../actor' |
15 | import { processDislikeActivity } from './process-dislike' | ||
16 | import { processFlagActivity } from './process-flag' | ||
17 | import { processViewActivity } from './process-view' | ||
15 | 18 | ||
16 | const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { | 19 | const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { |
17 | Create: processCreateActivity, | 20 | Create: processCreateActivity, |
@@ -22,7 +25,10 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac | |||
22 | Reject: processRejectActivity, | 25 | Reject: processRejectActivity, |
23 | Announce: processAnnounceActivity, | 26 | Announce: processAnnounceActivity, |
24 | Undo: processUndoActivity, | 27 | Undo: processUndoActivity, |
25 | Like: processLikeActivity | 28 | Like: processLikeActivity, |
29 | Dislike: processDislikeActivity, | ||
30 | Flag: processFlagActivity, | ||
31 | View: processViewActivity | ||
26 | } | 32 | } |
27 | 33 | ||
28 | async function processActivities ( | 34 | async function processActivities ( |
@@ -35,12 +41,12 @@ async function processActivities ( | |||
35 | const actorsCache: { [ url: string ]: ActorModel } = {} | 41 | const actorsCache: { [ url: string ]: ActorModel } = {} |
36 | 42 | ||
37 | for (const activity of activities) { | 43 | for (const activity of activities) { |
38 | if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { | 44 | if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) { |
39 | logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) | 45 | logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) |
40 | continue | 46 | continue |
41 | } | 47 | } |
42 | 48 | ||
43 | const actorUrl = getAPUrl(activity.actor) | 49 | const actorUrl = getAPId(activity.actor) |
44 | 50 | ||
45 | // When we fetch remote data, we don't have signature | 51 | // When we fetch remote data, we don't have signature |
46 | if (options.signatureActor && actorUrl !== options.signatureActor.url) { | 52 | if (options.signatureActor && actorUrl !== options.signatureActor.url) { |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index e3fca0a17..ef20e404c 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -3,9 +3,7 @@ import { ActivityAudience, ActivityCreate } from '../../../../shared/models/acti | |||
3 | import { VideoPrivacy } from '../../../../shared/models/videos' | 3 | import { VideoPrivacy } from '../../../../shared/models/videos' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 4 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { VideoModel } from '../../../models/video/video' | 5 | import { VideoModel } from '../../../models/video/video' |
6 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' | ||
9 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 7 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
10 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 8 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
11 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
@@ -25,31 +23,14 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) { | |||
25 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) | 23 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) |
26 | } | 24 | } |
27 | 25 | ||
28 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) { | 26 | async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) { |
29 | if (!video.VideoChannel.Account.Actor.serverId) return // Local | ||
30 | |||
31 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | ||
32 | |||
33 | logger.info('Creating job to send video abuse %s.', url) | ||
34 | |||
35 | // Custom audience, we only send the abuse to the origin instance | ||
36 | const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } | ||
37 | const createActivity = buildCreateActivity(url, byActor, videoAbuse.toActivityPubObject(), audience) | ||
38 | |||
39 | return unicastTo(createActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) | ||
40 | } | ||
41 | |||
42 | async function sendCreateCacheFile (byActor: ActorModel, fileRedundancy: VideoRedundancyModel) { | ||
43 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) | 27 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) |
44 | 28 | ||
45 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(fileRedundancy.VideoFile.Video.id) | ||
46 | const redundancyObject = fileRedundancy.toActivityPubObject() | ||
47 | |||
48 | return sendVideoRelatedCreateActivity({ | 29 | return sendVideoRelatedCreateActivity({ |
49 | byActor, | 30 | byActor, |
50 | video, | 31 | video, |
51 | url: fileRedundancy.url, | 32 | url: fileRedundancy.url, |
52 | object: redundancyObject | 33 | object: fileRedundancy.toActivityPubObject() |
53 | }) | 34 | }) |
54 | } | 35 | } |
55 | 36 | ||
@@ -91,37 +72,6 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio | |||
91 | return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl) | 72 | return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl) |
92 | } | 73 | } |
93 | 74 | ||
94 | async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
95 | logger.info('Creating job to send view of %s.', video.url) | ||
96 | |||
97 | const url = getVideoViewActivityPubUrl(byActor, video) | ||
98 | const viewActivity = buildViewActivity(url, byActor, video) | ||
99 | |||
100 | return sendVideoRelatedCreateActivity({ | ||
101 | // Use the server actor to send the view | ||
102 | byActor, | ||
103 | video, | ||
104 | url, | ||
105 | object: viewActivity, | ||
106 | transaction: t | ||
107 | }) | ||
108 | } | ||
109 | |||
110 | async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
111 | logger.info('Creating job to dislike %s.', video.url) | ||
112 | |||
113 | const url = getVideoDislikeActivityPubUrl(byActor, video) | ||
114 | const dislikeActivity = buildDislikeActivity(url, byActor, video) | ||
115 | |||
116 | return sendVideoRelatedCreateActivity({ | ||
117 | byActor, | ||
118 | video, | ||
119 | url, | ||
120 | object: dislikeActivity, | ||
121 | transaction: t | ||
122 | }) | ||
123 | } | ||
124 | |||
125 | function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { | 75 | function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { |
126 | if (!audience) audience = getAudience(byActor) | 76 | if (!audience) audience = getAudience(byActor) |
127 | 77 | ||
@@ -136,33 +86,11 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud | |||
136 | ) | 86 | ) |
137 | } | 87 | } |
138 | 88 | ||
139 | function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel) { | ||
140 | return { | ||
141 | id: url, | ||
142 | type: 'Dislike', | ||
143 | actor: byActor.url, | ||
144 | object: video.url | ||
145 | } | ||
146 | } | ||
147 | |||
148 | function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) { | ||
149 | return { | ||
150 | id: url, | ||
151 | type: 'View', | ||
152 | actor: byActor.url, | ||
153 | object: video.url | ||
154 | } | ||
155 | } | ||
156 | |||
157 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
158 | 90 | ||
159 | export { | 91 | export { |
160 | sendCreateVideo, | 92 | sendCreateVideo, |
161 | sendVideoAbuse, | ||
162 | buildCreateActivity, | 93 | buildCreateActivity, |
163 | sendCreateView, | ||
164 | sendCreateDislike, | ||
165 | buildDislikeActivity, | ||
166 | sendCreateVideoComment, | 94 | sendCreateVideoComment, |
167 | sendCreateCacheFile | 95 | sendCreateCacheFile |
168 | } | 96 | } |
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts new file mode 100644 index 000000000..a88436f2c --- /dev/null +++ b/server/lib/activitypub/send/send-dislike.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
2 | import { ActorModel } from '../../../models/activitypub/actor' | ||
3 | import { VideoModel } from '../../../models/video/video' | ||
4 | import { getVideoDislikeActivityPubUrl } from '../url' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub' | ||
7 | import { sendVideoRelatedActivity } from './utils' | ||
8 | import { audiencify, getAudience } from '../audience' | ||
9 | |||
10 | async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
11 | logger.info('Creating job to dislike %s.', video.url) | ||
12 | |||
13 | const activityBuilder = (audience: ActivityAudience) => { | ||
14 | const url = getVideoDislikeActivityPubUrl(byActor, video) | ||
15 | |||
16 | return buildDislikeActivity(url, byActor, video, audience) | ||
17 | } | ||
18 | |||
19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) | ||
20 | } | ||
21 | |||
22 | function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityDislike { | ||
23 | if (!audience) audience = getAudience(byActor) | ||
24 | |||
25 | return audiencify( | ||
26 | { | ||
27 | id: url, | ||
28 | type: 'Dislike' as 'Dislike', | ||
29 | actor: byActor.url, | ||
30 | object: video.url | ||
31 | }, | ||
32 | audience | ||
33 | ) | ||
34 | } | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | sendDislike, | ||
40 | buildDislikeActivity | ||
41 | } | ||
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts new file mode 100644 index 000000000..96a7311b9 --- /dev/null +++ b/server/lib/activitypub/send/send-flag.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | ||
2 | import { VideoModel } from '../../../models/video/video' | ||
3 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
4 | import { getVideoAbuseActivityPubUrl } from '../url' | ||
5 | import { unicastTo } from './utils' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' | ||
8 | import { audiencify, getAudience } from '../audience' | ||
9 | |||
10 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) { | ||
11 | if (!video.VideoChannel.Account.Actor.serverId) return // Local user | ||
12 | |||
13 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | ||
14 | |||
15 | logger.info('Creating job to send video abuse %s.', url) | ||
16 | |||
17 | // Custom audience, we only send the abuse to the origin instance | ||
18 | const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } | ||
19 | const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience) | ||
20 | |||
21 | return unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) | ||
22 | } | ||
23 | |||
24 | function buildFlagActivity (url: string, byActor: ActorModel, videoAbuse: VideoAbuseModel, audience: ActivityAudience): ActivityFlag { | ||
25 | if (!audience) audience = getAudience(byActor) | ||
26 | |||
27 | const activity = Object.assign( | ||
28 | { id: url, actor: byActor.url }, | ||
29 | videoAbuse.toActivityPubObject() | ||
30 | ) | ||
31 | |||
32 | return audiencify(activity, audience) | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | export { | ||
38 | sendVideoAbuse | ||
39 | } | ||
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index bf1b6e117..ecbf605d6 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize' | |||
2 | import { | 2 | import { |
3 | ActivityAnnounce, | 3 | ActivityAnnounce, |
4 | ActivityAudience, | 4 | ActivityAudience, |
5 | ActivityCreate, | 5 | ActivityCreate, ActivityDislike, |
6 | ActivityFollow, | 6 | ActivityFollow, |
7 | ActivityLike, | 7 | ActivityLike, |
8 | ActivityUndo | 8 | ActivityUndo |
@@ -13,13 +13,14 @@ import { VideoModel } from '../../../models/video/video' | |||
13 | import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' | 13 | import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' |
14 | import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 14 | import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
15 | import { audiencify, getAudience } from '../audience' | 15 | import { audiencify, getAudience } from '../audience' |
16 | import { buildCreateActivity, buildDislikeActivity } from './send-create' | 16 | import { buildCreateActivity } from './send-create' |
17 | import { buildFollowActivity } from './send-follow' | 17 | import { buildFollowActivity } from './send-follow' |
18 | import { buildLikeActivity } from './send-like' | 18 | import { buildLikeActivity } from './send-like' |
19 | import { VideoShareModel } from '../../../models/video/video-share' | 19 | import { VideoShareModel } from '../../../models/video/video-share' |
20 | import { buildAnnounceWithVideoAudience } from './send-announce' | 20 | import { buildAnnounceWithVideoAudience } from './send-announce' |
21 | import { logger } from '../../../helpers/logger' | 21 | import { logger } from '../../../helpers/logger' |
22 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 22 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
23 | import { buildDislikeActivity } from './send-dislike' | ||
23 | 24 | ||
24 | async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { | 25 | async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { |
25 | const me = actorFollow.ActorFollower | 26 | const me = actorFollow.ActorFollower |
@@ -65,15 +66,15 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans | |||
65 | 66 | ||
66 | const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) | 67 | const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) |
67 | const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video) | 68 | const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video) |
68 | const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity) | ||
69 | 69 | ||
70 | return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) | 70 | return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) |
71 | } | 71 | } |
72 | 72 | ||
73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { | 73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { |
74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) | 74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) |
75 | 75 | ||
76 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 76 | const videoId = redundancyModel.getVideo().id |
77 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
77 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) | 78 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) |
78 | 79 | ||
79 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) | 80 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) |
@@ -94,7 +95,7 @@ export { | |||
94 | function undoActivityData ( | 95 | function undoActivityData ( |
95 | url: string, | 96 | url: string, |
96 | byActor: ActorModel, | 97 | byActor: ActorModel, |
97 | object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, | 98 | object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, |
98 | audience?: ActivityAudience | 99 | audience?: ActivityAudience |
99 | ): ActivityUndo { | 100 | ): ActivityUndo { |
100 | if (!audience) audience = getAudience(byActor) | 101 | if (!audience) audience = getAudience(byActor) |
@@ -114,7 +115,7 @@ async function sendUndoVideoRelatedActivity (options: { | |||
114 | byActor: ActorModel, | 115 | byActor: ActorModel, |
115 | video: VideoModel, | 116 | video: VideoModel, |
116 | url: string, | 117 | url: string, |
117 | activity: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, | 118 | activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, |
118 | transaction: Transaction | 119 | transaction: Transaction |
119 | }) { | 120 | }) { |
120 | const activityBuilder = (audience: ActivityAudience) => { | 121 | const activityBuilder = (audience: ActivityAudience) => { |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index a68f03edf..839f66470 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -61,7 +61,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod | |||
61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { | 61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { |
62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) | 62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) |
63 | 63 | ||
64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id) |
65 | 65 | ||
66 | const activityBuilder = (audience: ActivityAudience) => { | 66 | const activityBuilder = (audience: ActivityAudience) => { |
67 | const redundancyObject = redundancyModel.toActivityPubObject() | 67 | const redundancyObject = redundancyModel.toActivityPubObject() |
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts new file mode 100644 index 000000000..8ad126be0 --- /dev/null +++ b/server/lib/activitypub/send/send-view.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
2 | import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' | ||
3 | import { ActorModel } from '../../../models/activitypub/actor' | ||
4 | import { VideoModel } from '../../../models/video/video' | ||
5 | import { getVideoLikeActivityPubUrl } from '../url' | ||
6 | import { sendVideoRelatedActivity } from './utils' | ||
7 | import { audiencify, getAudience } from '../audience' | ||
8 | import { logger } from '../../../helpers/logger' | ||
9 | |||
10 | async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
11 | logger.info('Creating job to send view of %s.', video.url) | ||
12 | |||
13 | const activityBuilder = (audience: ActivityAudience) => { | ||
14 | const url = getVideoLikeActivityPubUrl(byActor, video) | ||
15 | |||
16 | return buildViewActivity(url, byActor, video, audience) | ||
17 | } | ||
18 | |||
19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) | ||
20 | } | ||
21 | |||
22 | function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityView { | ||
23 | if (!audience) audience = getAudience(byActor) | ||
24 | |||
25 | return audiencify( | ||
26 | { | ||
27 | id: url, | ||
28 | type: 'View' as 'View', | ||
29 | actor: byActor.url, | ||
30 | object: video.url | ||
31 | }, | ||
32 | audience | ||
33 | ) | ||
34 | } | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | sendView | ||
40 | } | ||
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 5dcba778c..1767df0ae 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -11,7 +11,7 @@ import { doRequest } from '../../helpers/requests' | |||
11 | import { getOrCreateActorAndServerAndModel } from './actor' | 11 | import { getOrCreateActorAndServerAndModel } from './actor' |
12 | import { logger } from '../../helpers/logger' | 12 | import { logger } from '../../helpers/logger' |
13 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' | 13 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' |
14 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' | 14 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
15 | 15 | ||
16 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { | 16 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { |
17 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 17 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined |
@@ -41,7 +41,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) { | |||
41 | }) | 41 | }) |
42 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 42 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
43 | 43 | ||
44 | const actorUrl = getAPUrl(body.actor) | 44 | const actorUrl = getAPId(body.actor) |
45 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | 45 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { |
46 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | 46 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) |
47 | } | 47 | } |
@@ -78,7 +78,7 @@ async function shareByServer (video: VideoModel, t: Transaction) { | |||
78 | const serverActor = await getServerActor() | 78 | const serverActor = await getServerActor() |
79 | 79 | ||
80 | const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) | 80 | const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) |
81 | return VideoShareModel.findOrCreate({ | 81 | const [ serverShare ] = await VideoShareModel.findOrCreate({ |
82 | defaults: { | 82 | defaults: { |
83 | actorId: serverActor.id, | 83 | actorId: serverActor.id, |
84 | videoId: video.id, | 84 | videoId: video.id, |
@@ -88,16 +88,14 @@ async function shareByServer (video: VideoModel, t: Transaction) { | |||
88 | url: serverShareUrl | 88 | url: serverShareUrl |
89 | }, | 89 | }, |
90 | transaction: t | 90 | transaction: t |
91 | }).then(([ serverShare, created ]) => { | ||
92 | if (created) return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
93 | |||
94 | return undefined | ||
95 | }) | 91 | }) |
92 | |||
93 | return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
96 | } | 94 | } |
97 | 95 | ||
98 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { | 96 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { |
99 | const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) | 97 | const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) |
100 | return VideoShareModel.findOrCreate({ | 98 | const [ videoChannelShare ] = await VideoShareModel.findOrCreate({ |
101 | defaults: { | 99 | defaults: { |
102 | actorId: video.VideoChannel.actorId, | 100 | actorId: video.VideoChannel.actorId, |
103 | videoId: video.id, | 101 | videoId: video.id, |
@@ -107,11 +105,9 @@ async function shareByVideoChannel (video: VideoModel, t: Transaction) { | |||
107 | url: videoChannelShareUrl | 105 | url: videoChannelShareUrl |
108 | }, | 106 | }, |
109 | transaction: t | 107 | transaction: t |
110 | }).then(([ videoChannelShare, created ]) => { | ||
111 | if (created) return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
112 | |||
113 | return undefined | ||
114 | }) | 108 | }) |
109 | |||
110 | return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
115 | } | 111 | } |
116 | 112 | ||
117 | async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { | 113 | async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { |
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 38f15448c..4229fe094 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts | |||
@@ -5,6 +5,8 @@ import { VideoModel } from '../../models/video/video' | |||
5 | import { VideoAbuseModel } from '../../models/video/video-abuse' | 5 | import { VideoAbuseModel } from '../../models/video/video-abuse' |
6 | import { VideoCommentModel } from '../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../models/video/video-comment' |
7 | import { VideoFileModel } from '../../models/video/video-file' | 7 | import { VideoFileModel } from '../../models/video/video-file' |
8 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | ||
9 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
8 | 10 | ||
9 | function getVideoActivityPubUrl (video: VideoModel) { | 11 | function getVideoActivityPubUrl (video: VideoModel) { |
10 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 12 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid |
@@ -16,6 +18,10 @@ function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { | |||
16 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` | 18 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` |
17 | } | 19 | } |
18 | 20 | ||
21 | function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) { | ||
22 | return `${CONFIG.WEBSERVER.URL}/redundancy/video-playlists/${playlist.getStringType()}/${video.uuid}` | ||
23 | } | ||
24 | |||
19 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { | 25 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { |
20 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id | 26 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id |
21 | } | 27 | } |
@@ -92,6 +98,7 @@ function getUndoActivityPubUrl (originalUrl: string) { | |||
92 | 98 | ||
93 | export { | 99 | export { |
94 | getVideoActivityPubUrl, | 100 | getVideoActivityPubUrl, |
101 | getVideoCacheStreamingPlaylistActivityPubUrl, | ||
95 | getVideoChannelActivityPubUrl, | 102 | getVideoChannelActivityPubUrl, |
96 | getAccountActivityPubUrl, | 103 | getAccountActivityPubUrl, |
97 | getVideoAbuseActivityPubUrl, | 104 | getVideoAbuseActivityPubUrl, |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index 2cce67f0c..7aac79118 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { AccountModel } from '../../models/account/account' | 2 | import { AccountModel } from '../../models/account/account' |
3 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
4 | import { sendCreateDislike, sendLike, sendUndoDislike, sendUndoLike } from './send' | 4 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' |
5 | import { VideoRateType } from '../../../shared/models/videos' | 5 | import { VideoRateType } from '../../../shared/models/videos' |
6 | import * as Bluebird from 'bluebird' | 6 | import * as Bluebird from 'bluebird' |
7 | import { getOrCreateActorAndServerAndModel } from './actor' | 7 | import { getOrCreateActorAndServerAndModel } from './actor' |
@@ -9,9 +9,10 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate' | |||
9 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' | 10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' |
11 | import { doRequest } from '../../helpers/requests' | 11 | import { doRequest } from '../../helpers/requests' |
12 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' | 12 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
13 | import { ActorModel } from '../../models/activitypub/actor' | 13 | import { ActorModel } from '../../models/activitypub/actor' |
14 | import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' | 14 | import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' |
15 | import { sendDislike } from './send/send-dislike' | ||
15 | 16 | ||
16 | async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) { | 17 | async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) { |
17 | let rateCounts = 0 | 18 | let rateCounts = 0 |
@@ -26,7 +27,7 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa | |||
26 | }) | 27 | }) |
27 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 28 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
28 | 29 | ||
29 | const actorUrl = getAPUrl(body.actor) | 30 | const actorUrl = getAPId(body.actor) |
30 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | 31 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { |
31 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | 32 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) |
32 | } | 33 | } |
@@ -82,7 +83,7 @@ async function sendVideoRateChange (account: AccountModel, | |||
82 | // Like | 83 | // Like |
83 | if (likes > 0) await sendLike(actor, video, t) | 84 | if (likes > 0) await sendLike(actor, video, t) |
84 | // Dislike | 85 | // Dislike |
85 | if (dislikes > 0) await sendCreateDislike(actor, video, t) | 86 | if (dislikes > 0) await sendDislike(actor, video, t) |
86 | } | 87 | } |
87 | 88 | ||
88 | function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) { | 89 | function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) { |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 893768769..710929aac 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -2,7 +2,14 @@ import * as Bluebird from 'bluebird' | |||
2 | import * as sequelize from 'sequelize' | 2 | import * as sequelize from 'sequelize' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as request from 'request' | 4 | import * as request from 'request' |
5 | import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' | 5 | import { |
6 | ActivityIconObject, | ||
7 | ActivityPlaylistSegmentHashesObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityUrlObject, | ||
10 | ActivityVideoUrlObject, | ||
11 | VideoState | ||
12 | } from '../../../shared/index' | ||
6 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | 14 | import { VideoPrivacy } from '../../../shared/models/videos' |
8 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 15 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
@@ -28,8 +35,11 @@ import { createRates } from './video-rates' | |||
28 | import { addVideoShares, shareVideoByServerAndChannel } from './share' | 35 | import { addVideoShares, shareVideoByServerAndChannel } from './share' |
29 | import { AccountModel } from '../../models/account/account' | 36 | import { AccountModel } from '../../models/account/account' |
30 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 37 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
31 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' | 38 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
32 | import { Notifier } from '../notifier' | 39 | import { Notifier } from '../notifier' |
40 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
41 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
42 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | ||
33 | 43 | ||
34 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 44 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
35 | // If the video is not private and published, we federate it | 45 | // If the video is not private and published, we federate it |
@@ -155,7 +165,7 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid | |||
155 | } | 165 | } |
156 | 166 | ||
157 | async function getOrCreateVideoAndAccountAndChannel (options: { | 167 | async function getOrCreateVideoAndAccountAndChannel (options: { |
158 | videoObject: VideoTorrentObject | string, | 168 | videoObject: { id: string } | string, |
159 | syncParam?: SyncParam, | 169 | syncParam?: SyncParam, |
160 | fetchType?: VideoFetchByUrlType, | 170 | fetchType?: VideoFetchByUrlType, |
161 | allowRefresh?: boolean // true by default | 171 | allowRefresh?: boolean // true by default |
@@ -166,7 +176,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
166 | const allowRefresh = options.allowRefresh !== false | 176 | const allowRefresh = options.allowRefresh !== false |
167 | 177 | ||
168 | // Get video url | 178 | // Get video url |
169 | const videoUrl = getAPUrl(options.videoObject) | 179 | const videoUrl = getAPId(options.videoObject) |
170 | 180 | ||
171 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 181 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) |
172 | if (videoFromDatabase) { | 182 | if (videoFromDatabase) { |
@@ -179,7 +189,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
179 | } | 189 | } |
180 | 190 | ||
181 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | 191 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) |
182 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) | 192 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) |
183 | } | 193 | } |
184 | 194 | ||
185 | return { video: videoFromDatabase, created: false } | 195 | return { video: videoFromDatabase, created: false } |
@@ -233,6 +243,7 @@ async function updateVideoFromAP (options: { | |||
233 | options.video.set('support', videoData.support) | 243 | options.video.set('support', videoData.support) |
234 | options.video.set('nsfw', videoData.nsfw) | 244 | options.video.set('nsfw', videoData.nsfw) |
235 | options.video.set('commentsEnabled', videoData.commentsEnabled) | 245 | options.video.set('commentsEnabled', videoData.commentsEnabled) |
246 | options.video.set('downloadEnabled', videoData.downloadEnabled) | ||
236 | options.video.set('waitTranscoding', videoData.waitTranscoding) | 247 | options.video.set('waitTranscoding', videoData.waitTranscoding) |
237 | options.video.set('state', videoData.state) | 248 | options.video.set('state', videoData.state) |
238 | options.video.set('duration', videoData.duration) | 249 | options.video.set('duration', videoData.duration) |
@@ -264,6 +275,25 @@ async function updateVideoFromAP (options: { | |||
264 | } | 275 | } |
265 | 276 | ||
266 | { | 277 | { |
278 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject) | ||
279 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
280 | |||
281 | // Remove video files that do not exist anymore | ||
282 | const destroyTasks = options.video.VideoStreamingPlaylists | ||
283 | .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) | ||
284 | .map(f => f.destroy(sequelizeOptions)) | ||
285 | await Promise.all(destroyTasks) | ||
286 | |||
287 | // Update or add other one | ||
288 | const upsertTasks = streamingPlaylistAttributes.map(a => { | ||
289 | return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) | ||
290 | .then(([ streamingPlaylist ]) => streamingPlaylist) | ||
291 | }) | ||
292 | |||
293 | options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks) | ||
294 | } | ||
295 | |||
296 | { | ||
267 | // Update Tags | 297 | // Update Tags |
268 | const tags = options.videoObject.tag.map(tag => tag.name) | 298 | const tags = options.videoObject.tag.map(tag => tag.name) |
269 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 299 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
@@ -367,13 +397,25 @@ export { | |||
367 | 397 | ||
368 | // --------------------------------------------------------------------------- | 398 | // --------------------------------------------------------------------------- |
369 | 399 | ||
370 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 400 | function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { |
371 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | 401 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
372 | 402 | ||
373 | const urlMediaType = url.mediaType || url.mimeType | 403 | const urlMediaType = url.mediaType || url.mimeType |
374 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | 404 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') |
375 | } | 405 | } |
376 | 406 | ||
407 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | ||
408 | const urlMediaType = url.mediaType || url.mimeType | ||
409 | |||
410 | return urlMediaType === 'application/x-mpegURL' | ||
411 | } | ||
412 | |||
413 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | ||
414 | const urlMediaType = tag.mediaType || tag.mimeType | ||
415 | |||
416 | return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' | ||
417 | } | ||
418 | |||
377 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 419 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { |
378 | logger.debug('Adding remote video %s.', videoObject.id) | 420 | logger.debug('Adding remote video %s.', videoObject.id) |
379 | 421 | ||
@@ -394,8 +436,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
394 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 436 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
395 | await Promise.all(videoFilePromises) | 437 | await Promise.all(videoFilePromises) |
396 | 438 | ||
439 | const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject) | ||
440 | const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) | ||
441 | await Promise.all(playlistPromises) | ||
442 | |||
397 | // Process tags | 443 | // Process tags |
398 | const tags = videoObject.tag.map(t => t.name) | 444 | const tags = videoObject.tag |
445 | .filter(t => t.type === 'Hashtag') | ||
446 | .map(t => t.name) | ||
399 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 447 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
400 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | 448 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) |
401 | 449 | ||
@@ -456,6 +504,7 @@ async function videoActivityObjectToDBAttributes ( | |||
456 | support, | 504 | support, |
457 | nsfw: videoObject.sensitive, | 505 | nsfw: videoObject.sensitive, |
458 | commentsEnabled: videoObject.commentsEnabled, | 506 | commentsEnabled: videoObject.commentsEnabled, |
507 | downloadEnabled: videoObject.downloadEnabled, | ||
459 | waitTranscoding: videoObject.waitTranscoding, | 508 | waitTranscoding: videoObject.waitTranscoding, |
460 | state: videoObject.state, | 509 | state: videoObject.state, |
461 | channelId: videoChannel.id, | 510 | channelId: videoChannel.id, |
@@ -473,13 +522,13 @@ async function videoActivityObjectToDBAttributes ( | |||
473 | } | 522 | } |
474 | 523 | ||
475 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | 524 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { |
476 | const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] | 525 | const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] |
477 | 526 | ||
478 | if (fileUrls.length === 0) { | 527 | if (fileUrls.length === 0) { |
479 | throw new Error('Cannot find video files for ' + video.url) | 528 | throw new Error('Cannot find video files for ' + video.url) |
480 | } | 529 | } |
481 | 530 | ||
482 | const attributes: VideoFileModel[] = [] | 531 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] |
483 | for (const fileUrl of fileUrls) { | 532 | for (const fileUrl of fileUrls) { |
484 | // Fetch associated magnet uri | 533 | // Fetch associated magnet uri |
485 | const magnet = videoObject.url.find(u => { | 534 | const magnet = videoObject.url.find(u => { |
@@ -502,7 +551,45 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
502 | size: fileUrl.size, | 551 | size: fileUrl.size, |
503 | videoId: video.id, | 552 | videoId: video.id, |
504 | fps: fileUrl.fps || -1 | 553 | fps: fileUrl.fps || -1 |
505 | } as VideoFileModel | 554 | } |
555 | |||
556 | attributes.push(attribute) | ||
557 | } | ||
558 | |||
559 | return attributes | ||
560 | } | ||
561 | |||
562 | function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | ||
563 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | ||
564 | if (playlistUrls.length === 0) return [] | ||
565 | |||
566 | const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = [] | ||
567 | for (const playlistUrlObject of playlistUrls) { | ||
568 | const p2pMediaLoaderInfohashes = playlistUrlObject.tag | ||
569 | .filter(t => t.type === 'Infohash') | ||
570 | .map(t => t.name) | ||
571 | if (p2pMediaLoaderInfohashes.length === 0) { | ||
572 | logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
573 | continue | ||
574 | } | ||
575 | |||
576 | const segmentsSha256UrlObject = playlistUrlObject.tag | ||
577 | .find(t => { | ||
578 | return isAPPlaylistSegmentHashesUrlObject(t) | ||
579 | }) as ActivityPlaylistSegmentHashesObject | ||
580 | if (!segmentsSha256UrlObject) { | ||
581 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
582 | continue | ||
583 | } | ||
584 | |||
585 | const attribute = { | ||
586 | type: VideoStreamingPlaylistType.HLS, | ||
587 | playlistUrl: playlistUrlObject.href, | ||
588 | segmentsSha256Url: segmentsSha256UrlObject.href, | ||
589 | p2pMediaLoaderInfohashes, | ||
590 | videoId: video.id | ||
591 | } | ||
592 | |||
506 | attributes.push(attribute) | 593 | attributes.push(attribute) |
507 | } | 594 | } |
508 | 595 | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 1875ec1fc..b2c376e20 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' | 3 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' |
4 | import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, STATIC_PATHS } from '../initializers' | 4 | import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { escapeHTML } from '../helpers/core-utils' | 6 | import { escapeHTML } from '../helpers/core-utils' |
7 | import { VideoModel } from '../models/video/video' | 7 | import { VideoModel } from '../models/video/video' |
@@ -187,8 +187,8 @@ export class ClientHtml { | |||
187 | // Schema.org | 187 | // Schema.org |
188 | tagsString += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>` | 188 | tagsString += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>` |
189 | 189 | ||
190 | // SEO | 190 | // SEO, use origin video url so Google does not index remote videos |
191 | tagsString += `<link rel="canonical" href="${videoUrl}" />` | 191 | tagsString += `<link rel="canonical" href="${video.url}" />` |
192 | 192 | ||
193 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.OPENGRAPH_AND_OEMBED, tagsString) | 193 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.OPENGRAPH_AND_OEMBED, tagsString) |
194 | } | 194 | } |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index f384a254e..672414cc0 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -296,9 +296,9 @@ class Emailer { | |||
296 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 296 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
297 | } | 297 | } |
298 | 298 | ||
299 | addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { | 299 | addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { |
300 | const text = `Hi dear user,\n\n` + | 300 | const text = `Hi dear user,\n\n` + |
301 | `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + | 301 | `A reset password procedure for your account ${to} has been requested on ${CONFIG.WEBSERVER.HOST} ` + |
302 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + | 302 | `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + |
303 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | 303 | `If you are not the person who initiated this request, please ignore this email.\n\n` + |
304 | `Cheers,\n` + | 304 | `Cheers,\n` + |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts new file mode 100644 index 000000000..3575981f4 --- /dev/null +++ b/server/lib/hls.ts | |||
@@ -0,0 +1,164 @@ | |||
1 | import { VideoModel } from '../models/video/video' | ||
2 | import { basename, join, dirname } from 'path' | ||
3 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' | ||
4 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' | ||
5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' | ||
6 | import { sha256 } from '../helpers/core-utils' | ||
7 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
8 | import { logger } from '../helpers/logger' | ||
9 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' | ||
10 | import { generateRandomString } from '../helpers/utils' | ||
11 | import { flatten, uniq } from 'lodash' | ||
12 | |||
13 | async function updateMasterHLSPlaylist (video: VideoModel) { | ||
14 | const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
15 | const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] | ||
16 | const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
17 | |||
18 | for (const file of video.VideoFiles) { | ||
19 | // If we did not generated a playlist for this resolution, skip | ||
20 | const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
21 | if (await pathExists(filePlaylistPath) === false) continue | ||
22 | |||
23 | const videoFilePath = video.getVideoFilePath(file) | ||
24 | |||
25 | const size = await getVideoFileSize(videoFilePath) | ||
26 | |||
27 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | ||
28 | const resolution = `RESOLUTION=${size.width}x${size.height}` | ||
29 | |||
30 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | ||
31 | if (file.fps) line += ',FRAME-RATE=' + file.fps | ||
32 | |||
33 | masterPlaylists.push(line) | ||
34 | masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
35 | } | ||
36 | |||
37 | await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') | ||
38 | } | ||
39 | |||
40 | async function updateSha256Segments (video: VideoModel) { | ||
41 | const json: { [filename: string]: { [range: string]: string } } = {} | ||
42 | |||
43 | const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
44 | |||
45 | // For all the resolutions available for this video | ||
46 | for (const file of video.VideoFiles) { | ||
47 | const rangeHashes: { [range: string]: string } = {} | ||
48 | |||
49 | const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)) | ||
50 | const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) | ||
51 | |||
52 | // Maybe the playlist is not generated for this resolution yet | ||
53 | if (!await pathExists(playlistPath)) continue | ||
54 | |||
55 | const playlistContent = await readFile(playlistPath) | ||
56 | const ranges = getRangesFromPlaylist(playlistContent.toString()) | ||
57 | |||
58 | const fd = await open(videoPath, 'r') | ||
59 | for (const range of ranges) { | ||
60 | const buf = Buffer.alloc(range.length) | ||
61 | await read(fd, buf, 0, range.length, range.offset) | ||
62 | |||
63 | rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf) | ||
64 | } | ||
65 | await close(fd) | ||
66 | |||
67 | const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution) | ||
68 | json[videoFilename] = rangeHashes | ||
69 | } | ||
70 | |||
71 | const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | ||
72 | await outputJSON(outputPath, json) | ||
73 | } | ||
74 | |||
75 | function getRangesFromPlaylist (playlistContent: string) { | ||
76 | const ranges: { offset: number, length: number }[] = [] | ||
77 | const lines = playlistContent.split('\n') | ||
78 | const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/ | ||
79 | |||
80 | for (const line of lines) { | ||
81 | const captured = regex.exec(line) | ||
82 | |||
83 | if (captured) { | ||
84 | ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) }) | ||
85 | } | ||
86 | } | ||
87 | |||
88 | return ranges | ||
89 | } | ||
90 | |||
91 | function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) { | ||
92 | let timer | ||
93 | |||
94 | logger.info('Importing HLS playlist %s', playlistUrl) | ||
95 | |||
96 | return new Promise<string>(async (res, rej) => { | ||
97 | const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10)) | ||
98 | |||
99 | await ensureDir(tmpDirectory) | ||
100 | |||
101 | timer = setTimeout(() => { | ||
102 | deleteTmpDirectory(tmpDirectory) | ||
103 | |||
104 | return rej(new Error('HLS download timeout.')) | ||
105 | }, timeout) | ||
106 | |||
107 | try { | ||
108 | // Fetch master playlist | ||
109 | const subPlaylistUrls = await fetchUniqUrls(playlistUrl) | ||
110 | |||
111 | const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u)) | ||
112 | const fileUrls = uniq(flatten(await Promise.all(subRequests))) | ||
113 | |||
114 | logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls }) | ||
115 | |||
116 | for (const fileUrl of fileUrls) { | ||
117 | const destPath = join(tmpDirectory, basename(fileUrl)) | ||
118 | |||
119 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath) | ||
120 | } | ||
121 | |||
122 | clearTimeout(timer) | ||
123 | |||
124 | await move(tmpDirectory, destinationDir, { overwrite: true }) | ||
125 | |||
126 | return res() | ||
127 | } catch (err) { | ||
128 | deleteTmpDirectory(tmpDirectory) | ||
129 | |||
130 | return rej(err) | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | function deleteTmpDirectory (directory: string) { | ||
135 | remove(directory) | ||
136 | .catch(err => logger.error('Cannot delete path on HLS download error.', { err })) | ||
137 | } | ||
138 | |||
139 | async function fetchUniqUrls (playlistUrl: string) { | ||
140 | const { body } = await doRequest<string>({ uri: playlistUrl }) | ||
141 | |||
142 | if (!body) return [] | ||
143 | |||
144 | const urls = body.split('\n') | ||
145 | .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4')) | ||
146 | .map(url => { | ||
147 | if (url.startsWith('http://') || url.startsWith('https://')) return url | ||
148 | |||
149 | return `${dirname(playlistUrl)}/${url}` | ||
150 | }) | ||
151 | |||
152 | return uniq(urls) | ||
153 | } | ||
154 | } | ||
155 | |||
156 | // --------------------------------------------------------------------------- | ||
157 | |||
158 | export { | ||
159 | updateMasterHLSPlaylist, | ||
160 | updateSha256Segments, | ||
161 | downloadPlaylistSegments | ||
162 | } | ||
163 | |||
164 | // --------------------------------------------------------------------------- | ||
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts index 671b0f487..454b975fe 100644 --- a/server/lib/job-queue/handlers/activitypub-refresher.ts +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts | |||
@@ -1,30 +1,33 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import { fetchVideoByUrl } from '../../../helpers/video' | 3 | import { fetchVideoByUrl } from '../../../helpers/video' |
4 | import { refreshVideoIfNeeded } from '../../activitypub' | 4 | import { refreshVideoIfNeeded, refreshActorIfNeeded } from '../../activitypub' |
5 | import { ActorModel } from '../../../models/activitypub/actor' | ||
5 | 6 | ||
6 | export type RefreshPayload = { | 7 | export type RefreshPayload = { |
7 | videoUrl: string | 8 | type: 'video' | 'actor' |
8 | type: 'video' | 9 | url: string |
9 | } | 10 | } |
10 | 11 | ||
11 | async function refreshAPObject (job: Bull.Job) { | 12 | async function refreshAPObject (job: Bull.Job) { |
12 | const payload = job.data as RefreshPayload | 13 | const payload = job.data as RefreshPayload |
13 | 14 | ||
14 | logger.info('Processing AP refresher in job %d for video %s.', job.id, payload.videoUrl) | 15 | logger.info('Processing AP refresher in job %d for %s.', job.id, payload.url) |
15 | 16 | ||
16 | if (payload.type === 'video') return refreshAPVideo(payload.videoUrl) | 17 | if (payload.type === 'video') return refreshVideo(payload.url) |
18 | if (payload.type === 'actor') return refreshActor(payload.url) | ||
17 | } | 19 | } |
18 | 20 | ||
19 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
20 | 22 | ||
21 | export { | 23 | export { |
24 | refreshActor, | ||
22 | refreshAPObject | 25 | refreshAPObject |
23 | } | 26 | } |
24 | 27 | ||
25 | // --------------------------------------------------------------------------- | 28 | // --------------------------------------------------------------------------- |
26 | 29 | ||
27 | async function refreshAPVideo (videoUrl: string) { | 30 | async function refreshVideo (videoUrl: string) { |
28 | const fetchType = 'all' as 'all' | 31 | const fetchType = 'all' as 'all' |
29 | const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } | 32 | const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } |
30 | 33 | ||
@@ -39,3 +42,13 @@ async function refreshAPVideo (videoUrl: string) { | |||
39 | await refreshVideoIfNeeded(refreshOptions) | 42 | await refreshVideoIfNeeded(refreshOptions) |
40 | } | 43 | } |
41 | } | 44 | } |
45 | |||
46 | async function refreshActor (actorUrl: string) { | ||
47 | const fetchType = 'all' as 'all' | ||
48 | const actor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorUrl) | ||
49 | |||
50 | if (actor) { | ||
51 | await refreshActorIfNeeded(actor, fetchType) | ||
52 | } | ||
53 | |||
54 | } | ||
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index 593e43cc5..04983155c 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts | |||
@@ -5,17 +5,18 @@ import { VideoModel } from '../../../models/video/video' | |||
5 | import { JobQueue } from '../job-queue' | 5 | import { JobQueue } from '../job-queue' |
6 | import { federateVideoIfNeeded } from '../../activitypub' | 6 | import { federateVideoIfNeeded } from '../../activitypub' |
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
8 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript, CONFIG } from '../../../initializers' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' | 11 | import { generateHlsPlaylist, importVideoFile, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | 12 | import { Notifier } from '../../notifier' |
13 | 13 | ||
14 | export type VideoFilePayload = { | 14 | export type VideoFilePayload = { |
15 | videoUUID: string | 15 | videoUUID: string |
16 | isNewVideo?: boolean | ||
17 | resolution?: VideoResolution | 16 | resolution?: VideoResolution |
17 | isNewVideo?: boolean | ||
18 | isPortraitMode?: boolean | 18 | isPortraitMode?: boolean |
19 | generateHlsPlaylist?: boolean | ||
19 | } | 20 | } |
20 | 21 | ||
21 | export type VideoFileImportPayload = { | 22 | export type VideoFileImportPayload = { |
@@ -51,21 +52,38 @@ async function processVideoFile (job: Bull.Job) { | |||
51 | return undefined | 52 | return undefined |
52 | } | 53 | } |
53 | 54 | ||
54 | // Transcoding in other resolution | 55 | if (payload.generateHlsPlaylist) { |
55 | if (payload.resolution) { | 56 | await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) |
57 | |||
58 | await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) | ||
59 | } else if (payload.resolution) { // Transcoding in other resolution | ||
56 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) | 60 | await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) |
57 | 61 | ||
58 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) | 62 | await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video, payload) |
59 | } else { | 63 | } else { |
60 | await optimizeVideofile(video) | 64 | await optimizeVideofile(video) |
61 | 65 | ||
62 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) | 66 | await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload) |
63 | } | 67 | } |
64 | 68 | ||
65 | return video | 69 | return video |
66 | } | 70 | } |
67 | 71 | ||
68 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | 72 | async function onHlsPlaylistGenerationSuccess (video: VideoModel) { |
73 | if (video === undefined) return undefined | ||
74 | |||
75 | await sequelizeTypescript.transaction(async t => { | ||
76 | // Maybe the video changed in database, refresh it | ||
77 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | ||
78 | // Video does not exist anymore | ||
79 | if (!videoDatabase) return undefined | ||
80 | |||
81 | // If the video was not published, we consider it is a new one for other instances | ||
82 | await federateVideoIfNeeded(videoDatabase, false, t) | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | async function onVideoFileTranscoderOrImportSuccess (video: VideoModel, payload?: VideoFilePayload) { | ||
69 | if (video === undefined) return undefined | 87 | if (video === undefined) return undefined |
70 | 88 | ||
71 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 89 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
@@ -91,13 +109,16 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { | |||
91 | return { videoDatabase, videoPublished } | 109 | return { videoDatabase, videoPublished } |
92 | }) | 110 | }) |
93 | 111 | ||
94 | if (videoPublished) { | 112 | // don't notify prior to scheduled video update |
113 | if (videoPublished && !videoDatabase.ScheduleVideoUpdate) { | ||
95 | Notifier.Instance.notifyOnNewVideo(videoDatabase) | 114 | Notifier.Instance.notifyOnNewVideo(videoDatabase) |
96 | Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | 115 | Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) |
97 | } | 116 | } |
117 | |||
118 | await createHlsJobIfEnabled(payload) | ||
98 | } | 119 | } |
99 | 120 | ||
100 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) { | 121 | async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoFilePayload) { |
101 | if (videoArg === undefined) return undefined | 122 | if (videoArg === undefined) return undefined |
102 | 123 | ||
103 | // Outside the transaction (IO on disk) | 124 | // Outside the transaction (IO on disk) |
@@ -144,13 +165,18 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo | |||
144 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) | 165 | logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) |
145 | } | 166 | } |
146 | 167 | ||
147 | await federateVideoIfNeeded(videoDatabase, isNewVideo, t) | 168 | await federateVideoIfNeeded(videoDatabase, payload.isNewVideo, t) |
148 | 169 | ||
149 | return { videoDatabase, videoPublished } | 170 | return { videoDatabase, videoPublished } |
150 | }) | 171 | }) |
151 | 172 | ||
152 | if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | 173 | // don't notify prior to scheduled video update |
153 | if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | 174 | if (!videoDatabase.ScheduleVideoUpdate) { |
175 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) | ||
176 | if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) | ||
177 | } | ||
178 | |||
179 | await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) | ||
154 | } | 180 | } |
155 | 181 | ||
156 | // --------------------------------------------------------------------------- | 182 | // --------------------------------------------------------------------------- |
@@ -159,3 +185,20 @@ export { | |||
159 | processVideoFile, | 185 | processVideoFile, |
160 | processVideoFileImport | 186 | processVideoFileImport |
161 | } | 187 | } |
188 | |||
189 | // --------------------------------------------------------------------------- | ||
190 | |||
191 | function createHlsJobIfEnabled (payload?: VideoFilePayload) { | ||
192 | // Generate HLS playlist? | ||
193 | if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { | ||
194 | const hlsTranscodingPayload = { | ||
195 | videoUUID: payload.videoUUID, | ||
196 | resolution: payload.resolution, | ||
197 | isPortraitMode: payload.isPortraitMode, | ||
198 | |||
199 | generateHlsPlaylist: true | ||
200 | } | ||
201 | |||
202 | return JobQueue.Instance.createJob({ type: 'video-file', payload: hlsTranscodingPayload }) | ||
203 | } | ||
204 | } | ||
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index f643ee226..1a48f2bd0 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { AbstractScheduler } from './abstract-scheduler' | 1 | import { AbstractScheduler } from './abstract-scheduler' |
2 | import { CONFIG, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' | 2 | import { CONFIG, HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { VideosRedundancy } from '../../../shared/models/redundancy' | 4 | import { VideosRedundancy } from '../../../shared/models/redundancy' |
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
@@ -9,9 +9,19 @@ import { join } from 'path' | |||
9 | import { move } from 'fs-extra' | 9 | import { move } from 'fs-extra' |
10 | import { getServerActor } from '../../helpers/utils' | 10 | import { getServerActor } from '../../helpers/utils' |
11 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' | 11 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' |
12 | import { getVideoCacheFileActivityPubUrl } from '../activitypub/url' | 12 | import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' |
13 | import { removeVideoRedundancy } from '../redundancy' | 13 | import { removeVideoRedundancy } from '../redundancy' |
14 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' | 14 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' |
15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
16 | import { VideoModel } from '../../models/video/video' | ||
17 | import { downloadPlaylistSegments } from '../hls' | ||
18 | |||
19 | type CandidateToDuplicate = { | ||
20 | redundancy: VideosRedundancy, | ||
21 | video: VideoModel, | ||
22 | files: VideoFileModel[], | ||
23 | streamingPlaylists: VideoStreamingPlaylistModel[] | ||
24 | } | ||
15 | 25 | ||
16 | export class VideosRedundancyScheduler extends AbstractScheduler { | 26 | export class VideosRedundancyScheduler extends AbstractScheduler { |
17 | 27 | ||
@@ -24,28 +34,32 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
24 | } | 34 | } |
25 | 35 | ||
26 | protected async internalExecute () { | 36 | protected async internalExecute () { |
27 | for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { | 37 | for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { |
28 | logger.info('Running redundancy scheduler for strategy %s.', obj.strategy) | 38 | logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) |
29 | 39 | ||
30 | try { | 40 | try { |
31 | const videoToDuplicate = await this.findVideoToDuplicate(obj) | 41 | const videoToDuplicate = await this.findVideoToDuplicate(redundancyConfig) |
32 | if (!videoToDuplicate) continue | 42 | if (!videoToDuplicate) continue |
33 | 43 | ||
34 | const videoFiles = videoToDuplicate.VideoFiles | 44 | const candidateToDuplicate = { |
35 | videoFiles.forEach(f => f.Video = videoToDuplicate) | 45 | video: videoToDuplicate, |
46 | redundancy: redundancyConfig, | ||
47 | files: videoToDuplicate.VideoFiles, | ||
48 | streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists | ||
49 | } | ||
36 | 50 | ||
37 | await this.purgeCacheIfNeeded(obj, videoFiles) | 51 | await this.purgeCacheIfNeeded(candidateToDuplicate) |
38 | 52 | ||
39 | if (await this.isTooHeavy(obj, videoFiles)) { | 53 | if (await this.isTooHeavy(candidateToDuplicate)) { |
40 | logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url) | 54 | logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url) |
41 | continue | 55 | continue |
42 | } | 56 | } |
43 | 57 | ||
44 | logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, obj.strategy) | 58 | logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, redundancyConfig.strategy) |
45 | 59 | ||
46 | await this.createVideoRedundancy(obj, videoFiles) | 60 | await this.createVideoRedundancies(candidateToDuplicate) |
47 | } catch (err) { | 61 | } catch (err) { |
48 | logger.error('Cannot run videos redundancy %s.', obj.strategy, { err }) | 62 | logger.error('Cannot run videos redundancy %s.', redundancyConfig.strategy, { err }) |
49 | } | 63 | } |
50 | } | 64 | } |
51 | 65 | ||
@@ -63,25 +77,35 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
63 | 77 | ||
64 | for (const redundancyModel of expired) { | 78 | for (const redundancyModel of expired) { |
65 | try { | 79 | try { |
66 | await this.extendsOrDeleteRedundancy(redundancyModel) | 80 | const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
81 | const candidate = { | ||
82 | redundancy: redundancyConfig, | ||
83 | video: null, | ||
84 | files: [], | ||
85 | streamingPlaylists: [] | ||
86 | } | ||
87 | |||
88 | // If the administrator disabled the redundancy or decreased the cache size, remove this redundancy instead of extending it | ||
89 | if (!redundancyConfig || await this.isTooHeavy(candidate)) { | ||
90 | logger.info('Destroying redundancy %s because the cache size %s is too heavy.', redundancyModel.url, redundancyModel.strategy) | ||
91 | await removeVideoRedundancy(redundancyModel) | ||
92 | } else { | ||
93 | await this.extendsRedundancy(redundancyModel) | ||
94 | } | ||
67 | } catch (err) { | 95 | } catch (err) { |
68 | logger.error('Cannot extend expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel)) | 96 | logger.error( |
97 | 'Cannot extend or remove expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel), | ||
98 | { err } | ||
99 | ) | ||
69 | } | 100 | } |
70 | } | 101 | } |
71 | } | 102 | } |
72 | 103 | ||
73 | private async extendsOrDeleteRedundancy (redundancyModel: VideoRedundancyModel) { | 104 | private async extendsRedundancy (redundancyModel: VideoRedundancyModel) { |
74 | // Refresh the video, maybe it was deleted | ||
75 | const video = await this.loadAndRefreshVideo(redundancyModel.VideoFile.Video.url) | ||
76 | |||
77 | if (!video) { | ||
78 | logger.info('Destroying existing redundancy %s, because the associated video does not exist anymore.', redundancyModel.url) | ||
79 | |||
80 | await redundancyModel.destroy() | ||
81 | return | ||
82 | } | ||
83 | |||
84 | const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) | 105 | const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
106 | // Redundancy strategy disabled, remove our redundancy instead of extending expiration | ||
107 | if (!redundancy) await removeVideoRedundancy(redundancyModel) | ||
108 | |||
85 | await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime) | 109 | await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime) |
86 | } | 110 | } |
87 | 111 | ||
@@ -112,49 +136,93 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
112 | } | 136 | } |
113 | } | 137 | } |
114 | 138 | ||
115 | private async createVideoRedundancy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 139 | private async createVideoRedundancies (data: CandidateToDuplicate) { |
116 | const serverActor = await getServerActor() | 140 | const video = await this.loadAndRefreshVideo(data.video.url) |
141 | |||
142 | if (!video) { | ||
143 | logger.info('Video %s we want to duplicate does not existing anymore, skipping.', data.video.url) | ||
117 | 144 | ||
118 | for (const file of filesToDuplicate) { | 145 | return |
119 | const video = await this.loadAndRefreshVideo(file.Video.url) | 146 | } |
120 | 147 | ||
148 | for (const file of data.files) { | ||
121 | const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) | 149 | const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) |
122 | if (existingRedundancy) { | 150 | if (existingRedundancy) { |
123 | await this.extendsOrDeleteRedundancy(existingRedundancy) | 151 | await this.extendsRedundancy(existingRedundancy) |
124 | 152 | ||
125 | continue | 153 | continue |
126 | } | 154 | } |
127 | 155 | ||
128 | if (!video) { | 156 | await this.createVideoFileRedundancy(data.redundancy, video, file) |
129 | logger.info('Video %s we want to duplicate does not existing anymore, skipping.', file.Video.url) | 157 | } |
158 | |||
159 | for (const streamingPlaylist of data.streamingPlaylists) { | ||
160 | const existingRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(streamingPlaylist.id) | ||
161 | if (existingRedundancy) { | ||
162 | await this.extendsRedundancy(existingRedundancy) | ||
130 | 163 | ||
131 | continue | 164 | continue |
132 | } | 165 | } |
133 | 166 | ||
134 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) | 167 | await this.createStreamingPlaylistRedundancy(data.redundancy, video, streamingPlaylist) |
168 | } | ||
169 | } | ||
135 | 170 | ||
136 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 171 | private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: VideoModel, file: VideoFileModel) { |
137 | const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) | 172 | file.Video = video |
138 | 173 | ||
139 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) | 174 | const serverActor = await getServerActor() |
140 | 175 | ||
141 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) | 176 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) |
142 | await move(tmpPath, destPath) | ||
143 | 177 | ||
144 | const createdModel = await VideoRedundancyModel.create({ | 178 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
145 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 179 | const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) |
146 | url: getVideoCacheFileActivityPubUrl(file), | ||
147 | fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL), | ||
148 | strategy: redundancy.strategy, | ||
149 | videoFileId: file.id, | ||
150 | actorId: serverActor.id | ||
151 | }) | ||
152 | createdModel.VideoFile = file | ||
153 | 180 | ||
154 | await sendCreateCacheFile(serverActor, createdModel) | 181 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) |
155 | 182 | ||
156 | logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) | 183 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) |
157 | } | 184 | await move(tmpPath, destPath) |
185 | |||
186 | const createdModel = await VideoRedundancyModel.create({ | ||
187 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | ||
188 | url: getVideoCacheFileActivityPubUrl(file), | ||
189 | fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL), | ||
190 | strategy: redundancy.strategy, | ||
191 | videoFileId: file.id, | ||
192 | actorId: serverActor.id | ||
193 | }) | ||
194 | |||
195 | createdModel.VideoFile = file | ||
196 | |||
197 | await sendCreateCacheFile(serverActor, video, createdModel) | ||
198 | |||
199 | logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) | ||
200 | } | ||
201 | |||
202 | private async createStreamingPlaylistRedundancy (redundancy: VideosRedundancy, video: VideoModel, playlist: VideoStreamingPlaylistModel) { | ||
203 | playlist.Video = video | ||
204 | |||
205 | const serverActor = await getServerActor() | ||
206 | |||
207 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy) | ||
208 | |||
209 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | ||
210 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) | ||
211 | |||
212 | const createdModel = await VideoRedundancyModel.create({ | ||
213 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | ||
214 | url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), | ||
215 | fileUrl: playlist.getVideoRedundancyUrl(CONFIG.WEBSERVER.URL), | ||
216 | strategy: redundancy.strategy, | ||
217 | videoStreamingPlaylistId: playlist.id, | ||
218 | actorId: serverActor.id | ||
219 | }) | ||
220 | |||
221 | createdModel.VideoStreamingPlaylist = playlist | ||
222 | |||
223 | await sendCreateCacheFile(serverActor, video, createdModel) | ||
224 | |||
225 | logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url) | ||
158 | } | 226 | } |
159 | 227 | ||
160 | private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { | 228 | private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { |
@@ -168,8 +236,9 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
168 | await sendUpdateCacheFile(serverActor, redundancy) | 236 | await sendUpdateCacheFile(serverActor, redundancy) |
169 | } | 237 | } |
170 | 238 | ||
171 | private async purgeCacheIfNeeded (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 239 | private async purgeCacheIfNeeded (candidateToDuplicate: CandidateToDuplicate) { |
172 | while (this.isTooHeavy(redundancy, filesToDuplicate)) { | 240 | while (this.isTooHeavy(candidateToDuplicate)) { |
241 | const redundancy = candidateToDuplicate.redundancy | ||
173 | const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) | 242 | const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) |
174 | if (!toDelete) return | 243 | if (!toDelete) return |
175 | 244 | ||
@@ -177,11 +246,11 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
177 | } | 246 | } |
178 | } | 247 | } |
179 | 248 | ||
180 | private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { | 249 | private async isTooHeavy (candidateToDuplicate: CandidateToDuplicate) { |
181 | const maxSize = redundancy.size | 250 | const maxSize = candidateToDuplicate.redundancy.size |
182 | 251 | ||
183 | const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy) | 252 | const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(candidateToDuplicate.redundancy.strategy) |
184 | const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(filesToDuplicate) | 253 | const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(candidateToDuplicate.files, candidateToDuplicate.streamingPlaylists) |
185 | 254 | ||
186 | return totalWillDuplicate > maxSize | 255 | return totalWillDuplicate > maxSize |
187 | } | 256 | } |
@@ -191,13 +260,15 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
191 | } | 260 | } |
192 | 261 | ||
193 | private buildEntryLogId (object: VideoRedundancyModel) { | 262 | private buildEntryLogId (object: VideoRedundancyModel) { |
194 | return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` | 263 | if (object.VideoFile) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` |
264 | |||
265 | return `${object.VideoStreamingPlaylist.playlistUrl}` | ||
195 | } | 266 | } |
196 | 267 | ||
197 | private getTotalFileSizes (files: VideoFileModel[]) { | 268 | private getTotalFileSizes (files: VideoFileModel[], playlists: VideoStreamingPlaylistModel[]) { |
198 | const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size | 269 | const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size |
199 | 270 | ||
200 | return files.reduce(fileReducer, 0) | 271 | return files.reduce(fileReducer, 0) * playlists.length |
201 | } | 272 | } |
202 | 273 | ||
203 | private async loadAndRefreshVideo (videoUrl: string) { | 274 | private async loadAndRefreshVideo (videoUrl: string) { |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 4460f46e4..086b860a2 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -1,11 +1,14 @@ | |||
1 | import { CONFIG } from '../initializers' | 1 | import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers' |
2 | import { extname, join } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' | 3 | import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' |
4 | import { copy, remove, move, stat } from 'fs-extra' | 4 | import { copy, ensureDir, move, remove, stat } from 'fs-extra' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { VideoResolution } from '../../shared/models/videos' | 6 | import { VideoResolution } from '../../shared/models/videos' |
7 | import { VideoFileModel } from '../models/video/video-file' | 7 | import { VideoFileModel } from '../models/video/video-file' |
8 | import { VideoModel } from '../models/video/video' | 8 | import { VideoModel } from '../models/video/video' |
9 | import { updateMasterHLSPlaylist, updateSha256Segments } from './hls' | ||
10 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
11 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | ||
9 | 12 | ||
10 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { | 13 | async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { |
11 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 14 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
@@ -17,7 +20,8 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
17 | 20 | ||
18 | const transcodeOptions = { | 21 | const transcodeOptions = { |
19 | inputPath: videoInputPath, | 22 | inputPath: videoInputPath, |
20 | outputPath: videoTranscodedPath | 23 | outputPath: videoTranscodedPath, |
24 | resolution: inputVideoFile.resolution | ||
21 | } | 25 | } |
22 | 26 | ||
23 | // Could be very long! | 27 | // Could be very long! |
@@ -47,7 +51,7 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi | |||
47 | } | 51 | } |
48 | } | 52 | } |
49 | 53 | ||
50 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | 54 | async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { |
51 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 55 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
52 | const extname = '.mp4' | 56 | const extname = '.mp4' |
53 | 57 | ||
@@ -60,13 +64,13 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
60 | size: 0, | 64 | size: 0, |
61 | videoId: video.id | 65 | videoId: video.id |
62 | }) | 66 | }) |
63 | const videoOutputPath = join(videosDirectory, video.getVideoFilename(newVideoFile)) | 67 | const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile)) |
64 | 68 | ||
65 | const transcodeOptions = { | 69 | const transcodeOptions = { |
66 | inputPath: videoInputPath, | 70 | inputPath: videoInputPath, |
67 | outputPath: videoOutputPath, | 71 | outputPath: videoOutputPath, |
68 | resolution, | 72 | resolution, |
69 | isPortraitMode | 73 | isPortraitMode: isPortrait |
70 | } | 74 | } |
71 | 75 | ||
72 | await transcode(transcodeOptions) | 76 | await transcode(transcodeOptions) |
@@ -84,6 +88,41 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR | |||
84 | video.VideoFiles.push(newVideoFile) | 88 | video.VideoFiles.push(newVideoFile) |
85 | } | 89 | } |
86 | 90 | ||
91 | async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { | ||
92 | const baseHlsDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid) | ||
93 | await ensureDir(join(HLS_PLAYLIST_DIRECTORY, video.uuid)) | ||
94 | |||
95 | const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getOriginalFile())) | ||
96 | const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | ||
97 | |||
98 | const transcodeOptions = { | ||
99 | inputPath: videoInputPath, | ||
100 | outputPath, | ||
101 | resolution, | ||
102 | isPortraitMode, | ||
103 | |||
104 | hlsPlaylist: { | ||
105 | videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution) | ||
106 | } | ||
107 | } | ||
108 | |||
109 | await transcode(transcodeOptions) | ||
110 | |||
111 | await updateMasterHLSPlaylist(video) | ||
112 | await updateSha256Segments(video) | ||
113 | |||
114 | const playlistUrl = CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | ||
115 | |||
116 | await VideoStreamingPlaylistModel.upsert({ | ||
117 | videoId: video.id, | ||
118 | playlistUrl, | ||
119 | segmentsSha256Url: CONFIG.WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid), | ||
120 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles), | ||
121 | |||
122 | type: VideoStreamingPlaylistType.HLS | ||
123 | }) | ||
124 | } | ||
125 | |||
87 | async function importVideoFile (video: VideoModel, inputFilePath: string) { | 126 | async function importVideoFile (video: VideoModel, inputFilePath: string) { |
88 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | 127 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) |
89 | const { size } = await stat(inputFilePath) | 128 | const { size } = await stat(inputFilePath) |
@@ -125,6 +164,7 @@ async function importVideoFile (video: VideoModel, inputFilePath: string) { | |||
125 | } | 164 | } |
126 | 165 | ||
127 | export { | 166 | export { |
167 | generateHlsPlaylist, | ||
128 | optimizeVideofile, | 168 | optimizeVideofile, |
129 | transcodeOriginalVideofile, | 169 | transcodeOriginalVideofile, |
130 | importVideoFile | 170 | importVideoFile |
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts index 8b919af0d..5fa9d1ab5 100644 --- a/server/middlewares/csp.ts +++ b/server/middlewares/csp.ts | |||
@@ -16,7 +16,7 @@ const baseDirectives = Object.assign({}, | |||
16 | baseUri: ["'self'"], | 16 | baseUri: ["'self'"], |
17 | manifestSrc: ["'self'"], | 17 | manifestSrc: ["'self'"], |
18 | frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed | 18 | frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed |
19 | workerSrc: ["'self'"] // instead of deprecated child-src | 19 | workerSrc: ["'self'", 'blob:'] // instead of deprecated child-src |
20 | }, | 20 | }, |
21 | CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}, | 21 | CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}, |
22 | CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} | 22 | CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} |
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index c72ab78b2..329322509 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts | |||
@@ -13,7 +13,7 @@ import { ActorFollowModel } from '../../models/activitypub/actor-follow' | |||
13 | import { SERVER_ACTOR_NAME } from '../../initializers' | 13 | import { SERVER_ACTOR_NAME } from '../../initializers' |
14 | import { ServerModel } from '../../models/server/server' | 14 | import { ServerModel } from '../../models/server/server' |
15 | 15 | ||
16 | const videoRedundancyGetValidator = [ | 16 | const videoFileRedundancyGetValidator = [ |
17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 17 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), |
18 | param('resolution') | 18 | param('resolution') |
19 | .customSanitizer(toIntOrNull) | 19 | .customSanitizer(toIntOrNull) |
@@ -24,7 +24,7 @@ const videoRedundancyGetValidator = [ | |||
24 | .custom(exists).withMessage('Should have a valid fps'), | 24 | .custom(exists).withMessage('Should have a valid fps'), |
25 | 25 | ||
26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 26 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
27 | logger.debug('Checking videoRedundancyGetValidator parameters', { parameters: req.params }) | 27 | logger.debug('Checking videoFileRedundancyGetValidator parameters', { parameters: req.params }) |
28 | 28 | ||
29 | if (areValidationErrors(req, res)) return | 29 | if (areValidationErrors(req, res)) return |
30 | if (!await isVideoExist(req.params.videoId, res)) return | 30 | if (!await isVideoExist(req.params.videoId, res)) return |
@@ -38,7 +38,31 @@ const videoRedundancyGetValidator = [ | |||
38 | res.locals.videoFile = videoFile | 38 | res.locals.videoFile = videoFile |
39 | 39 | ||
40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) | 40 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) |
41 | if (!videoRedundancy)return res.status(404).json({ error: 'Video redundancy not found.' }) | 41 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) |
42 | res.locals.videoRedundancy = videoRedundancy | ||
43 | |||
44 | return next() | ||
45 | } | ||
46 | ] | ||
47 | |||
48 | const videoPlaylistRedundancyGetValidator = [ | ||
49 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | ||
50 | param('streamingPlaylistType').custom(exists).withMessage('Should have a valid streaming playlist type'), | ||
51 | |||
52 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
53 | logger.debug('Checking videoPlaylistRedundancyGetValidator parameters', { parameters: req.params }) | ||
54 | |||
55 | if (areValidationErrors(req, res)) return | ||
56 | if (!await isVideoExist(req.params.videoId, res)) return | ||
57 | |||
58 | const video: VideoModel = res.locals.video | ||
59 | const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType) | ||
60 | |||
61 | if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' }) | ||
62 | res.locals.videoStreamingPlaylist = videoStreamingPlaylist | ||
63 | |||
64 | const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) | ||
65 | if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' }) | ||
42 | res.locals.videoRedundancy = videoRedundancy | 66 | res.locals.videoRedundancy = videoRedundancy |
43 | 67 | ||
44 | return next() | 68 | return next() |
@@ -75,6 +99,7 @@ const updateServerRedundancyValidator = [ | |||
75 | // --------------------------------------------------------------------------- | 99 | // --------------------------------------------------------------------------- |
76 | 100 | ||
77 | export { | 101 | export { |
78 | videoRedundancyGetValidator, | 102 | videoFileRedundancyGetValidator, |
103 | videoPlaylistRedundancyGetValidator, | ||
79 | updateServerRedundancyValidator | 104 | updateServerRedundancyValidator |
80 | } | 105 | } |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 1bb0bfb1b..a52e3060a 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -113,6 +113,7 @@ const deleteMeValidator = [ | |||
113 | 113 | ||
114 | const usersUpdateValidator = [ | 114 | const usersUpdateValidator = [ |
115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
116 | body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), | ||
116 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 117 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
117 | body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), | 118 | body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), |
118 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 119 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
@@ -233,6 +234,7 @@ const usersAskResetPasswordValidator = [ | |||
233 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) | 234 | logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) |
234 | 235 | ||
235 | if (areValidationErrors(req, res)) return | 236 | if (areValidationErrors(req, res)) return |
237 | |||
236 | const exists = await checkUserEmailExist(req.body.email, res, false) | 238 | const exists = await checkUserEmailExist(req.body.email, res, false) |
237 | if (!exists) { | 239 | if (!exists) { |
238 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) | 240 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) |
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts index 13da7acff..2688f63ae 100644 --- a/server/middlewares/validators/videos/video-blacklist.ts +++ b/server/middlewares/validators/videos/video-blacklist.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | 3 | import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' |
4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' | 4 | import { isVideoExist } from '../../../helpers/custom-validators/videos' |
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { areValidationErrors } from '../utils' | 6 | import { areValidationErrors } from '../utils' |
7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' | 7 | import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' |
8 | import { VideoModel } from '../../../models/video/video' | ||
8 | 9 | ||
9 | const videosBlacklistRemoveValidator = [ | 10 | const videosBlacklistRemoveValidator = [ |
10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 11 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
@@ -22,6 +23,10 @@ const videosBlacklistRemoveValidator = [ | |||
22 | 23 | ||
23 | const videosBlacklistAddValidator = [ | 24 | const videosBlacklistAddValidator = [ |
24 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 25 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
26 | body('unfederate') | ||
27 | .optional() | ||
28 | .toBoolean() | ||
29 | .custom(isBooleanValid).withMessage('Should have a valid unfederate boolean'), | ||
25 | body('reason') | 30 | body('reason') |
26 | .optional() | 31 | .optional() |
27 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), | 32 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), |
@@ -32,6 +37,14 @@ const videosBlacklistAddValidator = [ | |||
32 | if (areValidationErrors(req, res)) return | 37 | if (areValidationErrors(req, res)) return |
33 | if (!await isVideoExist(req.params.videoId, res)) return | 38 | if (!await isVideoExist(req.params.videoId, res)) return |
34 | 39 | ||
40 | const video: VideoModel = res.locals.video | ||
41 | if (req.body.unfederate === true && video.remote === true) { | ||
42 | return res | ||
43 | .status(409) | ||
44 | .send({ error: 'You cannot unfederate a remote video.' }) | ||
45 | .end() | ||
46 | } | ||
47 | |||
35 | return next() | 48 | return next() |
36 | } | 49 | } |
37 | ] | 50 | ] |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 194d12c6e..159727e28 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -341,11 +341,14 @@ function getCommonVideoAttributes () { | |||
341 | .optional() | 341 | .optional() |
342 | .toBoolean() | 342 | .toBoolean() |
343 | .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), | 343 | .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), |
344 | body('originallyPublishedAt') | 344 | body('downloadEnabled') |
345 | .optional() | 345 | .optional() |
346 | .customSanitizer(toValueOrNull) | 346 | .toBoolean() |
347 | .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'), | 347 | .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'), |
348 | 348 | body('originallyPublishedAt') | |
349 | .optional() | ||
350 | .customSanitizer(toValueOrNull) | ||
351 | .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'), | ||
349 | body('scheduleUpdate') | 352 | body('scheduleUpdate') |
350 | .optional() | 353 | .optional() |
351 | .customSanitizer(toValueOrNull), | 354 | .customSanitizer(toValueOrNull), |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index a99e9b1ad..84ef0b30d 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -288,6 +288,10 @@ export class AccountModel extends Model<AccountModel> { | |||
288 | return this.Actor.isOwned() | 288 | return this.Actor.isOwned() |
289 | } | 289 | } |
290 | 290 | ||
291 | isOutdated () { | ||
292 | return this.Actor.isOutdated() | ||
293 | } | ||
294 | |||
291 | getDisplayName () { | 295 | getDisplayName () { |
292 | return this.name | 296 | return this.name |
293 | } | 297 | } |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 9e4f982a3..6cdbb827b 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -27,11 +27,33 @@ import { VideoBlacklistModel } from '../video/video-blacklist' | |||
27 | import { VideoImportModel } from '../video/video-import' | 27 | import { VideoImportModel } from '../video/video-import' |
28 | import { ActorModel } from '../activitypub/actor' | 28 | import { ActorModel } from '../activitypub/actor' |
29 | import { ActorFollowModel } from '../activitypub/actor-follow' | 29 | import { ActorFollowModel } from '../activitypub/actor-follow' |
30 | import { AvatarModel } from '../avatar/avatar' | ||
31 | import { ServerModel } from '../server/server' | ||
30 | 32 | ||
31 | enum ScopeNames { | 33 | enum ScopeNames { |
32 | WITH_ALL = 'WITH_ALL' | 34 | WITH_ALL = 'WITH_ALL' |
33 | } | 35 | } |
34 | 36 | ||
37 | function buildActorWithAvatarInclude () { | ||
38 | return { | ||
39 | attributes: [ 'preferredUsername' ], | ||
40 | model: () => ActorModel.unscoped(), | ||
41 | required: true, | ||
42 | include: [ | ||
43 | { | ||
44 | attributes: [ 'filename' ], | ||
45 | model: () => AvatarModel.unscoped(), | ||
46 | required: false | ||
47 | }, | ||
48 | { | ||
49 | attributes: [ 'host' ], | ||
50 | model: () => ServerModel.unscoped(), | ||
51 | required: false | ||
52 | } | ||
53 | ] | ||
54 | } | ||
55 | } | ||
56 | |||
35 | function buildVideoInclude (required: boolean) { | 57 | function buildVideoInclude (required: boolean) { |
36 | return { | 58 | return { |
37 | attributes: [ 'id', 'uuid', 'name' ], | 59 | attributes: [ 'id', 'uuid', 'name' ], |
@@ -40,19 +62,21 @@ function buildVideoInclude (required: boolean) { | |||
40 | } | 62 | } |
41 | } | 63 | } |
42 | 64 | ||
43 | function buildChannelInclude (required: boolean) { | 65 | function buildChannelInclude (required: boolean, withActor = false) { |
44 | return { | 66 | return { |
45 | required, | 67 | required, |
46 | attributes: [ 'id', 'name' ], | 68 | attributes: [ 'id', 'name' ], |
47 | model: () => VideoChannelModel.unscoped() | 69 | model: () => VideoChannelModel.unscoped(), |
70 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
48 | } | 71 | } |
49 | } | 72 | } |
50 | 73 | ||
51 | function buildAccountInclude (required: boolean) { | 74 | function buildAccountInclude (required: boolean, withActor = false) { |
52 | return { | 75 | return { |
53 | required, | 76 | required, |
54 | attributes: [ 'id', 'name' ], | 77 | attributes: [ 'id', 'name' ], |
55 | model: () => AccountModel.unscoped() | 78 | model: () => AccountModel.unscoped(), |
79 | include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] | ||
56 | } | 80 | } |
57 | } | 81 | } |
58 | 82 | ||
@@ -60,47 +84,40 @@ function buildAccountInclude (required: boolean) { | |||
60 | [ScopeNames.WITH_ALL]: { | 84 | [ScopeNames.WITH_ALL]: { |
61 | include: [ | 85 | include: [ |
62 | Object.assign(buildVideoInclude(false), { | 86 | Object.assign(buildVideoInclude(false), { |
63 | include: [ buildChannelInclude(true) ] | 87 | include: [ buildChannelInclude(true, true) ] |
64 | }), | 88 | }), |
89 | |||
65 | { | 90 | { |
66 | attributes: [ 'id', 'originCommentId' ], | 91 | attributes: [ 'id', 'originCommentId' ], |
67 | model: () => VideoCommentModel.unscoped(), | 92 | model: () => VideoCommentModel.unscoped(), |
68 | required: false, | 93 | required: false, |
69 | include: [ | 94 | include: [ |
70 | buildAccountInclude(true), | 95 | buildAccountInclude(true, true), |
71 | buildVideoInclude(true) | 96 | buildVideoInclude(true) |
72 | ] | 97 | ] |
73 | }, | 98 | }, |
99 | |||
74 | { | 100 | { |
75 | attributes: [ 'id' ], | 101 | attributes: [ 'id' ], |
76 | model: () => VideoAbuseModel.unscoped(), | 102 | model: () => VideoAbuseModel.unscoped(), |
77 | required: false, | 103 | required: false, |
78 | include: [ buildVideoInclude(true) ] | 104 | include: [ buildVideoInclude(true) ] |
79 | }, | 105 | }, |
106 | |||
80 | { | 107 | { |
81 | attributes: [ 'id' ], | 108 | attributes: [ 'id' ], |
82 | model: () => VideoBlacklistModel.unscoped(), | 109 | model: () => VideoBlacklistModel.unscoped(), |
83 | required: false, | 110 | required: false, |
84 | include: [ buildVideoInclude(true) ] | 111 | include: [ buildVideoInclude(true) ] |
85 | }, | 112 | }, |
113 | |||
86 | { | 114 | { |
87 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], | 115 | attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], |
88 | model: () => VideoImportModel.unscoped(), | 116 | model: () => VideoImportModel.unscoped(), |
89 | required: false, | 117 | required: false, |
90 | include: [ buildVideoInclude(false) ] | 118 | include: [ buildVideoInclude(false) ] |
91 | }, | 119 | }, |
92 | { | 120 | |
93 | attributes: [ 'id', 'name' ], | ||
94 | model: () => AccountModel.unscoped(), | ||
95 | required: false, | ||
96 | include: [ | ||
97 | { | ||
98 | attributes: [ 'id', 'preferredUsername' ], | ||
99 | model: () => ActorModel.unscoped(), | ||
100 | required: true | ||
101 | } | ||
102 | ] | ||
103 | }, | ||
104 | { | 121 | { |
105 | attributes: [ 'id' ], | 122 | attributes: [ 'id' ], |
106 | model: () => ActorFollowModel.unscoped(), | 123 | model: () => ActorFollowModel.unscoped(), |
@@ -111,7 +128,23 @@ function buildAccountInclude (required: boolean) { | |||
111 | model: () => ActorModel.unscoped(), | 128 | model: () => ActorModel.unscoped(), |
112 | required: true, | 129 | required: true, |
113 | as: 'ActorFollower', | 130 | as: 'ActorFollower', |
114 | include: [ buildAccountInclude(true) ] | 131 | include: [ |
132 | { | ||
133 | attributes: [ 'id', 'name' ], | ||
134 | model: () => AccountModel.unscoped(), | ||
135 | required: true | ||
136 | }, | ||
137 | { | ||
138 | attributes: [ 'filename' ], | ||
139 | model: () => AvatarModel.unscoped(), | ||
140 | required: false | ||
141 | }, | ||
142 | { | ||
143 | attributes: [ 'host' ], | ||
144 | model: () => ServerModel.unscoped(), | ||
145 | required: false | ||
146 | } | ||
147 | ] | ||
115 | }, | 148 | }, |
116 | { | 149 | { |
117 | attributes: [ 'preferredUsername' ], | 150 | attributes: [ 'preferredUsername' ], |
@@ -124,7 +157,9 @@ function buildAccountInclude (required: boolean) { | |||
124 | ] | 157 | ] |
125 | } | 158 | } |
126 | ] | 159 | ] |
127 | } | 160 | }, |
161 | |||
162 | buildAccountInclude(false, true) | ||
128 | ] | 163 | ] |
129 | } | 164 | } |
130 | }) | 165 | }) |
@@ -132,10 +167,63 @@ function buildAccountInclude (required: boolean) { | |||
132 | tableName: 'userNotification', | 167 | tableName: 'userNotification', |
133 | indexes: [ | 168 | indexes: [ |
134 | { | 169 | { |
135 | fields: [ 'videoId' ] | 170 | fields: [ 'userId' ] |
171 | }, | ||
172 | { | ||
173 | fields: [ 'videoId' ], | ||
174 | where: { | ||
175 | videoId: { | ||
176 | [Op.ne]: null | ||
177 | } | ||
178 | } | ||
136 | }, | 179 | }, |
137 | { | 180 | { |
138 | fields: [ 'commentId' ] | 181 | fields: [ 'commentId' ], |
182 | where: { | ||
183 | commentId: { | ||
184 | [Op.ne]: null | ||
185 | } | ||
186 | } | ||
187 | }, | ||
188 | { | ||
189 | fields: [ 'videoAbuseId' ], | ||
190 | where: { | ||
191 | videoAbuseId: { | ||
192 | [Op.ne]: null | ||
193 | } | ||
194 | } | ||
195 | }, | ||
196 | { | ||
197 | fields: [ 'videoBlacklistId' ], | ||
198 | where: { | ||
199 | videoBlacklistId: { | ||
200 | [Op.ne]: null | ||
201 | } | ||
202 | } | ||
203 | }, | ||
204 | { | ||
205 | fields: [ 'videoImportId' ], | ||
206 | where: { | ||
207 | videoImportId: { | ||
208 | [Op.ne]: null | ||
209 | } | ||
210 | } | ||
211 | }, | ||
212 | { | ||
213 | fields: [ 'accountId' ], | ||
214 | where: { | ||
215 | accountId: { | ||
216 | [Op.ne]: null | ||
217 | } | ||
218 | } | ||
219 | }, | ||
220 | { | ||
221 | fields: [ 'actorFollowId' ], | ||
222 | where: { | ||
223 | actorFollowId: { | ||
224 | [Op.ne]: null | ||
225 | } | ||
226 | } | ||
139 | } | 227 | } |
140 | ] | 228 | ] |
141 | }) | 229 | }) |
@@ -297,12 +385,9 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
297 | } | 385 | } |
298 | 386 | ||
299 | toFormattedJSON (): UserNotification { | 387 | toFormattedJSON (): UserNotification { |
300 | const video = this.Video ? Object.assign(this.formatVideo(this.Video), { | 388 | const video = this.Video |
301 | channel: { | 389 | ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) }) |
302 | id: this.Video.VideoChannel.id, | 390 | : undefined |
303 | displayName: this.Video.VideoChannel.getDisplayName() | ||
304 | } | ||
305 | }) : undefined | ||
306 | 391 | ||
307 | const videoImport = this.VideoImport ? { | 392 | const videoImport = this.VideoImport ? { |
308 | id: this.VideoImport.id, | 393 | id: this.VideoImport.id, |
@@ -315,10 +400,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
315 | const comment = this.Comment ? { | 400 | const comment = this.Comment ? { |
316 | id: this.Comment.id, | 401 | id: this.Comment.id, |
317 | threadId: this.Comment.getThreadId(), | 402 | threadId: this.Comment.getThreadId(), |
318 | account: { | 403 | account: this.formatActor(this.Comment.Account), |
319 | id: this.Comment.Account.id, | ||
320 | displayName: this.Comment.Account.getDisplayName() | ||
321 | }, | ||
322 | video: this.formatVideo(this.Comment.Video) | 404 | video: this.formatVideo(this.Comment.Video) |
323 | } : undefined | 405 | } : undefined |
324 | 406 | ||
@@ -332,17 +414,16 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
332 | video: this.formatVideo(this.VideoBlacklist.Video) | 414 | video: this.formatVideo(this.VideoBlacklist.Video) |
333 | } : undefined | 415 | } : undefined |
334 | 416 | ||
335 | const account = this.Account ? { | 417 | const account = this.Account ? this.formatActor(this.Account) : undefined |
336 | id: this.Account.id, | ||
337 | displayName: this.Account.getDisplayName(), | ||
338 | name: this.Account.Actor.preferredUsername | ||
339 | } : undefined | ||
340 | 418 | ||
341 | const actorFollow = this.ActorFollow ? { | 419 | const actorFollow = this.ActorFollow ? { |
342 | id: this.ActorFollow.id, | 420 | id: this.ActorFollow.id, |
343 | follower: { | 421 | follower: { |
422 | id: this.ActorFollow.ActorFollower.Account.id, | ||
344 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), | 423 | displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), |
345 | name: this.ActorFollow.ActorFollower.preferredUsername | 424 | name: this.ActorFollow.ActorFollower.preferredUsername, |
425 | avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined, | ||
426 | host: this.ActorFollow.ActorFollower.getHost() | ||
346 | }, | 427 | }, |
347 | following: { | 428 | following: { |
348 | type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', | 429 | type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', |
@@ -374,4 +455,18 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
374 | name: video.name | 455 | name: video.name |
375 | } | 456 | } |
376 | } | 457 | } |
458 | |||
459 | private formatActor (accountOrChannel: AccountModel | VideoChannelModel) { | ||
460 | const avatar = accountOrChannel.Actor.Avatar | ||
461 | ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() } | ||
462 | : undefined | ||
463 | |||
464 | return { | ||
465 | id: accountOrChannel.id, | ||
466 | displayName: accountOrChannel.getDisplayName(), | ||
467 | name: accountOrChannel.Actor.preferredUsername, | ||
468 | host: accountOrChannel.Actor.getHost(), | ||
469 | avatar | ||
470 | } | ||
471 | } | ||
377 | } | 472 | } |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 8b6cd146a..b722bed14 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -28,6 +28,7 @@ import { sample } from 'lodash' | |||
28 | import { isTestInstance } from '../../helpers/core-utils' | 28 | import { isTestInstance } from '../../helpers/core-utils' |
29 | import * as Bluebird from 'bluebird' | 29 | import * as Bluebird from 'bluebird' |
30 | import * as Sequelize from 'sequelize' | 30 | import * as Sequelize from 'sequelize' |
31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' | ||
31 | 32 | ||
32 | export enum ScopeNames { | 33 | export enum ScopeNames { |
33 | WITH_VIDEO = 'WITH_VIDEO' | 34 | WITH_VIDEO = 'WITH_VIDEO' |
@@ -38,7 +39,17 @@ export enum ScopeNames { | |||
38 | include: [ | 39 | include: [ |
39 | { | 40 | { |
40 | model: () => VideoFileModel, | 41 | model: () => VideoFileModel, |
41 | required: true, | 42 | required: false, |
43 | include: [ | ||
44 | { | ||
45 | model: () => VideoModel, | ||
46 | required: true | ||
47 | } | ||
48 | ] | ||
49 | }, | ||
50 | { | ||
51 | model: () => VideoStreamingPlaylistModel, | ||
52 | required: false, | ||
42 | include: [ | 53 | include: [ |
43 | { | 54 | { |
44 | model: () => VideoModel, | 55 | model: () => VideoModel, |
@@ -97,12 +108,24 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
97 | 108 | ||
98 | @BelongsTo(() => VideoFileModel, { | 109 | @BelongsTo(() => VideoFileModel, { |
99 | foreignKey: { | 110 | foreignKey: { |
100 | allowNull: false | 111 | allowNull: true |
101 | }, | 112 | }, |
102 | onDelete: 'cascade' | 113 | onDelete: 'cascade' |
103 | }) | 114 | }) |
104 | VideoFile: VideoFileModel | 115 | VideoFile: VideoFileModel |
105 | 116 | ||
117 | @ForeignKey(() => VideoStreamingPlaylistModel) | ||
118 | @Column | ||
119 | videoStreamingPlaylistId: number | ||
120 | |||
121 | @BelongsTo(() => VideoStreamingPlaylistModel, { | ||
122 | foreignKey: { | ||
123 | allowNull: true | ||
124 | }, | ||
125 | onDelete: 'cascade' | ||
126 | }) | ||
127 | VideoStreamingPlaylist: VideoStreamingPlaylistModel | ||
128 | |||
106 | @ForeignKey(() => ActorModel) | 129 | @ForeignKey(() => ActorModel) |
107 | @Column | 130 | @Column |
108 | actorId: number | 131 | actorId: number |
@@ -119,13 +142,25 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
119 | static async removeFile (instance: VideoRedundancyModel) { | 142 | static async removeFile (instance: VideoRedundancyModel) { |
120 | if (!instance.isOwned()) return | 143 | if (!instance.isOwned()) return |
121 | 144 | ||
122 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | 145 | if (instance.videoFileId) { |
146 | const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) | ||
123 | 147 | ||
124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 148 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
125 | logger.info('Removing duplicated video file %s.', logIdentifier) | 149 | logger.info('Removing duplicated video file %s.', logIdentifier) |
126 | 150 | ||
127 | videoFile.Video.removeFile(videoFile, true) | 151 | videoFile.Video.removeFile(videoFile, true) |
128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 152 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
153 | } | ||
154 | |||
155 | if (instance.videoStreamingPlaylistId) { | ||
156 | const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId) | ||
157 | |||
158 | const videoUUID = videoStreamingPlaylist.Video.uuid | ||
159 | logger.info('Removing duplicated video streaming playlist %s.', videoUUID) | ||
160 | |||
161 | videoStreamingPlaylist.Video.removeStreamingPlaylist(true) | ||
162 | .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err })) | ||
163 | } | ||
129 | 164 | ||
130 | return undefined | 165 | return undefined |
131 | } | 166 | } |
@@ -143,6 +178,19 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
143 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | 178 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) |
144 | } | 179 | } |
145 | 180 | ||
181 | static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) { | ||
182 | const actor = await getServerActor() | ||
183 | |||
184 | const query = { | ||
185 | where: { | ||
186 | actorId: actor.id, | ||
187 | videoStreamingPlaylistId | ||
188 | } | ||
189 | } | ||
190 | |||
191 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | ||
192 | } | ||
193 | |||
146 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 194 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
147 | const query = { | 195 | const query = { |
148 | where: { | 196 | where: { |
@@ -191,7 +239,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
191 | const ids = rows.map(r => r.id) | 239 | const ids = rows.map(r => r.id) |
192 | const id = sample(ids) | 240 | const id = sample(ids) |
193 | 241 | ||
194 | return VideoModel.loadWithFile(id, undefined, !isTestInstance()) | 242 | return VideoModel.loadWithFiles(id, undefined, !isTestInstance()) |
195 | } | 243 | } |
196 | 244 | ||
197 | static async findMostViewToDuplicate (randomizedFactor: number) { | 245 | static async findMostViewToDuplicate (randomizedFactor: number) { |
@@ -333,40 +381,44 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
333 | 381 | ||
334 | static async listLocalOfServer (serverId: number) { | 382 | static async listLocalOfServer (serverId: number) { |
335 | const actor = await getServerActor() | 383 | const actor = await getServerActor() |
336 | 384 | const buildVideoInclude = () => ({ | |
337 | const query = { | 385 | model: VideoModel, |
338 | where: { | 386 | required: true, |
339 | actorId: actor.id | ||
340 | }, | ||
341 | include: [ | 387 | include: [ |
342 | { | 388 | { |
343 | model: VideoFileModel, | 389 | attributes: [], |
390 | model: VideoChannelModel.unscoped(), | ||
344 | required: true, | 391 | required: true, |
345 | include: [ | 392 | include: [ |
346 | { | 393 | { |
347 | model: VideoModel, | 394 | attributes: [], |
395 | model: ActorModel.unscoped(), | ||
348 | required: true, | 396 | required: true, |
349 | include: [ | 397 | where: { |
350 | { | 398 | serverId |
351 | attributes: [], | 399 | } |
352 | model: VideoChannelModel.unscoped(), | ||
353 | required: true, | ||
354 | include: [ | ||
355 | { | ||
356 | attributes: [], | ||
357 | model: ActorModel.unscoped(), | ||
358 | required: true, | ||
359 | where: { | ||
360 | serverId | ||
361 | } | ||
362 | } | ||
363 | ] | ||
364 | } | ||
365 | ] | ||
366 | } | 400 | } |
367 | ] | 401 | ] |
368 | } | 402 | } |
369 | ] | 403 | ] |
404 | }) | ||
405 | |||
406 | const query = { | ||
407 | where: { | ||
408 | actorId: actor.id | ||
409 | }, | ||
410 | include: [ | ||
411 | { | ||
412 | model: VideoFileModel, | ||
413 | required: false, | ||
414 | include: [ buildVideoInclude() ] | ||
415 | }, | ||
416 | { | ||
417 | model: VideoStreamingPlaylistModel, | ||
418 | required: false, | ||
419 | include: [ buildVideoInclude() ] | ||
420 | } | ||
421 | ] | ||
370 | } | 422 | } |
371 | 423 | ||
372 | return VideoRedundancyModel.findAll(query) | 424 | return VideoRedundancyModel.findAll(query) |
@@ -395,7 +447,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
395 | ] | 447 | ] |
396 | } | 448 | } |
397 | 449 | ||
398 | return VideoRedundancyModel.find(query as any) // FIXME: typings | 450 | return VideoRedundancyModel.findOne(query as any) // FIXME: typings |
399 | .then((r: any) => ({ | 451 | .then((r: any) => ({ |
400 | totalUsed: parseInt(r.totalUsed.toString(), 10), | 452 | totalUsed: parseInt(r.totalUsed.toString(), 10), |
401 | totalVideos: r.totalVideos, | 453 | totalVideos: r.totalVideos, |
@@ -403,11 +455,32 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
403 | })) | 455 | })) |
404 | } | 456 | } |
405 | 457 | ||
458 | getVideo () { | ||
459 | if (this.VideoFile) return this.VideoFile.Video | ||
460 | |||
461 | return this.VideoStreamingPlaylist.Video | ||
462 | } | ||
463 | |||
406 | isOwned () { | 464 | isOwned () { |
407 | return !!this.strategy | 465 | return !!this.strategy |
408 | } | 466 | } |
409 | 467 | ||
410 | toActivityPubObject (): CacheFileObject { | 468 | toActivityPubObject (): CacheFileObject { |
469 | if (this.VideoStreamingPlaylist) { | ||
470 | return { | ||
471 | id: this.url, | ||
472 | type: 'CacheFile' as 'CacheFile', | ||
473 | object: this.VideoStreamingPlaylist.Video.url, | ||
474 | expires: this.expiresOn.toISOString(), | ||
475 | url: { | ||
476 | type: 'Link', | ||
477 | mimeType: 'application/x-mpegURL', | ||
478 | mediaType: 'application/x-mpegURL', | ||
479 | href: this.fileUrl | ||
480 | } | ||
481 | } | ||
482 | } | ||
483 | |||
411 | return { | 484 | return { |
412 | id: this.url, | 485 | id: this.url, |
413 | type: 'CacheFile' as 'CacheFile', | 486 | type: 'CacheFile' as 'CacheFile', |
@@ -431,7 +504,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
431 | 504 | ||
432 | const notIn = Sequelize.literal( | 505 | const notIn = Sequelize.literal( |
433 | '(' + | 506 | '(' + |
434 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` + | 507 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + |
435 | ')' | 508 | ')' |
436 | ) | 509 | ) |
437 | 510 | ||
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index 4c9e2d05e..cc47644f2 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -1,17 +1,4 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AllowNull, | ||
4 | BelongsTo, | ||
5 | Column, | ||
6 | CreatedAt, | ||
7 | DataType, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | 2 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' |
16 | import { VideoAbuse } from '../../../shared/models/videos' | 3 | import { VideoAbuse } from '../../../shared/models/videos' |
17 | import { | 4 | import { |
@@ -19,7 +6,6 @@ import { | |||
19 | isVideoAbuseReasonValid, | 6 | isVideoAbuseReasonValid, |
20 | isVideoAbuseStateValid | 7 | isVideoAbuseStateValid |
21 | } from '../../helpers/custom-validators/video-abuses' | 8 | } from '../../helpers/custom-validators/video-abuses' |
22 | import { Emailer } from '../../lib/emailer' | ||
23 | import { AccountModel } from '../account/account' | 9 | import { AccountModel } from '../account/account' |
24 | import { getSort, throwIfNotValid } from '../utils' | 10 | import { getSort, throwIfNotValid } from '../utils' |
25 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' | |||
40 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | 26 | export class VideoAbuseModel extends Model<VideoAbuseModel> { |
41 | 27 | ||
42 | @AllowNull(false) | 28 | @AllowNull(false) |
29 | @Default(null) | ||
43 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) | 30 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) |
44 | @Column | 31 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) |
45 | reason: string | 32 | reason: string |
46 | 33 | ||
47 | @AllowNull(false) | 34 | @AllowNull(false) |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 23e992685..3b567e488 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,21 +1,7 @@ | |||
1 | import { | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | AfterCreate, | ||
3 | AfterDestroy, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' | 2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' |
16 | import { VideoModel } from './video' | 3 | import { VideoModel } from './video' |
17 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' | 4 | import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' |
18 | import { Emailer } from '../../lib/emailer' | ||
19 | import { VideoBlacklist } from '../../../shared/models/videos' | 5 | import { VideoBlacklist } from '../../../shared/models/videos' |
20 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 6 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
21 | 7 | ||
@@ -35,6 +21,10 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
35 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) | 21 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) |
36 | reason: string | 22 | reason: string |
37 | 23 | ||
24 | @AllowNull(false) | ||
25 | @Column | ||
26 | unfederated: boolean | ||
27 | |||
38 | @CreatedAt | 28 | @CreatedAt |
39 | createdAt: Date | 29 | createdAt: Date |
40 | 30 | ||
@@ -93,6 +83,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
93 | createdAt: this.createdAt, | 83 | createdAt: this.createdAt, |
94 | updatedAt: this.updatedAt, | 84 | updatedAt: this.updatedAt, |
95 | reason: this.reason, | 85 | reason: this.reason, |
86 | unfederated: this.unfederated, | ||
96 | 87 | ||
97 | video: { | 88 | video: { |
98 | id: video.id, | 89 | id: video.id, |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 86bf0461a..5598d80f6 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -470,4 +470,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
470 | getDisplayName () { | 470 | getDisplayName () { |
471 | return this.name | 471 | return this.name |
472 | } | 472 | } |
473 | |||
474 | isOutdated () { | ||
475 | return this.Actor.isOutdated() | ||
476 | } | ||
473 | } | 477 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 0fd868cd6..7d1e371b9 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -62,7 +62,7 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
62 | extname: string | 62 | extname: string |
63 | 63 | ||
64 | @AllowNull(false) | 64 | @AllowNull(false) |
65 | @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) | 65 | @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) |
66 | @Column | 66 | @Column |
67 | infoHash: string | 67 | infoHash: string |
68 | 68 | ||
@@ -86,14 +86,14 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
86 | 86 | ||
87 | @HasMany(() => VideoRedundancyModel, { | 87 | @HasMany(() => VideoRedundancyModel, { |
88 | foreignKey: { | 88 | foreignKey: { |
89 | allowNull: false | 89 | allowNull: true |
90 | }, | 90 | }, |
91 | onDelete: 'CASCADE', | 91 | onDelete: 'CASCADE', |
92 | hooks: true | 92 | hooks: true |
93 | }) | 93 | }) |
94 | RedundancyVideos: VideoRedundancyModel[] | 94 | RedundancyVideos: VideoRedundancyModel[] |
95 | 95 | ||
96 | static isInfohashExists (infoHash: string) { | 96 | static doesInfohashExist (infoHash: string) { |
97 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 97 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
98 | const options = { | 98 | const options = { |
99 | type: Sequelize.QueryTypes.SELECT, | 99 | type: Sequelize.QueryTypes.SELECT, |
@@ -120,6 +120,26 @@ export class VideoFileModel extends Model<VideoFileModel> { | |||
120 | return VideoFileModel.findById(id, options) | 120 | return VideoFileModel.findById(id, options) |
121 | } | 121 | } |
122 | 122 | ||
123 | static async getStats () { | ||
124 | let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { | ||
125 | include: [ | ||
126 | { | ||
127 | attributes: [], | ||
128 | model: VideoModel.unscoped(), | ||
129 | where: { | ||
130 | remote: false | ||
131 | } | ||
132 | } | ||
133 | ] | ||
134 | } as any) | ||
135 | // Sequelize could return null... | ||
136 | if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0 | ||
137 | |||
138 | return { | ||
139 | totalLocalVideoFilesSize | ||
140 | } | ||
141 | } | ||
142 | |||
123 | hasSameUniqueKeysThan (other: VideoFileModel) { | 143 | hasSameUniqueKeysThan (other: VideoFileModel) { |
124 | return this.fps === other.fps && | 144 | return this.fps === other.fps && |
125 | this.resolution === other.resolution && | 145 | this.resolution === other.resolution && |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index 7a9513cbe..c63285e25 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -1,7 +1,12 @@ | |||
1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
2 | import { VideoModel } from './video' | 2 | import { VideoModel } from './video' |
3 | import { VideoFileModel } from './video-file' | 3 | import { VideoFileModel } from './video-file' |
4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 4 | import { |
5 | ActivityPlaylistInfohashesObject, | ||
6 | ActivityPlaylistSegmentHashesObject, | ||
7 | ActivityUrlObject, | ||
8 | VideoTorrentObject | ||
9 | } from '../../../shared/models/activitypub/objects' | ||
5 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' | 10 | import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' |
6 | import { VideoCaptionModel } from './video-caption' | 11 | import { VideoCaptionModel } from './video-caption' |
7 | import { | 12 | import { |
@@ -11,6 +16,8 @@ import { | |||
11 | getVideoSharesActivityPubUrl | 16 | getVideoSharesActivityPubUrl |
12 | } from '../../lib/activitypub' | 17 | } from '../../lib/activitypub' |
13 | import { isArray } from '../../helpers/custom-validators/misc' | 18 | import { isArray } from '../../helpers/custom-validators/misc' |
19 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | ||
20 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
14 | 21 | ||
15 | export type VideoFormattingJSONOptions = { | 22 | export type VideoFormattingJSONOptions = { |
16 | completeDescription?: boolean | 23 | completeDescription?: boolean |
@@ -121,7 +128,12 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
121 | } | 128 | } |
122 | }) | 129 | }) |
123 | 130 | ||
131 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
132 | |||
124 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | 133 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] |
134 | |||
135 | const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) | ||
136 | |||
125 | const detailsJson = { | 137 | const detailsJson = { |
126 | support: video.support, | 138 | support: video.support, |
127 | descriptionPath: video.getDescriptionAPIPath(), | 139 | descriptionPath: video.getDescriptionAPIPath(), |
@@ -129,12 +141,17 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
129 | account: video.VideoChannel.Account.toFormattedJSON(), | 141 | account: video.VideoChannel.Account.toFormattedJSON(), |
130 | tags, | 142 | tags, |
131 | commentsEnabled: video.commentsEnabled, | 143 | commentsEnabled: video.commentsEnabled, |
144 | downloadEnabled: video.downloadEnabled, | ||
132 | waitTranscoding: video.waitTranscoding, | 145 | waitTranscoding: video.waitTranscoding, |
133 | state: { | 146 | state: { |
134 | id: video.state, | 147 | id: video.state, |
135 | label: VideoModel.getStateLabel(video.state) | 148 | label: VideoModel.getStateLabel(video.state) |
136 | }, | 149 | }, |
137 | files: [] | 150 | |
151 | trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs), | ||
152 | |||
153 | files: [], | ||
154 | streamingPlaylists | ||
138 | } | 155 | } |
139 | 156 | ||
140 | // Format and sort video files | 157 | // Format and sort video files |
@@ -143,6 +160,25 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | |||
143 | return Object.assign(formattedJson, detailsJson) | 160 | return Object.assign(formattedJson, detailsJson) |
144 | } | 161 | } |
145 | 162 | ||
163 | function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] { | ||
164 | if (isArray(playlists) === false) return [] | ||
165 | |||
166 | return playlists | ||
167 | .map(playlist => { | ||
168 | const redundancies = isArray(playlist.RedundancyVideos) | ||
169 | ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) | ||
170 | : [] | ||
171 | |||
172 | return { | ||
173 | id: playlist.id, | ||
174 | type: playlist.type, | ||
175 | playlistUrl: playlist.playlistUrl, | ||
176 | segmentsSha256Url: playlist.segmentsSha256Url, | ||
177 | redundancies | ||
178 | } as VideoStreamingPlaylist | ||
179 | }) | ||
180 | } | ||
181 | |||
146 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { | 182 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { |
147 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 183 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
148 | 184 | ||
@@ -233,6 +269,28 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
233 | }) | 269 | }) |
234 | } | 270 | } |
235 | 271 | ||
272 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | ||
273 | let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[] | ||
274 | |||
275 | tag = playlist.p2pMediaLoaderInfohashes | ||
276 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) | ||
277 | tag.push({ | ||
278 | type: 'Link', | ||
279 | name: 'sha256', | ||
280 | mimeType: 'application/json' as 'application/json', | ||
281 | mediaType: 'application/json' as 'application/json', | ||
282 | href: playlist.segmentsSha256Url | ||
283 | }) | ||
284 | |||
285 | url.push({ | ||
286 | type: 'Link', | ||
287 | mimeType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
288 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | ||
289 | href: playlist.playlistUrl, | ||
290 | tag | ||
291 | }) | ||
292 | } | ||
293 | |||
236 | // Add video url too | 294 | // Add video url too |
237 | url.push({ | 295 | url.push({ |
238 | type: 'Link', | 296 | type: 'Link', |
@@ -264,6 +322,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
264 | waitTranscoding: video.waitTranscoding, | 322 | waitTranscoding: video.waitTranscoding, |
265 | state: video.state, | 323 | state: video.state, |
266 | commentsEnabled: video.commentsEnabled, | 324 | commentsEnabled: video.commentsEnabled, |
325 | downloadEnabled: video.downloadEnabled, | ||
267 | published: video.publishedAt.toISOString(), | 326 | published: video.publishedAt.toISOString(), |
268 | originallyPublishedAt: video.originallyPublishedAt ? | 327 | originallyPublishedAt: video.originallyPublishedAt ? |
269 | video.originallyPublishedAt.toISOString() : | 328 | video.originallyPublishedAt.toISOString() : |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts new file mode 100644 index 000000000..bf6f7b0c4 --- /dev/null +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -0,0 +1,158 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | ||
3 | import { throwIfNotValid } from '../utils' | ||
4 | import { VideoModel } from './video' | ||
5 | import * as Sequelize from 'sequelize' | ||
6 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
7 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
9 | import { CONSTRAINTS_FIELDS, STATIC_PATHS } from '../../initializers' | ||
10 | import { VideoFileModel } from './video-file' | ||
11 | import { join } from 'path' | ||
12 | import { sha1 } from '../../helpers/core-utils' | ||
13 | import { isArrayOf } from '../../helpers/custom-validators/misc' | ||
14 | |||
15 | @Table({ | ||
16 | tableName: 'videoStreamingPlaylist', | ||
17 | indexes: [ | ||
18 | { | ||
19 | fields: [ 'videoId' ] | ||
20 | }, | ||
21 | { | ||
22 | fields: [ 'videoId', 'type' ], | ||
23 | unique: true | ||
24 | }, | ||
25 | { | ||
26 | fields: [ 'p2pMediaLoaderInfohashes' ], | ||
27 | using: 'gin' | ||
28 | } | ||
29 | ] | ||
30 | }) | ||
31 | export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> { | ||
32 | @CreatedAt | ||
33 | createdAt: Date | ||
34 | |||
35 | @UpdatedAt | ||
36 | updatedAt: Date | ||
37 | |||
38 | @AllowNull(false) | ||
39 | @Column | ||
40 | type: VideoStreamingPlaylistType | ||
41 | |||
42 | @AllowNull(false) | ||
43 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url')) | ||
44 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | ||
45 | playlistUrl: string | ||
46 | |||
47 | @AllowNull(false) | ||
48 | @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes')) | ||
49 | @Column(DataType.ARRAY(DataType.STRING)) | ||
50 | p2pMediaLoaderInfohashes: string[] | ||
51 | |||
52 | @AllowNull(false) | ||
53 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url')) | ||
54 | @Column | ||
55 | segmentsSha256Url: string | ||
56 | |||
57 | @ForeignKey(() => VideoModel) | ||
58 | @Column | ||
59 | videoId: number | ||
60 | |||
61 | @BelongsTo(() => VideoModel, { | ||
62 | foreignKey: { | ||
63 | allowNull: false | ||
64 | }, | ||
65 | onDelete: 'CASCADE' | ||
66 | }) | ||
67 | Video: VideoModel | ||
68 | |||
69 | @HasMany(() => VideoRedundancyModel, { | ||
70 | foreignKey: { | ||
71 | allowNull: false | ||
72 | }, | ||
73 | onDelete: 'CASCADE', | ||
74 | hooks: true | ||
75 | }) | ||
76 | RedundancyVideos: VideoRedundancyModel[] | ||
77 | |||
78 | static doesInfohashExist (infoHash: string) { | ||
79 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | ||
80 | const options = { | ||
81 | type: Sequelize.QueryTypes.SELECT, | ||
82 | bind: { infoHash }, | ||
83 | raw: true | ||
84 | } | ||
85 | |||
86 | return VideoModel.sequelize.query(query, options) | ||
87 | .then(results => { | ||
88 | return results.length === 1 | ||
89 | }) | ||
90 | } | ||
91 | |||
92 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: VideoFileModel[]) { | ||
93 | const hashes: string[] = [] | ||
94 | |||
95 | // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L97 | ||
96 | for (let i = 0; i < videoFiles.length; i++) { | ||
97 | hashes.push(sha1(`1${playlistUrl}+V${i}`)) | ||
98 | } | ||
99 | |||
100 | return hashes | ||
101 | } | ||
102 | |||
103 | static loadWithVideo (id: number) { | ||
104 | const options = { | ||
105 | include: [ | ||
106 | { | ||
107 | model: VideoModel.unscoped(), | ||
108 | required: true | ||
109 | } | ||
110 | ] | ||
111 | } | ||
112 | |||
113 | return VideoStreamingPlaylistModel.findById(id, options) | ||
114 | } | ||
115 | |||
116 | static getHlsPlaylistFilename (resolution: number) { | ||
117 | return resolution + '.m3u8' | ||
118 | } | ||
119 | |||
120 | static getMasterHlsPlaylistFilename () { | ||
121 | return 'master.m3u8' | ||
122 | } | ||
123 | |||
124 | static getHlsSha256SegmentsFilename () { | ||
125 | return 'segments-sha256.json' | ||
126 | } | ||
127 | |||
128 | static getHlsVideoName (uuid: string, resolution: number) { | ||
129 | return `${uuid}-${resolution}-fragmented.mp4` | ||
130 | } | ||
131 | |||
132 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | ||
133 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
134 | } | ||
135 | |||
136 | static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) { | ||
137 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | ||
138 | } | ||
139 | |||
140 | static getHlsSha256SegmentsStaticPath (videoUUID: string) { | ||
141 | return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | ||
142 | } | ||
143 | |||
144 | getStringType () { | ||
145 | if (this.type === VideoStreamingPlaylistType.HLS) return 'hls' | ||
146 | |||
147 | return 'unknown' | ||
148 | } | ||
149 | |||
150 | getVideoRedundancyUrl (baseUrlHttp: string) { | ||
151 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid | ||
152 | } | ||
153 | |||
154 | hasSameUniqueKeysThan (other: VideoStreamingPlaylistModel) { | ||
155 | return this.type === other.type && | ||
156 | this.videoId === other.videoId | ||
157 | } | ||
158 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 806b6e046..73626b6a0 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -52,7 +52,7 @@ import { | |||
52 | ACTIVITY_PUB, | 52 | ACTIVITY_PUB, |
53 | API_VERSION, | 53 | API_VERSION, |
54 | CONFIG, | 54 | CONFIG, |
55 | CONSTRAINTS_FIELDS, | 55 | CONSTRAINTS_FIELDS, HLS_PLAYLIST_DIRECTORY, HLS_REDUNDANCY_DIRECTORY, |
56 | PREVIEWS_SIZE, | 56 | PREVIEWS_SIZE, |
57 | REMOTE_SCHEME, | 57 | REMOTE_SCHEME, |
58 | STATIC_DOWNLOAD_PATHS, | 58 | STATIC_DOWNLOAD_PATHS, |
@@ -95,6 +95,7 @@ import * as validator from 'validator' | |||
95 | import { UserVideoHistoryModel } from '../account/user-video-history' | 95 | import { UserVideoHistoryModel } from '../account/user-video-history' |
96 | import { UserModel } from '../account/user' | 96 | import { UserModel } from '../account/user' |
97 | import { VideoImportModel } from './video-import' | 97 | import { VideoImportModel } from './video-import' |
98 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
98 | 99 | ||
99 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 100 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
100 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 101 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -160,7 +161,9 @@ export enum ScopeNames { | |||
160 | WITH_FILES = 'WITH_FILES', | 161 | WITH_FILES = 'WITH_FILES', |
161 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 162 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
162 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 163 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
163 | WITH_USER_HISTORY = 'WITH_USER_HISTORY' | 164 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
165 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | ||
166 | WITH_USER_ID = 'WITH_USER_ID' | ||
164 | } | 167 | } |
165 | 168 | ||
166 | type ForAPIOptions = { | 169 | type ForAPIOptions = { |
@@ -464,6 +467,22 @@ type AvailableForListIDsOptions = { | |||
464 | 467 | ||
465 | return query | 468 | return query |
466 | }, | 469 | }, |
470 | [ ScopeNames.WITH_USER_ID ]: { | ||
471 | include: [ | ||
472 | { | ||
473 | attributes: [ 'accountId' ], | ||
474 | model: () => VideoChannelModel.unscoped(), | ||
475 | required: true, | ||
476 | include: [ | ||
477 | { | ||
478 | attributes: [ 'userId' ], | ||
479 | model: () => AccountModel.unscoped(), | ||
480 | required: true | ||
481 | } | ||
482 | ] | ||
483 | } | ||
484 | ] | ||
485 | }, | ||
467 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 486 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { |
468 | include: [ | 487 | include: [ |
469 | { | 488 | { |
@@ -528,22 +547,55 @@ type AvailableForListIDsOptions = { | |||
528 | } | 547 | } |
529 | ] | 548 | ] |
530 | }, | 549 | }, |
531 | [ ScopeNames.WITH_FILES ]: { | 550 | [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => { |
532 | include: [ | 551 | let subInclude: any[] = [] |
533 | { | 552 | |
534 | model: () => VideoFileModel.unscoped(), | 553 | if (withRedundancies === true) { |
535 | // FIXME: typings | 554 | subInclude = [ |
536 | [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join | 555 | { |
537 | required: false, | 556 | attributes: [ 'fileUrl' ], |
538 | include: [ | 557 | model: VideoRedundancyModel.unscoped(), |
539 | { | 558 | required: false |
540 | attributes: [ 'fileUrl' ], | 559 | } |
541 | model: () => VideoRedundancyModel.unscoped(), | 560 | ] |
542 | required: false | 561 | } |
543 | } | 562 | |
544 | ] | 563 | return { |
545 | } | 564 | include: [ |
546 | ] | 565 | { |
566 | model: VideoFileModel.unscoped(), | ||
567 | // FIXME: typings | ||
568 | [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join | ||
569 | required: false, | ||
570 | include: subInclude | ||
571 | } | ||
572 | ] | ||
573 | } | ||
574 | }, | ||
575 | [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { | ||
576 | let subInclude: any[] = [] | ||
577 | |||
578 | if (withRedundancies === true) { | ||
579 | subInclude = [ | ||
580 | { | ||
581 | attributes: [ 'fileUrl' ], | ||
582 | model: VideoRedundancyModel.unscoped(), | ||
583 | required: false | ||
584 | } | ||
585 | ] | ||
586 | } | ||
587 | |||
588 | return { | ||
589 | include: [ | ||
590 | { | ||
591 | model: VideoStreamingPlaylistModel.unscoped(), | ||
592 | // FIXME: typings | ||
593 | [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join | ||
594 | required: false, | ||
595 | include: subInclude | ||
596 | } | ||
597 | ] | ||
598 | } | ||
547 | }, | 599 | }, |
548 | [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { | 600 | [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { |
549 | include: [ | 601 | include: [ |
@@ -666,6 +718,10 @@ export class VideoModel extends Model<VideoModel> { | |||
666 | 718 | ||
667 | @AllowNull(false) | 719 | @AllowNull(false) |
668 | @Column | 720 | @Column |
721 | downloadEnabled: boolean | ||
722 | |||
723 | @AllowNull(false) | ||
724 | @Column | ||
669 | waitTranscoding: boolean | 725 | waitTranscoding: boolean |
670 | 726 | ||
671 | @AllowNull(false) | 727 | @AllowNull(false) |
@@ -726,6 +782,16 @@ export class VideoModel extends Model<VideoModel> { | |||
726 | }) | 782 | }) |
727 | VideoFiles: VideoFileModel[] | 783 | VideoFiles: VideoFileModel[] |
728 | 784 | ||
785 | @HasMany(() => VideoStreamingPlaylistModel, { | ||
786 | foreignKey: { | ||
787 | name: 'videoId', | ||
788 | allowNull: false | ||
789 | }, | ||
790 | hooks: true, | ||
791 | onDelete: 'cascade' | ||
792 | }) | ||
793 | VideoStreamingPlaylists: VideoStreamingPlaylistModel[] | ||
794 | |||
729 | @HasMany(() => VideoShareModel, { | 795 | @HasMany(() => VideoShareModel, { |
730 | foreignKey: { | 796 | foreignKey: { |
731 | name: 'videoId', | 797 | name: 'videoId', |
@@ -851,6 +917,9 @@ export class VideoModel extends Model<VideoModel> { | |||
851 | tasks.push(instance.removeFile(file)) | 917 | tasks.push(instance.removeFile(file)) |
852 | tasks.push(instance.removeTorrent(file)) | 918 | tasks.push(instance.removeTorrent(file)) |
853 | }) | 919 | }) |
920 | |||
921 | // Remove playlists file | ||
922 | tasks.push(instance.removeStreamingPlaylist()) | ||
854 | } | 923 | } |
855 | 924 | ||
856 | // Do not wait video deletion because we could be in a transaction | 925 | // Do not wait video deletion because we could be in a transaction |
@@ -862,10 +931,6 @@ export class VideoModel extends Model<VideoModel> { | |||
862 | return undefined | 931 | return undefined |
863 | } | 932 | } |
864 | 933 | ||
865 | static list () { | ||
866 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll() | ||
867 | } | ||
868 | |||
869 | static listLocal () { | 934 | static listLocal () { |
870 | const query = { | 935 | const query = { |
871 | where: { | 936 | where: { |
@@ -873,7 +938,7 @@ export class VideoModel extends Model<VideoModel> { | |||
873 | } | 938 | } |
874 | } | 939 | } |
875 | 940 | ||
876 | return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query) | 941 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query) |
877 | } | 942 | } |
878 | 943 | ||
879 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { | 944 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { |
@@ -1204,6 +1269,16 @@ export class VideoModel extends Model<VideoModel> { | |||
1204 | return VideoModel.findOne(options) | 1269 | return VideoModel.findOne(options) |
1205 | } | 1270 | } |
1206 | 1271 | ||
1272 | static loadWithRights (id: number | string, t?: Sequelize.Transaction) { | ||
1273 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1274 | const options = { | ||
1275 | where, | ||
1276 | transaction: t | ||
1277 | } | ||
1278 | |||
1279 | return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options) | ||
1280 | } | ||
1281 | |||
1207 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { | 1282 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { |
1208 | const where = VideoModel.buildWhereIdOrUUID(id) | 1283 | const where = VideoModel.buildWhereIdOrUUID(id) |
1209 | 1284 | ||
@@ -1216,8 +1291,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1216 | return VideoModel.findOne(options) | 1291 | return VideoModel.findOne(options) |
1217 | } | 1292 | } |
1218 | 1293 | ||
1219 | static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { | 1294 | static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { |
1220 | return VideoModel.scope(ScopeNames.WITH_FILES) | 1295 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]) |
1221 | .findById(id, { transaction: t, logging }) | 1296 | .findById(id, { transaction: t, logging }) |
1222 | } | 1297 | } |
1223 | 1298 | ||
@@ -1228,9 +1303,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1228 | } | 1303 | } |
1229 | } | 1304 | } |
1230 | 1305 | ||
1231 | return VideoModel | 1306 | return VideoModel.findOne(options) |
1232 | .scope([ ScopeNames.WITH_FILES ]) | ||
1233 | .findOne(options) | ||
1234 | } | 1307 | } |
1235 | 1308 | ||
1236 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 1309 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
@@ -1252,7 +1325,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1252 | transaction | 1325 | transaction |
1253 | } | 1326 | } |
1254 | 1327 | ||
1255 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) | 1328 | return VideoModel.scope([ |
1329 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1330 | ScopeNames.WITH_FILES, | ||
1331 | ScopeNames.WITH_STREAMING_PLAYLISTS | ||
1332 | ]).findOne(query) | ||
1256 | } | 1333 | } |
1257 | 1334 | ||
1258 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { | 1335 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { |
@@ -1267,9 +1344,37 @@ export class VideoModel extends Model<VideoModel> { | |||
1267 | const scopes = [ | 1344 | const scopes = [ |
1268 | ScopeNames.WITH_TAGS, | 1345 | ScopeNames.WITH_TAGS, |
1269 | ScopeNames.WITH_BLACKLISTED, | 1346 | ScopeNames.WITH_BLACKLISTED, |
1347 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1348 | ScopeNames.WITH_SCHEDULED_UPDATE, | ||
1270 | ScopeNames.WITH_FILES, | 1349 | ScopeNames.WITH_FILES, |
1350 | ScopeNames.WITH_STREAMING_PLAYLISTS | ||
1351 | ] | ||
1352 | |||
1353 | if (userId) { | ||
1354 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings | ||
1355 | } | ||
1356 | |||
1357 | return VideoModel | ||
1358 | .scope(scopes) | ||
1359 | .findOne(options) | ||
1360 | } | ||
1361 | |||
1362 | static loadForGetAPI (id: number | string, t?: Sequelize.Transaction, userId?: number) { | ||
1363 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1364 | |||
1365 | const options = { | ||
1366 | order: [ [ 'Tags', 'name', 'ASC' ] ], | ||
1367 | where, | ||
1368 | transaction: t | ||
1369 | } | ||
1370 | |||
1371 | const scopes = [ | ||
1372 | ScopeNames.WITH_TAGS, | ||
1373 | ScopeNames.WITH_BLACKLISTED, | ||
1271 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1374 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1272 | ScopeNames.WITH_SCHEDULED_UPDATE | 1375 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1376 | { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings | ||
1377 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings | ||
1273 | ] | 1378 | ] |
1274 | 1379 | ||
1275 | if (userId) { | 1380 | if (userId) { |
@@ -1616,6 +1721,14 @@ export class VideoModel extends Model<VideoModel> { | |||
1616 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) | 1721 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) |
1617 | } | 1722 | } |
1618 | 1723 | ||
1724 | removeStreamingPlaylist (isRedundancy = false) { | ||
1725 | const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_PLAYLIST_DIRECTORY | ||
1726 | |||
1727 | const filePath = join(baseDir, this.uuid) | ||
1728 | return remove(filePath) | ||
1729 | .catch(err => logger.warn('Cannot delete playlist directory %s.', filePath, { err })) | ||
1730 | } | ||
1731 | |||
1619 | isOutdated () { | 1732 | isOutdated () { |
1620 | if (this.isOwned()) return false | 1733 | if (this.isOwned()) return false |
1621 | 1734 | ||
@@ -1650,7 +1763,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1650 | 1763 | ||
1651 | generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { | 1764 | generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { |
1652 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) | 1765 | const xs = this.getTorrentUrl(videoFile, baseUrlHttp) |
1653 | const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | 1766 | const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs) |
1654 | let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] | 1767 | let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] |
1655 | 1768 | ||
1656 | const redundancies = videoFile.RedundancyVideos | 1769 | const redundancies = videoFile.RedundancyVideos |
@@ -1667,6 +1780,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1667 | return magnetUtil.encode(magnetHash) | 1780 | return magnetUtil.encode(magnetHash) |
1668 | } | 1781 | } |
1669 | 1782 | ||
1783 | getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { | ||
1784 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | ||
1785 | } | ||
1786 | |||
1670 | getThumbnailUrl (baseUrlHttp: string) { | 1787 | getThumbnailUrl (baseUrlHttp: string) { |
1671 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() | 1788 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() |
1672 | } | 1789 | } |
@@ -1690,4 +1807,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1690 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1807 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1691 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1808 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1692 | } | 1809 | } |
1810 | |||
1811 | getBandwidthBits (videoFile: VideoFileModel) { | ||
1812 | return Math.ceil((videoFile.size * 8) / this.duration) | ||
1813 | } | ||
1693 | } | 1814 | } |
diff --git a/server/tests/api/check-params/accounts.ts b/server/tests/api/check-params/accounts.ts index 567fd072c..68f9519c6 100644 --- a/server/tests/api/check-params/accounts.ts +++ b/server/tests/api/check-params/accounts.ts | |||
@@ -10,7 +10,7 @@ import { | |||
10 | } from '../../../../shared/utils/requests/check-api-params' | 10 | } from '../../../../shared/utils/requests/check-api-params' |
11 | import { getAccount } from '../../../../shared/utils/users/accounts' | 11 | import { getAccount } from '../../../../shared/utils/users/accounts' |
12 | 12 | ||
13 | describe('Test users API validators', function () { | 13 | describe('Test accounts API validators', function () { |
14 | const path = '/api/v1/accounts/' | 14 | const path = '/api/v1/accounts/' |
15 | let server: ServerInfo | 15 | let server: ServerInfo |
16 | 16 | ||
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 4038ecbf0..07de2b5a5 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -65,6 +65,9 @@ describe('Test config API validators', function () { | |||
65 | '480p': true, | 65 | '480p': true, |
66 | '720p': false, | 66 | '720p': false, |
67 | '1080p': false | 67 | '1080p': false |
68 | }, | ||
69 | hls: { | ||
70 | enabled: false | ||
68 | } | 71 | } |
69 | }, | 72 | }, |
70 | import: { | 73 | import: { |
diff --git a/server/tests/api/check-params/contact-form.ts b/server/tests/api/check-params/contact-form.ts index 2407ac0b5..c7e014b1f 100644 --- a/server/tests/api/check-params/contact-form.ts +++ b/server/tests/api/check-params/contact-form.ts | |||
@@ -46,6 +46,8 @@ describe('Test contact form API validators', function () { | |||
46 | }) | 46 | }) |
47 | 47 | ||
48 | it('Should not accept a contact form if it is disabled in the configuration', async function () { | 48 | it('Should not accept a contact form if it is disabled in the configuration', async function () { |
49 | this.timeout(10000) | ||
50 | |||
49 | killallServers([ server ]) | 51 | killallServers([ server ]) |
50 | 52 | ||
51 | // Contact form is disabled | 53 | // Contact form is disabled |
@@ -54,6 +56,8 @@ describe('Test contact form API validators', function () { | |||
54 | }) | 56 | }) |
55 | 57 | ||
56 | it('Should not accept a contact form if from email is invalid', async function () { | 58 | it('Should not accept a contact form if from email is invalid', async function () { |
59 | this.timeout(10000) | ||
60 | |||
57 | killallServers([ server ]) | 61 | killallServers([ server ]) |
58 | 62 | ||
59 | // Email & contact form enabled | 63 | // Email & contact form enabled |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index a3e8e2e9c..13be8b460 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -464,6 +464,24 @@ describe('Test users API validators', function () { | |||
464 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | 464 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) |
465 | }) | 465 | }) |
466 | 466 | ||
467 | it('Should fail with a too small password', async function () { | ||
468 | const fields = { | ||
469 | currentPassword: 'my super password', | ||
470 | password: 'bla' | ||
471 | } | ||
472 | |||
473 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
474 | }) | ||
475 | |||
476 | it('Should fail with a too long password', async function () { | ||
477 | const fields = { | ||
478 | currentPassword: 'my super password', | ||
479 | password: 'super'.repeat(61) | ||
480 | } | ||
481 | |||
482 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
483 | }) | ||
484 | |||
467 | it('Should fail with an non authenticated user', async function () { | 485 | it('Should fail with an non authenticated user', async function () { |
468 | const fields = { | 486 | const fields = { |
469 | videoQuota: 42 | 487 | videoQuota: 42 |
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts index a79ab4201..3b8f5f14d 100644 --- a/server/tests/api/check-params/video-abuses.ts +++ b/server/tests/api/check-params/video-abuses.ts | |||
@@ -113,8 +113,8 @@ describe('Test video abuses API validators', function () { | |||
113 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 113 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
114 | }) | 114 | }) |
115 | 115 | ||
116 | it('Should fail with a reason too big', async function () { | 116 | it('Should fail with a too big reason', async function () { |
117 | const fields = { reason: 'super'.repeat(61) } | 117 | const fields = { reason: 'super'.repeat(605) } |
118 | 118 | ||
119 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 119 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) |
120 | }) | 120 | }) |
@@ -154,7 +154,7 @@ describe('Test video abuses API validators', function () { | |||
154 | }) | 154 | }) |
155 | 155 | ||
156 | it('Should fail with a bad moderation comment', async function () { | 156 | it('Should fail with a bad moderation comment', async function () { |
157 | const body = { moderationComment: 'b'.repeat(305) } | 157 | const body = { moderationComment: 'b'.repeat(3001) } |
158 | await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400) | 158 | await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400) |
159 | }) | 159 | }) |
160 | 160 | ||
diff --git a/server/tests/api/check-params/video-blacklist.ts b/server/tests/api/check-params/video-blacklist.ts index 8e1206db3..6b82643f4 100644 --- a/server/tests/api/check-params/video-blacklist.ts +++ b/server/tests/api/check-params/video-blacklist.ts | |||
@@ -4,17 +4,20 @@ import 'mocha' | |||
4 | 4 | ||
5 | import { | 5 | import { |
6 | createUser, | 6 | createUser, |
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
7 | flushTests, | 9 | flushTests, |
8 | getBlacklistedVideosList, getVideo, getVideoWithToken, | 10 | getBlacklistedVideosList, |
11 | getVideo, | ||
12 | getVideoWithToken, | ||
9 | killallServers, | 13 | killallServers, |
10 | makePostBodyRequest, | 14 | makePostBodyRequest, |
11 | makePutBodyRequest, | 15 | makePutBodyRequest, |
12 | removeVideoFromBlacklist, | 16 | removeVideoFromBlacklist, |
13 | runServer, | ||
14 | ServerInfo, | 17 | ServerInfo, |
15 | setAccessTokensToServers, | 18 | setAccessTokensToServers, |
16 | uploadVideo, | 19 | uploadVideo, |
17 | userLogin | 20 | userLogin, waitJobs |
18 | } from '../../../../shared/utils' | 21 | } from '../../../../shared/utils' |
19 | import { | 22 | import { |
20 | checkBadCountPagination, | 23 | checkBadCountPagination, |
@@ -25,8 +28,9 @@ import { VideoDetails } from '../../../../shared/models/videos' | |||
25 | import { expect } from 'chai' | 28 | import { expect } from 'chai' |
26 | 29 | ||
27 | describe('Test video blacklist API validators', function () { | 30 | describe('Test video blacklist API validators', function () { |
28 | let server: ServerInfo | 31 | let servers: ServerInfo[] |
29 | let notBlacklistedVideoId: number | 32 | let notBlacklistedVideoId: number |
33 | let remoteVideoUUID: string | ||
30 | let userAccessToken1 = '' | 34 | let userAccessToken1 = '' |
31 | let userAccessToken2 = '' | 35 | let userAccessToken2 = '' |
32 | 36 | ||
@@ -36,75 +40,89 @@ describe('Test video blacklist API validators', function () { | |||
36 | this.timeout(120000) | 40 | this.timeout(120000) |
37 | 41 | ||
38 | await flushTests() | 42 | await flushTests() |
43 | servers = await flushAndRunMultipleServers(2) | ||
39 | 44 | ||
40 | server = await runServer(1) | 45 | await setAccessTokensToServers(servers) |
41 | 46 | await doubleFollow(servers[0], servers[1]) | |
42 | await setAccessTokensToServers([ server ]) | ||
43 | 47 | ||
44 | { | 48 | { |
45 | const username = 'user1' | 49 | const username = 'user1' |
46 | const password = 'my super password' | 50 | const password = 'my super password' |
47 | await createUser(server.url, server.accessToken, username, password) | 51 | await createUser(servers[0].url, servers[0].accessToken, username, password) |
48 | userAccessToken1 = await userLogin(server, { username, password }) | 52 | userAccessToken1 = await userLogin(servers[0], { username, password }) |
49 | } | 53 | } |
50 | 54 | ||
51 | { | 55 | { |
52 | const username = 'user2' | 56 | const username = 'user2' |
53 | const password = 'my super password' | 57 | const password = 'my super password' |
54 | await createUser(server.url, server.accessToken, username, password) | 58 | await createUser(servers[0].url, servers[0].accessToken, username, password) |
55 | userAccessToken2 = await userLogin(server, { username, password }) | 59 | userAccessToken2 = await userLogin(servers[0], { username, password }) |
56 | } | 60 | } |
57 | 61 | ||
58 | { | 62 | { |
59 | const res = await uploadVideo(server.url, userAccessToken1, {}) | 63 | const res = await uploadVideo(servers[0].url, userAccessToken1, {}) |
60 | server.video = res.body.video | 64 | servers[0].video = res.body.video |
61 | } | 65 | } |
62 | 66 | ||
63 | { | 67 | { |
64 | const res = await uploadVideo(server.url, server.accessToken, {}) | 68 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, {}) |
65 | notBlacklistedVideoId = res.body.video.uuid | 69 | notBlacklistedVideoId = res.body.video.uuid |
66 | } | 70 | } |
71 | |||
72 | { | ||
73 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, {}) | ||
74 | remoteVideoUUID = res.body.video.uuid | ||
75 | } | ||
76 | |||
77 | await waitJobs(servers) | ||
67 | }) | 78 | }) |
68 | 79 | ||
69 | describe('When adding a video in blacklist', function () { | 80 | describe('When adding a video in blacklist', function () { |
70 | const basePath = '/api/v1/videos/' | 81 | const basePath = '/api/v1/videos/' |
71 | 82 | ||
72 | it('Should fail with nothing', async function () { | 83 | it('Should fail with nothing', async function () { |
73 | const path = basePath + server.video + '/blacklist' | 84 | const path = basePath + servers[0].video + '/blacklist' |
74 | const fields = {} | 85 | const fields = {} |
75 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 86 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) |
76 | }) | 87 | }) |
77 | 88 | ||
78 | it('Should fail with a wrong video', async function () { | 89 | it('Should fail with a wrong video', async function () { |
79 | const wrongPath = '/api/v1/videos/blabla/blacklist' | 90 | const wrongPath = '/api/v1/videos/blabla/blacklist' |
80 | const fields = {} | 91 | const fields = {} |
81 | await makePostBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields }) | 92 | await makePostBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) |
82 | }) | 93 | }) |
83 | 94 | ||
84 | it('Should fail with a non authenticated user', async function () { | 95 | it('Should fail with a non authenticated user', async function () { |
85 | const path = basePath + server.video + '/blacklist' | 96 | const path = basePath + servers[0].video + '/blacklist' |
86 | const fields = {} | 97 | const fields = {} |
87 | await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) | 98 | await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, statusCodeExpected: 401 }) |
88 | }) | 99 | }) |
89 | 100 | ||
90 | it('Should fail with a non admin user', async function () { | 101 | it('Should fail with a non admin user', async function () { |
91 | const path = basePath + server.video + '/blacklist' | 102 | const path = basePath + servers[0].video + '/blacklist' |
92 | const fields = {} | 103 | const fields = {} |
93 | await makePostBodyRequest({ url: server.url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) | 104 | await makePostBodyRequest({ url: servers[0].url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) |
94 | }) | 105 | }) |
95 | 106 | ||
96 | it('Should fail with an invalid reason', async function () { | 107 | it('Should fail with an invalid reason', async function () { |
97 | const path = basePath + server.video.uuid + '/blacklist' | 108 | const path = basePath + servers[0].video.uuid + '/blacklist' |
98 | const fields = { reason: 'a'.repeat(305) } | 109 | const fields = { reason: 'a'.repeat(305) } |
99 | 110 | ||
100 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 111 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) |
112 | }) | ||
113 | |||
114 | it('Should fail to unfederate a remote video', async function () { | ||
115 | const path = basePath + remoteVideoUUID + '/blacklist' | ||
116 | const fields = { unfederate: true } | ||
117 | |||
118 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 409 }) | ||
101 | }) | 119 | }) |
102 | 120 | ||
103 | it('Should succeed with the correct params', async function () { | 121 | it('Should succeed with the correct params', async function () { |
104 | const path = basePath + server.video.uuid + '/blacklist' | 122 | const path = basePath + servers[0].video.uuid + '/blacklist' |
105 | const fields = { } | 123 | const fields = { } |
106 | 124 | ||
107 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 }) | 125 | await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 204 }) |
108 | }) | 126 | }) |
109 | }) | 127 | }) |
110 | 128 | ||
@@ -114,61 +132,61 @@ describe('Test video blacklist API validators', function () { | |||
114 | it('Should fail with a wrong video', async function () { | 132 | it('Should fail with a wrong video', async function () { |
115 | const wrongPath = '/api/v1/videos/blabla/blacklist' | 133 | const wrongPath = '/api/v1/videos/blabla/blacklist' |
116 | const fields = {} | 134 | const fields = {} |
117 | await makePutBodyRequest({ url: server.url, path: wrongPath, token: server.accessToken, fields }) | 135 | await makePutBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) |
118 | }) | 136 | }) |
119 | 137 | ||
120 | it('Should fail with a video not blacklisted', async function () { | 138 | it('Should fail with a video not blacklisted', async function () { |
121 | const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist' | 139 | const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist' |
122 | const fields = {} | 140 | const fields = {} |
123 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 404 }) | 141 | await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 404 }) |
124 | }) | 142 | }) |
125 | 143 | ||
126 | it('Should fail with a non authenticated user', async function () { | 144 | it('Should fail with a non authenticated user', async function () { |
127 | const path = basePath + server.video + '/blacklist' | 145 | const path = basePath + servers[0].video + '/blacklist' |
128 | const fields = {} | 146 | const fields = {} |
129 | await makePutBodyRequest({ url: server.url, path, token: 'hello', fields, statusCodeExpected: 401 }) | 147 | await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, statusCodeExpected: 401 }) |
130 | }) | 148 | }) |
131 | 149 | ||
132 | it('Should fail with a non admin user', async function () { | 150 | it('Should fail with a non admin user', async function () { |
133 | const path = basePath + server.video + '/blacklist' | 151 | const path = basePath + servers[0].video + '/blacklist' |
134 | const fields = {} | 152 | const fields = {} |
135 | await makePutBodyRequest({ url: server.url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) | 153 | await makePutBodyRequest({ url: servers[0].url, path, token: userAccessToken2, fields, statusCodeExpected: 403 }) |
136 | }) | 154 | }) |
137 | 155 | ||
138 | it('Should fail with an invalid reason', async function () { | 156 | it('Should fail with an invalid reason', async function () { |
139 | const path = basePath + server.video.uuid + '/blacklist' | 157 | const path = basePath + servers[0].video.uuid + '/blacklist' |
140 | const fields = { reason: 'a'.repeat(305) } | 158 | const fields = { reason: 'a'.repeat(305) } |
141 | 159 | ||
142 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | 160 | await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) |
143 | }) | 161 | }) |
144 | 162 | ||
145 | it('Should succeed with the correct params', async function () { | 163 | it('Should succeed with the correct params', async function () { |
146 | const path = basePath + server.video.uuid + '/blacklist' | 164 | const path = basePath + servers[0].video.uuid + '/blacklist' |
147 | const fields = { reason: 'hello' } | 165 | const fields = { reason: 'hello' } |
148 | 166 | ||
149 | await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 }) | 167 | await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 204 }) |
150 | }) | 168 | }) |
151 | }) | 169 | }) |
152 | 170 | ||
153 | describe('When getting blacklisted video', function () { | 171 | describe('When getting blacklisted video', function () { |
154 | 172 | ||
155 | it('Should fail with a non authenticated user', async function () { | 173 | it('Should fail with a non authenticated user', async function () { |
156 | await getVideo(server.url, server.video.uuid, 401) | 174 | await getVideo(servers[0].url, servers[0].video.uuid, 401) |
157 | }) | 175 | }) |
158 | 176 | ||
159 | it('Should fail with another user', async function () { | 177 | it('Should fail with another user', async function () { |
160 | await getVideoWithToken(server.url, userAccessToken2, server.video.uuid, 403) | 178 | await getVideoWithToken(servers[0].url, userAccessToken2, servers[0].video.uuid, 403) |
161 | }) | 179 | }) |
162 | 180 | ||
163 | it('Should succeed with the owner authenticated user', async function () { | 181 | it('Should succeed with the owner authenticated user', async function () { |
164 | const res = await getVideoWithToken(server.url, userAccessToken1, server.video.uuid, 200) | 182 | const res = await getVideoWithToken(servers[0].url, userAccessToken1, servers[0].video.uuid, 200) |
165 | const video: VideoDetails = res.body | 183 | const video: VideoDetails = res.body |
166 | 184 | ||
167 | expect(video.blacklisted).to.be.true | 185 | expect(video.blacklisted).to.be.true |
168 | }) | 186 | }) |
169 | 187 | ||
170 | it('Should succeed with an admin', async function () { | 188 | it('Should succeed with an admin', async function () { |
171 | const res = await getVideoWithToken(server.url, server.accessToken, server.video.uuid, 200) | 189 | const res = await getVideoWithToken(servers[0].url, servers[0].accessToken, servers[0].video.uuid, 200) |
172 | const video: VideoDetails = res.body | 190 | const video: VideoDetails = res.body |
173 | 191 | ||
174 | expect(video.blacklisted).to.be.true | 192 | expect(video.blacklisted).to.be.true |
@@ -177,24 +195,24 @@ describe('Test video blacklist API validators', function () { | |||
177 | 195 | ||
178 | describe('When removing a video in blacklist', function () { | 196 | describe('When removing a video in blacklist', function () { |
179 | it('Should fail with a non authenticated user', async function () { | 197 | it('Should fail with a non authenticated user', async function () { |
180 | await removeVideoFromBlacklist(server.url, 'fake token', server.video.uuid, 401) | 198 | await removeVideoFromBlacklist(servers[0].url, 'fake token', servers[0].video.uuid, 401) |
181 | }) | 199 | }) |
182 | 200 | ||
183 | it('Should fail with a non admin user', async function () { | 201 | it('Should fail with a non admin user', async function () { |
184 | await removeVideoFromBlacklist(server.url, userAccessToken2, server.video.uuid, 403) | 202 | await removeVideoFromBlacklist(servers[0].url, userAccessToken2, servers[0].video.uuid, 403) |
185 | }) | 203 | }) |
186 | 204 | ||
187 | it('Should fail with an incorrect id', async function () { | 205 | it('Should fail with an incorrect id', async function () { |
188 | await removeVideoFromBlacklist(server.url, server.accessToken, 'hello', 400) | 206 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, 'hello', 400) |
189 | }) | 207 | }) |
190 | 208 | ||
191 | it('Should fail with a not blacklisted video', async function () { | 209 | it('Should fail with a not blacklisted video', async function () { |
192 | // The video was not added to the blacklist so it should fail | 210 | // The video was not added to the blacklist so it should fail |
193 | await removeVideoFromBlacklist(server.url, server.accessToken, notBlacklistedVideoId, 404) | 211 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, notBlacklistedVideoId, 404) |
194 | }) | 212 | }) |
195 | 213 | ||
196 | it('Should succeed with the correct params', async function () { | 214 | it('Should succeed with the correct params', async function () { |
197 | await removeVideoFromBlacklist(server.url, server.accessToken, server.video.uuid, 204) | 215 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, servers[0].video.uuid, 204) |
198 | }) | 216 | }) |
199 | }) | 217 | }) |
200 | 218 | ||
@@ -202,28 +220,28 @@ describe('Test video blacklist API validators', function () { | |||
202 | const basePath = '/api/v1/videos/blacklist/' | 220 | const basePath = '/api/v1/videos/blacklist/' |
203 | 221 | ||
204 | it('Should fail with a non authenticated user', async function () { | 222 | it('Should fail with a non authenticated user', async function () { |
205 | await getBlacklistedVideosList(server.url, 'fake token', 401) | 223 | await getBlacklistedVideosList(servers[0].url, 'fake token', 401) |
206 | }) | 224 | }) |
207 | 225 | ||
208 | it('Should fail with a non admin user', async function () { | 226 | it('Should fail with a non admin user', async function () { |
209 | await getBlacklistedVideosList(server.url, userAccessToken2, 403) | 227 | await getBlacklistedVideosList(servers[0].url, userAccessToken2, 403) |
210 | }) | 228 | }) |
211 | 229 | ||
212 | it('Should fail with a bad start pagination', async function () { | 230 | it('Should fail with a bad start pagination', async function () { |
213 | await checkBadStartPagination(server.url, basePath, server.accessToken) | 231 | await checkBadStartPagination(servers[0].url, basePath, servers[0].accessToken) |
214 | }) | 232 | }) |
215 | 233 | ||
216 | it('Should fail with a bad count pagination', async function () { | 234 | it('Should fail with a bad count pagination', async function () { |
217 | await checkBadCountPagination(server.url, basePath, server.accessToken) | 235 | await checkBadCountPagination(servers[0].url, basePath, servers[0].accessToken) |
218 | }) | 236 | }) |
219 | 237 | ||
220 | it('Should fail with an incorrect sort', async function () { | 238 | it('Should fail with an incorrect sort', async function () { |
221 | await checkBadSortPagination(server.url, basePath, server.accessToken) | 239 | await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken) |
222 | }) | 240 | }) |
223 | }) | 241 | }) |
224 | 242 | ||
225 | after(async function () { | 243 | after(async function () { |
226 | killallServers([ server ]) | 244 | killallServers(servers) |
227 | 245 | ||
228 | // Keep the logs if the test failed | 246 | // Keep the logs if the test failed |
229 | if (this['ok']) { | 247 | if (this['ok']) { |
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts index 7bf187007..6dd9f15f7 100644 --- a/server/tests/api/check-params/video-imports.ts +++ b/server/tests/api/check-params/video-imports.ts | |||
@@ -88,6 +88,7 @@ describe('Test video imports API validator', function () { | |||
88 | language: 'pt', | 88 | language: 'pt', |
89 | nsfw: false, | 89 | nsfw: false, |
90 | commentsEnabled: true, | 90 | commentsEnabled: true, |
91 | downloadEnabled: true, | ||
91 | waitTranscoding: true, | 92 | waitTranscoding: true, |
92 | description: 'my super description', | 93 | description: 'my super description', |
93 | support: 'my super support text', | 94 | support: 'my super support text', |
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index f26b91435..878ffe025 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -179,6 +179,7 @@ describe('Test videos API validator', function () { | |||
179 | language: 'pt', | 179 | language: 'pt', |
180 | nsfw: false, | 180 | nsfw: false, |
181 | commentsEnabled: true, | 181 | commentsEnabled: true, |
182 | downloadEnabled: true, | ||
182 | waitTranscoding: true, | 183 | waitTranscoding: true, |
183 | description: 'my super description', | 184 | description: 'my super description', |
184 | support: 'my super support text', | 185 | support: 'my super support text', |
@@ -428,6 +429,7 @@ describe('Test videos API validator', function () { | |||
428 | language: 'pt', | 429 | language: 'pt', |
429 | nsfw: false, | 430 | nsfw: false, |
430 | commentsEnabled: false, | 431 | commentsEnabled: false, |
432 | downloadEnabled: false, | ||
431 | description: 'my super description', | 433 | description: 'my super description', |
432 | privacy: VideoPrivacy.PUBLIC, | 434 | privacy: VideoPrivacy.PUBLIC, |
433 | tags: [ 'tag1', 'tag2' ] | 435 | tags: [ 'tag1', 'tag2' ] |
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 9d3ce8153..778611fff 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | viewVideo, | 17 | viewVideo, |
18 | wait, | 18 | wait, |
19 | waitUntilLog, | 19 | waitUntilLog, |
20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken | 20 | checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash |
21 | } from '../../../../shared/utils' | 21 | } from '../../../../shared/utils' |
22 | import { waitJobs } from '../../../../shared/utils/server/jobs' | 22 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
23 | 23 | ||
@@ -48,6 +48,11 @@ function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: numbe | |||
48 | 48 | ||
49 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { | 49 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { |
50 | const config = { | 50 | const config = { |
51 | transcoding: { | ||
52 | hls: { | ||
53 | enabled: true | ||
54 | } | ||
55 | }, | ||
51 | redundancy: { | 56 | redundancy: { |
52 | videos: { | 57 | videos: { |
53 | check_interval: '5 seconds', | 58 | check_interval: '5 seconds', |
@@ -85,7 +90,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams: | |||
85 | await waitJobs(servers) | 90 | await waitJobs(servers) |
86 | } | 91 | } |
87 | 92 | ||
88 | async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { | 93 | async function check1WebSeed (videoUUID?: string) { |
89 | if (!videoUUID) videoUUID = video1Server2UUID | 94 | if (!videoUUID) videoUUID = video1Server2UUID |
90 | 95 | ||
91 | const webseeds = [ | 96 | const webseeds = [ |
@@ -93,47 +98,17 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str | |||
93 | ] | 98 | ] |
94 | 99 | ||
95 | for (const server of servers) { | 100 | for (const server of servers) { |
96 | { | 101 | // With token to avoid issues with video follow constraints |
97 | // With token to avoid issues with video follow constraints | 102 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) |
98 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) | ||
99 | 103 | ||
100 | const video: VideoDetails = res.body | 104 | const video: VideoDetails = res.body |
101 | for (const f of video.files) { | 105 | for (const f of video.files) { |
102 | checkMagnetWebseeds(f, webseeds, server) | 106 | checkMagnetWebseeds(f, webseeds, server) |
103 | } | ||
104 | } | 107 | } |
105 | } | 108 | } |
106 | } | 109 | } |
107 | 110 | ||
108 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | 111 | async function check2Webseeds (videoUUID?: string) { |
109 | const res = await getStats(servers[0].url) | ||
110 | const data: ServerStats = res.body | ||
111 | |||
112 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
113 | const stat = data.videosRedundancy[0] | ||
114 | |||
115 | expect(stat.strategy).to.equal(strategy) | ||
116 | expect(stat.totalSize).to.equal(204800) | ||
117 | expect(stat.totalUsed).to.be.at.least(1).and.below(204801) | ||
118 | expect(stat.totalVideoFiles).to.equal(4) | ||
119 | expect(stat.totalVideos).to.equal(1) | ||
120 | } | ||
121 | |||
122 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
123 | const res = await getStats(servers[0].url) | ||
124 | const data: ServerStats = res.body | ||
125 | |||
126 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
127 | |||
128 | const stat = data.videosRedundancy[0] | ||
129 | expect(stat.strategy).to.equal(strategy) | ||
130 | expect(stat.totalSize).to.equal(204800) | ||
131 | expect(stat.totalUsed).to.equal(0) | ||
132 | expect(stat.totalVideoFiles).to.equal(0) | ||
133 | expect(stat.totalVideos).to.equal(0) | ||
134 | } | ||
135 | |||
136 | async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) { | ||
137 | if (!videoUUID) videoUUID = video1Server2UUID | 112 | if (!videoUUID) videoUUID = video1Server2UUID |
138 | 113 | ||
139 | const webseeds = [ | 114 | const webseeds = [ |
@@ -158,7 +133,7 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
158 | await makeGetRequest({ | 133 | await makeGetRequest({ |
159 | url: servers[1].url, | 134 | url: servers[1].url, |
160 | statusCodeExpected: 200, | 135 | statusCodeExpected: 200, |
161 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | 136 | path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`, |
162 | contentType: null | 137 | contentType: null |
163 | }) | 138 | }) |
164 | } | 139 | } |
@@ -174,6 +149,85 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
174 | } | 149 | } |
175 | } | 150 | } |
176 | 151 | ||
152 | async function check0PlaylistRedundancies (videoUUID?: string) { | ||
153 | if (!videoUUID) videoUUID = video1Server2UUID | ||
154 | |||
155 | for (const server of servers) { | ||
156 | // With token to avoid issues with video follow constraints | ||
157 | const res = await getVideoWithToken(server.url, server.accessToken, videoUUID) | ||
158 | const video: VideoDetails = res.body | ||
159 | |||
160 | expect(video.streamingPlaylists).to.be.an('array') | ||
161 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
162 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0) | ||
163 | } | ||
164 | } | ||
165 | |||
166 | async function check1PlaylistRedundancies (videoUUID?: string) { | ||
167 | if (!videoUUID) videoUUID = video1Server2UUID | ||
168 | |||
169 | for (const server of servers) { | ||
170 | const res = await getVideo(server.url, videoUUID) | ||
171 | const video: VideoDetails = res.body | ||
172 | |||
173 | expect(video.streamingPlaylists).to.have.lengthOf(1) | ||
174 | expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1) | ||
175 | |||
176 | const redundancy = video.streamingPlaylists[0].redundancies[0] | ||
177 | |||
178 | expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID) | ||
179 | } | ||
180 | |||
181 | const baseUrlPlaylist = servers[1].url + '/static/playlists/hls' | ||
182 | const baseUrlSegment = servers[0].url + '/static/redundancy/hls' | ||
183 | |||
184 | const res = await getVideo(servers[0].url, videoUUID) | ||
185 | const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0] | ||
186 | |||
187 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
188 | await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist) | ||
189 | } | ||
190 | |||
191 | for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) { | ||
192 | const files = await readdir(join(root(), directory, videoUUID)) | ||
193 | expect(files).to.have.length.at.least(4) | ||
194 | |||
195 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
196 | const filename = `${videoUUID}-${resolution}-fragmented.mp4` | ||
197 | |||
198 | expect(files.find(f => f === filename)).to.not.be.undefined | ||
199 | } | ||
200 | } | ||
201 | } | ||
202 | |||
203 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | ||
204 | const res = await getStats(servers[0].url) | ||
205 | const data: ServerStats = res.body | ||
206 | |||
207 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
208 | const stat = data.videosRedundancy[0] | ||
209 | |||
210 | expect(stat.strategy).to.equal(strategy) | ||
211 | expect(stat.totalSize).to.equal(204800) | ||
212 | expect(stat.totalUsed).to.be.at.least(1).and.below(204801) | ||
213 | expect(stat.totalVideoFiles).to.equal(4) | ||
214 | expect(stat.totalVideos).to.equal(1) | ||
215 | } | ||
216 | |||
217 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
218 | const res = await getStats(servers[0].url) | ||
219 | const data: ServerStats = res.body | ||
220 | |||
221 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
222 | |||
223 | const stat = data.videosRedundancy[0] | ||
224 | expect(stat.strategy).to.equal(strategy) | ||
225 | expect(stat.totalSize).to.equal(204800) | ||
226 | expect(stat.totalUsed).to.equal(0) | ||
227 | expect(stat.totalVideoFiles).to.equal(0) | ||
228 | expect(stat.totalVideos).to.equal(0) | ||
229 | } | ||
230 | |||
177 | async function enableRedundancyOnServer1 () { | 231 | async function enableRedundancyOnServer1 () { |
178 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) | 232 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) |
179 | 233 | ||
@@ -220,7 +274,8 @@ describe('Test videos redundancy', function () { | |||
220 | }) | 274 | }) |
221 | 275 | ||
222 | it('Should have 1 webseed on the first video', async function () { | 276 | it('Should have 1 webseed on the first video', async function () { |
223 | await check1WebSeed(strategy) | 277 | await check1WebSeed() |
278 | await check0PlaylistRedundancies() | ||
224 | await checkStatsWith1Webseed(strategy) | 279 | await checkStatsWith1Webseed(strategy) |
225 | }) | 280 | }) |
226 | 281 | ||
@@ -229,27 +284,29 @@ describe('Test videos redundancy', function () { | |||
229 | }) | 284 | }) |
230 | 285 | ||
231 | it('Should have 2 webseeds on the first video', async function () { | 286 | it('Should have 2 webseeds on the first video', async function () { |
232 | this.timeout(40000) | 287 | this.timeout(80000) |
233 | 288 | ||
234 | await waitJobs(servers) | 289 | await waitJobs(servers) |
235 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 290 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
236 | await waitJobs(servers) | 291 | await waitJobs(servers) |
237 | 292 | ||
238 | await check2Webseeds(strategy) | 293 | await check2Webseeds() |
294 | await check1PlaylistRedundancies() | ||
239 | await checkStatsWith2Webseed(strategy) | 295 | await checkStatsWith2Webseed(strategy) |
240 | }) | 296 | }) |
241 | 297 | ||
242 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { | 298 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { |
243 | this.timeout(40000) | 299 | this.timeout(80000) |
244 | 300 | ||
245 | await disableRedundancyOnServer1() | 301 | await disableRedundancyOnServer1() |
246 | 302 | ||
247 | await waitJobs(servers) | 303 | await waitJobs(servers) |
248 | await wait(5000) | 304 | await wait(5000) |
249 | 305 | ||
250 | await check1WebSeed(strategy) | 306 | await check1WebSeed() |
307 | await check0PlaylistRedundancies() | ||
251 | 308 | ||
252 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | 309 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos', join('playlists', 'hls') ]) |
253 | }) | 310 | }) |
254 | 311 | ||
255 | after(function () { | 312 | after(function () { |
@@ -267,7 +324,8 @@ describe('Test videos redundancy', function () { | |||
267 | }) | 324 | }) |
268 | 325 | ||
269 | it('Should have 1 webseed on the first video', async function () { | 326 | it('Should have 1 webseed on the first video', async function () { |
270 | await check1WebSeed(strategy) | 327 | await check1WebSeed() |
328 | await check0PlaylistRedundancies() | ||
271 | await checkStatsWith1Webseed(strategy) | 329 | await checkStatsWith1Webseed(strategy) |
272 | }) | 330 | }) |
273 | 331 | ||
@@ -276,25 +334,27 @@ describe('Test videos redundancy', function () { | |||
276 | }) | 334 | }) |
277 | 335 | ||
278 | it('Should have 2 webseeds on the first video', async function () { | 336 | it('Should have 2 webseeds on the first video', async function () { |
279 | this.timeout(40000) | 337 | this.timeout(80000) |
280 | 338 | ||
281 | await waitJobs(servers) | 339 | await waitJobs(servers) |
282 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 340 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
283 | await waitJobs(servers) | 341 | await waitJobs(servers) |
284 | 342 | ||
285 | await check2Webseeds(strategy) | 343 | await check2Webseeds() |
344 | await check1PlaylistRedundancies() | ||
286 | await checkStatsWith2Webseed(strategy) | 345 | await checkStatsWith2Webseed(strategy) |
287 | }) | 346 | }) |
288 | 347 | ||
289 | it('Should unfollow on server 1 and remove duplicated videos', async function () { | 348 | it('Should unfollow on server 1 and remove duplicated videos', async function () { |
290 | this.timeout(40000) | 349 | this.timeout(80000) |
291 | 350 | ||
292 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) | 351 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) |
293 | 352 | ||
294 | await waitJobs(servers) | 353 | await waitJobs(servers) |
295 | await wait(5000) | 354 | await wait(5000) |
296 | 355 | ||
297 | await check1WebSeed(strategy) | 356 | await check1WebSeed() |
357 | await check0PlaylistRedundancies() | ||
298 | 358 | ||
299 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | 359 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) |
300 | }) | 360 | }) |
@@ -314,7 +374,8 @@ describe('Test videos redundancy', function () { | |||
314 | }) | 374 | }) |
315 | 375 | ||
316 | it('Should have 1 webseed on the first video', async function () { | 376 | it('Should have 1 webseed on the first video', async function () { |
317 | await check1WebSeed(strategy) | 377 | await check1WebSeed() |
378 | await check0PlaylistRedundancies() | ||
318 | await checkStatsWith1Webseed(strategy) | 379 | await checkStatsWith1Webseed(strategy) |
319 | }) | 380 | }) |
320 | 381 | ||
@@ -323,18 +384,19 @@ describe('Test videos redundancy', function () { | |||
323 | }) | 384 | }) |
324 | 385 | ||
325 | it('Should still have 1 webseed on the first video', async function () { | 386 | it('Should still have 1 webseed on the first video', async function () { |
326 | this.timeout(40000) | 387 | this.timeout(80000) |
327 | 388 | ||
328 | await waitJobs(servers) | 389 | await waitJobs(servers) |
329 | await wait(15000) | 390 | await wait(15000) |
330 | await waitJobs(servers) | 391 | await waitJobs(servers) |
331 | 392 | ||
332 | await check1WebSeed(strategy) | 393 | await check1WebSeed() |
394 | await check0PlaylistRedundancies() | ||
333 | await checkStatsWith1Webseed(strategy) | 395 | await checkStatsWith1Webseed(strategy) |
334 | }) | 396 | }) |
335 | 397 | ||
336 | it('Should view 2 times the first video to have > min_views config', async function () { | 398 | it('Should view 2 times the first video to have > min_views config', async function () { |
337 | this.timeout(40000) | 399 | this.timeout(80000) |
338 | 400 | ||
339 | await viewVideo(servers[ 0 ].url, video1Server2UUID) | 401 | await viewVideo(servers[ 0 ].url, video1Server2UUID) |
340 | await viewVideo(servers[ 2 ].url, video1Server2UUID) | 402 | await viewVideo(servers[ 2 ].url, video1Server2UUID) |
@@ -344,13 +406,14 @@ describe('Test videos redundancy', function () { | |||
344 | }) | 406 | }) |
345 | 407 | ||
346 | it('Should have 2 webseeds on the first video', async function () { | 408 | it('Should have 2 webseeds on the first video', async function () { |
347 | this.timeout(40000) | 409 | this.timeout(80000) |
348 | 410 | ||
349 | await waitJobs(servers) | 411 | await waitJobs(servers) |
350 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 412 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
351 | await waitJobs(servers) | 413 | await waitJobs(servers) |
352 | 414 | ||
353 | await check2Webseeds(strategy) | 415 | await check2Webseeds() |
416 | await check1PlaylistRedundancies() | ||
354 | await checkStatsWith2Webseed(strategy) | 417 | await checkStatsWith2Webseed(strategy) |
355 | }) | 418 | }) |
356 | 419 | ||
@@ -405,7 +468,7 @@ describe('Test videos redundancy', function () { | |||
405 | }) | 468 | }) |
406 | 469 | ||
407 | it('Should still have 2 webseeds after 10 seconds', async function () { | 470 | it('Should still have 2 webseeds after 10 seconds', async function () { |
408 | this.timeout(40000) | 471 | this.timeout(80000) |
409 | 472 | ||
410 | await wait(10000) | 473 | await wait(10000) |
411 | 474 | ||
@@ -420,7 +483,7 @@ describe('Test videos redundancy', function () { | |||
420 | }) | 483 | }) |
421 | 484 | ||
422 | it('Should stop server 1 and expire video redundancy', async function () { | 485 | it('Should stop server 1 and expire video redundancy', async function () { |
423 | this.timeout(40000) | 486 | this.timeout(80000) |
424 | 487 | ||
425 | killallServers([ servers[0] ]) | 488 | killallServers([ servers[0] ]) |
426 | 489 | ||
@@ -446,10 +509,11 @@ describe('Test videos redundancy', function () { | |||
446 | await enableRedundancyOnServer1() | 509 | await enableRedundancyOnServer1() |
447 | 510 | ||
448 | await waitJobs(servers) | 511 | await waitJobs(servers) |
449 | await waitUntilLog(servers[0], 'Duplicated ', 4) | 512 | await waitUntilLog(servers[0], 'Duplicated ', 5) |
450 | await waitJobs(servers) | 513 | await waitJobs(servers) |
451 | 514 | ||
452 | await check2Webseeds(strategy) | 515 | await check2Webseeds() |
516 | await check1PlaylistRedundancies() | ||
453 | await checkStatsWith2Webseed(strategy) | 517 | await checkStatsWith2Webseed(strategy) |
454 | 518 | ||
455 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) | 519 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) |
@@ -467,8 +531,10 @@ describe('Test videos redundancy', function () { | |||
467 | await wait(1000) | 531 | await wait(1000) |
468 | 532 | ||
469 | try { | 533 | try { |
470 | await check1WebSeed(strategy, video1Server2UUID) | 534 | await check1WebSeed(video1Server2UUID) |
471 | await check2Webseeds(strategy, video2Server2UUID) | 535 | await check0PlaylistRedundancies(video1Server2UUID) |
536 | await check2Webseeds(video2Server2UUID) | ||
537 | await check1PlaylistRedundancies(video2Server2UUID) | ||
472 | 538 | ||
473 | checked = true | 539 | checked = true |
474 | } catch { | 540 | } catch { |
@@ -477,6 +543,26 @@ describe('Test videos redundancy', function () { | |||
477 | } | 543 | } |
478 | }) | 544 | }) |
479 | 545 | ||
546 | it('Should disable strategy and remove redundancies', async function () { | ||
547 | this.timeout(80000) | ||
548 | |||
549 | await waitJobs(servers) | ||
550 | |||
551 | killallServers([ servers[ 0 ] ]) | ||
552 | await reRunServer(servers[ 0 ], { | ||
553 | redundancy: { | ||
554 | videos: { | ||
555 | check_interval: '1 second', | ||
556 | strategies: [] | ||
557 | } | ||
558 | } | ||
559 | }) | ||
560 | |||
561 | await waitJobs(servers) | ||
562 | |||
563 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ join('redundancy', 'hls') ]) | ||
564 | }) | ||
565 | |||
480 | after(function () { | 566 | after(function () { |
481 | return cleanServers() | 567 | return cleanServers() |
482 | }) | 568 | }) |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index bebfc7398..0dfe6e4fe 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -57,6 +57,8 @@ function checkInitialConfig (data: CustomConfig) { | |||
57 | expect(data.transcoding.resolutions['480p']).to.be.true | 57 | expect(data.transcoding.resolutions['480p']).to.be.true |
58 | expect(data.transcoding.resolutions['720p']).to.be.true | 58 | expect(data.transcoding.resolutions['720p']).to.be.true |
59 | expect(data.transcoding.resolutions['1080p']).to.be.true | 59 | expect(data.transcoding.resolutions['1080p']).to.be.true |
60 | expect(data.transcoding.hls.enabled).to.be.true | ||
61 | |||
60 | expect(data.import.videos.http.enabled).to.be.true | 62 | expect(data.import.videos.http.enabled).to.be.true |
61 | expect(data.import.videos.torrent.enabled).to.be.true | 63 | expect(data.import.videos.torrent.enabled).to.be.true |
62 | } | 64 | } |
@@ -95,6 +97,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
95 | expect(data.transcoding.resolutions['480p']).to.be.true | 97 | expect(data.transcoding.resolutions['480p']).to.be.true |
96 | expect(data.transcoding.resolutions['720p']).to.be.false | 98 | expect(data.transcoding.resolutions['720p']).to.be.false |
97 | expect(data.transcoding.resolutions['1080p']).to.be.false | 99 | expect(data.transcoding.resolutions['1080p']).to.be.false |
100 | expect(data.transcoding.hls.enabled).to.be.false | ||
98 | 101 | ||
99 | expect(data.import.videos.http.enabled).to.be.false | 102 | expect(data.import.videos.http.enabled).to.be.false |
100 | expect(data.import.videos.torrent.enabled).to.be.false | 103 | expect(data.import.videos.torrent.enabled).to.be.false |
@@ -205,6 +208,9 @@ describe('Test config', function () { | |||
205 | '480p': true, | 208 | '480p': true, |
206 | '720p': false, | 209 | '720p': false, |
207 | '1080p': false | 210 | '1080p': false |
211 | }, | ||
212 | hls: { | ||
213 | enabled: false | ||
208 | } | 214 | } |
209 | }, | 215 | }, |
210 | import: { | 216 | import: { |
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index b0fc5d293..ad4c87c73 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts | |||
@@ -348,6 +348,7 @@ describe('Test follows', function () { | |||
348 | }, | 348 | }, |
349 | isLocal, | 349 | isLocal, |
350 | commentsEnabled: true, | 350 | commentsEnabled: true, |
351 | downloadEnabled: true, | ||
351 | duration: 5, | 352 | duration: 5, |
352 | tags: [ 'tag1', 'tag2', 'tag3' ], | 353 | tags: [ 'tag1', 'tag2', 'tag3' ], |
353 | privacy: VideoPrivacy.PUBLIC, | 354 | privacy: VideoPrivacy.PUBLIC, |
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts index cd7baadad..cd5acbe16 100644 --- a/server/tests/api/server/handle-down.ts +++ b/server/tests/api/server/handle-down.ts | |||
@@ -76,6 +76,7 @@ describe('Test handle downs', function () { | |||
76 | tags: [ 'tag1p1', 'tag2p1' ], | 76 | tags: [ 'tag1p1', 'tag2p1' ], |
77 | privacy: VideoPrivacy.PUBLIC, | 77 | privacy: VideoPrivacy.PUBLIC, |
78 | commentsEnabled: true, | 78 | commentsEnabled: true, |
79 | downloadEnabled: true, | ||
79 | channel: { | 80 | channel: { |
80 | name: 'root_channel', | 81 | name: 'root_channel', |
81 | displayName: 'Main root channel', | 82 | displayName: 'Main root channel', |
diff --git a/server/tests/api/server/redundancy.ts b/server/tests/api/server/redundancy.ts deleted file mode 100644 index 8053d0491..000000000 --- a/server/tests/api/server/redundancy.ts +++ /dev/null | |||
@@ -1,479 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { VideoDetails } from '../../../../shared/models/videos' | ||
6 | import { | ||
7 | doubleFollow, | ||
8 | flushAndRunMultipleServers, | ||
9 | getFollowingListPaginationAndSort, | ||
10 | getVideo, | ||
11 | immutableAssign, | ||
12 | killallServers, makeGetRequest, | ||
13 | root, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, unfollow, | ||
16 | uploadVideo, | ||
17 | viewVideo, | ||
18 | wait, | ||
19 | waitUntilLog, | ||
20 | checkVideoFilesWereRemoved, removeVideo | ||
21 | } from '../../../../shared/utils' | ||
22 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
23 | import * as magnetUtil from 'magnet-uri' | ||
24 | import { updateRedundancy } from '../../../../shared/utils/server/redundancy' | ||
25 | import { ActorFollow } from '../../../../shared/models/actors' | ||
26 | import { readdir } from 'fs-extra' | ||
27 | import { join } from 'path' | ||
28 | import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' | ||
29 | import { getStats } from '../../../../shared/utils/server/stats' | ||
30 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' | ||
31 | |||
32 | const expect = chai.expect | ||
33 | |||
34 | let servers: ServerInfo[] = [] | ||
35 | let video1Server2UUID: string | ||
36 | |||
37 | function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) { | ||
38 | const parsed = magnetUtil.decode(file.magnetUri) | ||
39 | |||
40 | for (const ws of baseWebseeds) { | ||
41 | const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`) | ||
42 | expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined | ||
43 | } | ||
44 | |||
45 | expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) | ||
46 | } | ||
47 | |||
48 | async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { | ||
49 | const config = { | ||
50 | redundancy: { | ||
51 | videos: { | ||
52 | check_interval: '5 seconds', | ||
53 | strategies: [ | ||
54 | immutableAssign({ | ||
55 | min_lifetime: '1 hour', | ||
56 | strategy: strategy, | ||
57 | size: '100KB' | ||
58 | }, additionalParams) | ||
59 | ] | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | servers = await flushAndRunMultipleServers(3, config) | ||
64 | |||
65 | // Get the access tokens | ||
66 | await setAccessTokensToServers(servers) | ||
67 | |||
68 | { | ||
69 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' }) | ||
70 | video1Server2UUID = res.body.video.uuid | ||
71 | |||
72 | await viewVideo(servers[ 1 ].url, video1Server2UUID) | ||
73 | } | ||
74 | |||
75 | await waitJobs(servers) | ||
76 | |||
77 | // Server 1 and server 2 follow each other | ||
78 | await doubleFollow(servers[ 0 ], servers[ 1 ]) | ||
79 | // Server 1 and server 3 follow each other | ||
80 | await doubleFollow(servers[ 0 ], servers[ 2 ]) | ||
81 | // Server 2 and server 3 follow each other | ||
82 | await doubleFollow(servers[ 1 ], servers[ 2 ]) | ||
83 | |||
84 | await waitJobs(servers) | ||
85 | } | ||
86 | |||
87 | async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { | ||
88 | if (!videoUUID) videoUUID = video1Server2UUID | ||
89 | |||
90 | const webseeds = [ | ||
91 | 'http://localhost:9002/static/webseed/' + videoUUID | ||
92 | ] | ||
93 | |||
94 | for (const server of servers) { | ||
95 | { | ||
96 | const res = await getVideo(server.url, videoUUID) | ||
97 | |||
98 | const video: VideoDetails = res.body | ||
99 | for (const f of video.files) { | ||
100 | checkMagnetWebseeds(f, webseeds, server) | ||
101 | } | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | |||
106 | async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { | ||
107 | const res = await getStats(servers[0].url) | ||
108 | const data: ServerStats = res.body | ||
109 | |||
110 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
111 | const stat = data.videosRedundancy[0] | ||
112 | |||
113 | expect(stat.strategy).to.equal(strategy) | ||
114 | expect(stat.totalSize).to.equal(102400) | ||
115 | expect(stat.totalUsed).to.be.at.least(1).and.below(102401) | ||
116 | expect(stat.totalVideoFiles).to.equal(4) | ||
117 | expect(stat.totalVideos).to.equal(1) | ||
118 | } | ||
119 | |||
120 | async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { | ||
121 | const res = await getStats(servers[0].url) | ||
122 | const data: ServerStats = res.body | ||
123 | |||
124 | expect(data.videosRedundancy).to.have.lengthOf(1) | ||
125 | |||
126 | const stat = data.videosRedundancy[0] | ||
127 | expect(stat.strategy).to.equal(strategy) | ||
128 | expect(stat.totalSize).to.equal(102400) | ||
129 | expect(stat.totalUsed).to.equal(0) | ||
130 | expect(stat.totalVideoFiles).to.equal(0) | ||
131 | expect(stat.totalVideos).to.equal(0) | ||
132 | } | ||
133 | |||
134 | async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) { | ||
135 | if (!videoUUID) videoUUID = video1Server2UUID | ||
136 | |||
137 | const webseeds = [ | ||
138 | 'http://localhost:9001/static/webseed/' + videoUUID, | ||
139 | 'http://localhost:9002/static/webseed/' + videoUUID | ||
140 | ] | ||
141 | |||
142 | for (const server of servers) { | ||
143 | const res = await getVideo(server.url, videoUUID) | ||
144 | |||
145 | const video: VideoDetails = res.body | ||
146 | |||
147 | for (const file of video.files) { | ||
148 | checkMagnetWebseeds(file, webseeds, server) | ||
149 | |||
150 | // Only servers 1 and 2 have the video | ||
151 | if (server.serverNumber !== 3) { | ||
152 | await makeGetRequest({ | ||
153 | url: server.url, | ||
154 | statusCodeExpected: 200, | ||
155 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | ||
156 | contentType: null | ||
157 | }) | ||
158 | } | ||
159 | } | ||
160 | } | ||
161 | |||
162 | for (const directory of [ 'test1', 'test2' ]) { | ||
163 | const files = await readdir(join(root(), directory, 'videos')) | ||
164 | expect(files).to.have.length.at.least(4) | ||
165 | |||
166 | for (const resolution of [ 240, 360, 480, 720 ]) { | ||
167 | expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined | ||
168 | } | ||
169 | } | ||
170 | } | ||
171 | |||
172 | async function enableRedundancyOnServer1 () { | ||
173 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) | ||
174 | |||
175 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') | ||
176 | const follows: ActorFollow[] = res.body.data | ||
177 | const server2 = follows.find(f => f.following.host === 'localhost:9002') | ||
178 | const server3 = follows.find(f => f.following.host === 'localhost:9003') | ||
179 | |||
180 | expect(server3).to.not.be.undefined | ||
181 | expect(server3.following.hostRedundancyAllowed).to.be.false | ||
182 | |||
183 | expect(server2).to.not.be.undefined | ||
184 | expect(server2.following.hostRedundancyAllowed).to.be.true | ||
185 | } | ||
186 | |||
187 | async function disableRedundancyOnServer1 () { | ||
188 | await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false) | ||
189 | |||
190 | const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') | ||
191 | const follows: ActorFollow[] = res.body.data | ||
192 | const server2 = follows.find(f => f.following.host === 'localhost:9002') | ||
193 | const server3 = follows.find(f => f.following.host === 'localhost:9003') | ||
194 | |||
195 | expect(server3).to.not.be.undefined | ||
196 | expect(server3.following.hostRedundancyAllowed).to.be.false | ||
197 | |||
198 | expect(server2).to.not.be.undefined | ||
199 | expect(server2.following.hostRedundancyAllowed).to.be.false | ||
200 | } | ||
201 | |||
202 | async function cleanServers () { | ||
203 | killallServers(servers) | ||
204 | } | ||
205 | |||
206 | describe('Test videos redundancy', function () { | ||
207 | |||
208 | describe('With most-views strategy', function () { | ||
209 | const strategy = 'most-views' | ||
210 | |||
211 | before(function () { | ||
212 | this.timeout(120000) | ||
213 | |||
214 | return runServers(strategy) | ||
215 | }) | ||
216 | |||
217 | it('Should have 1 webseed on the first video', async function () { | ||
218 | await check1WebSeed(strategy) | ||
219 | await checkStatsWith1Webseed(strategy) | ||
220 | }) | ||
221 | |||
222 | it('Should enable redundancy on server 1', function () { | ||
223 | return enableRedundancyOnServer1() | ||
224 | }) | ||
225 | |||
226 | it('Should have 2 webseed on the first video', async function () { | ||
227 | this.timeout(40000) | ||
228 | |||
229 | await waitJobs(servers) | ||
230 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
231 | await waitJobs(servers) | ||
232 | |||
233 | await check2Webseeds(strategy) | ||
234 | await checkStatsWith2Webseed(strategy) | ||
235 | }) | ||
236 | |||
237 | it('Should undo redundancy on server 1 and remove duplicated videos', async function () { | ||
238 | this.timeout(40000) | ||
239 | |||
240 | await disableRedundancyOnServer1() | ||
241 | |||
242 | await waitJobs(servers) | ||
243 | await wait(5000) | ||
244 | |||
245 | await check1WebSeed(strategy) | ||
246 | |||
247 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | ||
248 | }) | ||
249 | |||
250 | after(function () { | ||
251 | return cleanServers() | ||
252 | }) | ||
253 | }) | ||
254 | |||
255 | describe('With trending strategy', function () { | ||
256 | const strategy = 'trending' | ||
257 | |||
258 | before(function () { | ||
259 | this.timeout(120000) | ||
260 | |||
261 | return runServers(strategy) | ||
262 | }) | ||
263 | |||
264 | it('Should have 1 webseed on the first video', async function () { | ||
265 | await check1WebSeed(strategy) | ||
266 | await checkStatsWith1Webseed(strategy) | ||
267 | }) | ||
268 | |||
269 | it('Should enable redundancy on server 1', function () { | ||
270 | return enableRedundancyOnServer1() | ||
271 | }) | ||
272 | |||
273 | it('Should have 2 webseed on the first video', async function () { | ||
274 | this.timeout(40000) | ||
275 | |||
276 | await waitJobs(servers) | ||
277 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
278 | await waitJobs(servers) | ||
279 | |||
280 | await check2Webseeds(strategy) | ||
281 | await checkStatsWith2Webseed(strategy) | ||
282 | }) | ||
283 | |||
284 | it('Should unfollow on server 1 and remove duplicated videos', async function () { | ||
285 | this.timeout(40000) | ||
286 | |||
287 | await unfollow(servers[0].url, servers[0].accessToken, servers[1]) | ||
288 | |||
289 | await waitJobs(servers) | ||
290 | await wait(5000) | ||
291 | |||
292 | await check1WebSeed(strategy) | ||
293 | |||
294 | await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) | ||
295 | }) | ||
296 | |||
297 | after(function () { | ||
298 | return cleanServers() | ||
299 | }) | ||
300 | }) | ||
301 | |||
302 | describe('With recently added strategy', function () { | ||
303 | const strategy = 'recently-added' | ||
304 | |||
305 | before(function () { | ||
306 | this.timeout(120000) | ||
307 | |||
308 | return runServers(strategy, { min_views: 3 }) | ||
309 | }) | ||
310 | |||
311 | it('Should have 1 webseed on the first video', async function () { | ||
312 | await check1WebSeed(strategy) | ||
313 | await checkStatsWith1Webseed(strategy) | ||
314 | }) | ||
315 | |||
316 | it('Should enable redundancy on server 1', function () { | ||
317 | return enableRedundancyOnServer1() | ||
318 | }) | ||
319 | |||
320 | it('Should still have 1 webseed on the first video', async function () { | ||
321 | this.timeout(40000) | ||
322 | |||
323 | await waitJobs(servers) | ||
324 | await wait(15000) | ||
325 | await waitJobs(servers) | ||
326 | |||
327 | await check1WebSeed(strategy) | ||
328 | await checkStatsWith1Webseed(strategy) | ||
329 | }) | ||
330 | |||
331 | it('Should view 2 times the first video to have > min_views config', async function () { | ||
332 | this.timeout(40000) | ||
333 | |||
334 | await viewVideo(servers[ 0 ].url, video1Server2UUID) | ||
335 | await viewVideo(servers[ 2 ].url, video1Server2UUID) | ||
336 | |||
337 | await wait(10000) | ||
338 | await waitJobs(servers) | ||
339 | }) | ||
340 | |||
341 | it('Should have 2 webseed on the first video', async function () { | ||
342 | this.timeout(40000) | ||
343 | |||
344 | await waitJobs(servers) | ||
345 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
346 | await waitJobs(servers) | ||
347 | |||
348 | await check2Webseeds(strategy) | ||
349 | await checkStatsWith2Webseed(strategy) | ||
350 | }) | ||
351 | |||
352 | it('Should remove the video and the redundancy files', async function () { | ||
353 | this.timeout(20000) | ||
354 | |||
355 | await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID) | ||
356 | |||
357 | await waitJobs(servers) | ||
358 | |||
359 | for (const server of servers) { | ||
360 | await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber) | ||
361 | } | ||
362 | }) | ||
363 | |||
364 | after(function () { | ||
365 | return cleanServers() | ||
366 | }) | ||
367 | }) | ||
368 | |||
369 | describe('Test expiration', function () { | ||
370 | const strategy = 'recently-added' | ||
371 | |||
372 | async function checkContains (servers: ServerInfo[], str: string) { | ||
373 | for (const server of servers) { | ||
374 | const res = await getVideo(server.url, video1Server2UUID) | ||
375 | const video: VideoDetails = res.body | ||
376 | |||
377 | for (const f of video.files) { | ||
378 | expect(f.magnetUri).to.contain(str) | ||
379 | } | ||
380 | } | ||
381 | } | ||
382 | |||
383 | async function checkNotContains (servers: ServerInfo[], str: string) { | ||
384 | for (const server of servers) { | ||
385 | const res = await getVideo(server.url, video1Server2UUID) | ||
386 | const video: VideoDetails = res.body | ||
387 | |||
388 | for (const f of video.files) { | ||
389 | expect(f.magnetUri).to.not.contain(str) | ||
390 | } | ||
391 | } | ||
392 | } | ||
393 | |||
394 | before(async function () { | ||
395 | this.timeout(120000) | ||
396 | |||
397 | await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) | ||
398 | |||
399 | await enableRedundancyOnServer1() | ||
400 | }) | ||
401 | |||
402 | it('Should still have 2 webseeds after 10 seconds', async function () { | ||
403 | this.timeout(40000) | ||
404 | |||
405 | await wait(10000) | ||
406 | |||
407 | try { | ||
408 | await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') | ||
409 | } catch { | ||
410 | // Maybe a server deleted a redundancy in the scheduler | ||
411 | await wait(2000) | ||
412 | |||
413 | await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') | ||
414 | } | ||
415 | }) | ||
416 | |||
417 | it('Should stop server 1 and expire video redundancy', async function () { | ||
418 | this.timeout(40000) | ||
419 | |||
420 | killallServers([ servers[0] ]) | ||
421 | |||
422 | await wait(15000) | ||
423 | |||
424 | await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') | ||
425 | }) | ||
426 | |||
427 | after(function () { | ||
428 | return killallServers([ servers[1], servers[2] ]) | ||
429 | }) | ||
430 | }) | ||
431 | |||
432 | describe('Test file replacement', function () { | ||
433 | let video2Server2UUID: string | ||
434 | const strategy = 'recently-added' | ||
435 | |||
436 | before(async function () { | ||
437 | this.timeout(120000) | ||
438 | |||
439 | await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) | ||
440 | |||
441 | await enableRedundancyOnServer1() | ||
442 | |||
443 | await waitJobs(servers) | ||
444 | await waitUntilLog(servers[0], 'Duplicated ', 4) | ||
445 | await waitJobs(servers) | ||
446 | |||
447 | await check2Webseeds(strategy) | ||
448 | await checkStatsWith2Webseed(strategy) | ||
449 | |||
450 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) | ||
451 | video2Server2UUID = res.body.video.uuid | ||
452 | }) | ||
453 | |||
454 | it('Should cache video 2 webseed on the first video', async function () { | ||
455 | this.timeout(120000) | ||
456 | |||
457 | await waitJobs(servers) | ||
458 | |||
459 | let checked = false | ||
460 | |||
461 | while (checked === false) { | ||
462 | await wait(1000) | ||
463 | |||
464 | try { | ||
465 | await check1WebSeed(strategy, video1Server2UUID) | ||
466 | await check2Webseeds(strategy, video2Server2UUID) | ||
467 | |||
468 | checked = true | ||
469 | } catch { | ||
470 | checked = false | ||
471 | } | ||
472 | } | ||
473 | }) | ||
474 | |||
475 | after(function () { | ||
476 | return cleanServers() | ||
477 | }) | ||
478 | }) | ||
479 | }) | ||
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts index d4c08c346..ee0fffd5a 100644 --- a/server/tests/api/server/reverse-proxy.ts +++ b/server/tests/api/server/reverse-proxy.ts | |||
@@ -95,7 +95,7 @@ describe('Test application behind a reverse proxy', function () { | |||
95 | it('Should rate limit logins', async function () { | 95 | it('Should rate limit logins', async function () { |
96 | const user = { username: 'root', password: 'fail' } | 96 | const user = { username: 'root', password: 'fail' } |
97 | 97 | ||
98 | for (let i = 0; i < 14; i++) { | 98 | for (let i = 0; i < 19; i++) { |
99 | await userLogin(server, user, 400) | 99 | await userLogin(server, user, 400) |
100 | } | 100 | } |
101 | 101 | ||
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts index 517b4e542..aaa6c62f7 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts | |||
@@ -39,7 +39,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
39 | } | 39 | } |
40 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) | 40 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) |
41 | 41 | ||
42 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, {}) | 42 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' }) |
43 | const videoUUID = resVideo.body.video.uuid | 43 | const videoUUID = resVideo.body.video.uuid |
44 | 44 | ||
45 | await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment') | 45 | await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment') |
@@ -60,6 +60,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
60 | expect(data.totalLocalVideoComments).to.equal(1) | 60 | expect(data.totalLocalVideoComments).to.equal(1) |
61 | expect(data.totalLocalVideos).to.equal(1) | 61 | expect(data.totalLocalVideos).to.equal(1) |
62 | expect(data.totalLocalVideoViews).to.equal(1) | 62 | expect(data.totalLocalVideoViews).to.equal(1) |
63 | expect(data.totalLocalVideoFilesSize).to.equal(218910) | ||
63 | expect(data.totalUsers).to.equal(2) | 64 | expect(data.totalUsers).to.equal(2) |
64 | expect(data.totalVideoComments).to.equal(1) | 65 | expect(data.totalVideoComments).to.equal(1) |
65 | expect(data.totalVideos).to.equal(1) | 66 | expect(data.totalVideos).to.equal(1) |
@@ -74,6 +75,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
74 | expect(data.totalLocalVideoComments).to.equal(0) | 75 | expect(data.totalLocalVideoComments).to.equal(0) |
75 | expect(data.totalLocalVideos).to.equal(0) | 76 | expect(data.totalLocalVideos).to.equal(0) |
76 | expect(data.totalLocalVideoViews).to.equal(0) | 77 | expect(data.totalLocalVideoViews).to.equal(0) |
78 | expect(data.totalLocalVideoFilesSize).to.equal(0) | ||
77 | expect(data.totalUsers).to.equal(1) | 79 | expect(data.totalUsers).to.equal(1) |
78 | expect(data.totalVideoComments).to.equal(1) | 80 | expect(data.totalVideoComments).to.equal(1) |
79 | expect(data.totalVideos).to.equal(1) | 81 | expect(data.totalVideos).to.equal(1) |
diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/users/user-notifications.ts index 5260d64cc..69e51677e 100644 --- a/server/tests/api/users/user-notifications.ts +++ b/server/tests/api/users/user-notifications.ts | |||
@@ -165,6 +165,8 @@ describe('Test users notifications', function () { | |||
165 | }) | 165 | }) |
166 | 166 | ||
167 | it('Should not send notifications if the user does not follow the video publisher', async function () { | 167 | it('Should not send notifications if the user does not follow the video publisher', async function () { |
168 | this.timeout(10000) | ||
169 | |||
168 | await uploadVideoByLocalAccount(servers) | 170 | await uploadVideoByLocalAccount(servers) |
169 | 171 | ||
170 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) | 172 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) |
@@ -644,6 +646,8 @@ describe('Test users notifications', function () { | |||
644 | }) | 646 | }) |
645 | 647 | ||
646 | it('Should not send a notification if transcoding is not enabled', async function () { | 648 | it('Should not send a notification if transcoding is not enabled', async function () { |
649 | this.timeout(10000) | ||
650 | |||
647 | const { name, uuid } = await uploadVideoByLocalAccount(servers) | 651 | const { name, uuid } = await uploadVideoByLocalAccount(servers) |
648 | await waitJobs(servers) | 652 | await waitJobs(servers) |
649 | 653 | ||
@@ -717,6 +721,24 @@ describe('Test users notifications', function () { | |||
717 | await wait(6000) | 721 | await wait(6000) |
718 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') | 722 | await checkVideoIsPublished(baseParams, name, uuid, 'presence') |
719 | }) | 723 | }) |
724 | |||
725 | it('Should not send a notification before the video is published', async function () { | ||
726 | this.timeout(20000) | ||
727 | |||
728 | let updateAt = new Date(new Date().getTime() + 100000) | ||
729 | |||
730 | const data = { | ||
731 | privacy: VideoPrivacy.PRIVATE, | ||
732 | scheduleUpdate: { | ||
733 | updateAt: updateAt.toISOString(), | ||
734 | privacy: VideoPrivacy.PUBLIC | ||
735 | } | ||
736 | } | ||
737 | const { name, uuid } = await uploadVideoByRemoteAccount(servers, data) | ||
738 | |||
739 | await wait(6000) | ||
740 | await checkVideoIsPublished(baseParams, name, uuid, 'absence') | ||
741 | }) | ||
720 | }) | 742 | }) |
721 | 743 | ||
722 | describe('My video is imported', function () { | 744 | describe('My video is imported', function () { |
@@ -781,6 +803,8 @@ describe('Test users notifications', function () { | |||
781 | }) | 803 | }) |
782 | 804 | ||
783 | it('Should send a notification only to moderators when a user registers on the instance', async function () { | 805 | it('Should send a notification only to moderators when a user registers on the instance', async function () { |
806 | this.timeout(10000) | ||
807 | |||
784 | await registerUser(servers[0].url, 'user_45', 'password') | 808 | await registerUser(servers[0].url, 'user_45', 'password') |
785 | 809 | ||
786 | await waitJobs(servers) | 810 | await waitJobs(servers) |
@@ -849,6 +873,8 @@ describe('Test users notifications', function () { | |||
849 | }) | 873 | }) |
850 | 874 | ||
851 | it('Should notify when a local account is following one of our channel', async function () { | 875 | it('Should notify when a local account is following one of our channel', async function () { |
876 | this.timeout(10000) | ||
877 | |||
852 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') | 878 | await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') |
853 | 879 | ||
854 | await waitJobs(servers) | 880 | await waitJobs(servers) |
@@ -857,6 +883,8 @@ describe('Test users notifications', function () { | |||
857 | }) | 883 | }) |
858 | 884 | ||
859 | it('Should notify when a remote account is following one of our channel', async function () { | 885 | it('Should notify when a remote account is following one of our channel', async function () { |
886 | this.timeout(10000) | ||
887 | |||
860 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') | 888 | await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') |
861 | 889 | ||
862 | await waitJobs(servers) | 890 | await waitJobs(servers) |
@@ -926,6 +954,8 @@ describe('Test users notifications', function () { | |||
926 | }) | 954 | }) |
927 | 955 | ||
928 | it('Should not have notifications', async function () { | 956 | it('Should not have notifications', async function () { |
957 | this.timeout(10000) | ||
958 | |||
929 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 959 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
930 | newVideoFromSubscription: UserNotificationSettingValue.NONE | 960 | newVideoFromSubscription: UserNotificationSettingValue.NONE |
931 | })) | 961 | })) |
@@ -943,6 +973,8 @@ describe('Test users notifications', function () { | |||
943 | }) | 973 | }) |
944 | 974 | ||
945 | it('Should only have web notifications', async function () { | 975 | it('Should only have web notifications', async function () { |
976 | this.timeout(10000) | ||
977 | |||
946 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 978 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
947 | newVideoFromSubscription: UserNotificationSettingValue.WEB | 979 | newVideoFromSubscription: UserNotificationSettingValue.WEB |
948 | })) | 980 | })) |
@@ -967,6 +999,8 @@ describe('Test users notifications', function () { | |||
967 | }) | 999 | }) |
968 | 1000 | ||
969 | it('Should only have mail notifications', async function () { | 1001 | it('Should only have mail notifications', async function () { |
1002 | this.timeout(10000) | ||
1003 | |||
970 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 1004 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
971 | newVideoFromSubscription: UserNotificationSettingValue.EMAIL | 1005 | newVideoFromSubscription: UserNotificationSettingValue.EMAIL |
972 | })) | 1006 | })) |
@@ -991,6 +1025,8 @@ describe('Test users notifications', function () { | |||
991 | }) | 1025 | }) |
992 | 1026 | ||
993 | it('Should have email and web notifications', async function () { | 1027 | it('Should have email and web notifications', async function () { |
1028 | this.timeout(10000) | ||
1029 | |||
994 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { | 1030 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { |
995 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | 1031 | newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL |
996 | })) | 1032 | })) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index ad98ab1c7..c4465d541 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -501,6 +501,22 @@ describe('Test users', function () { | |||
501 | accessTokenUser = await userLogin(server, user) | 501 | accessTokenUser = await userLogin(server, user) |
502 | }) | 502 | }) |
503 | 503 | ||
504 | it('Should be able to update another user password', async function () { | ||
505 | await updateUser({ | ||
506 | url: server.url, | ||
507 | userId, | ||
508 | accessToken, | ||
509 | password: 'password updated' | ||
510 | }) | ||
511 | |||
512 | await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401) | ||
513 | |||
514 | await userLogin(server, user, 400) | ||
515 | |||
516 | user.password = 'password updated' | ||
517 | accessTokenUser = await userLogin(server, user) | ||
518 | }) | ||
519 | |||
504 | it('Should be able to list video blacklist by a moderator', async function () { | 520 | it('Should be able to list video blacklist by a moderator', async function () { |
505 | await getBlacklistedVideosList(server.url, accessTokenUser) | 521 | await getBlacklistedVideosList(server.url, accessTokenUser) |
506 | }) | 522 | }) |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index 9bdb78491..a501a80b2 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -3,12 +3,12 @@ import './services' | |||
3 | import './single-server' | 3 | import './single-server' |
4 | import './video-abuse' | 4 | import './video-abuse' |
5 | import './video-blacklist' | 5 | import './video-blacklist' |
6 | import './video-blacklist-management' | ||
7 | import './video-captions' | 6 | import './video-captions' |
8 | import './video-change-ownership' | 7 | import './video-change-ownership' |
9 | import './video-channels' | 8 | import './video-channels' |
10 | import './video-comments' | 9 | import './video-comments' |
11 | import './video-description' | 10 | import './video-description' |
11 | import './video-hls' | ||
12 | import './video-imports' | 12 | import './video-imports' |
13 | import './video-nsfw' | 13 | import './video-nsfw' |
14 | import './video-privacy' | 14 | import './video-privacy' |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 6c281e49e..1b471ba79 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -128,6 +128,7 @@ describe('Test multiple servers', function () { | |||
128 | tags: [ 'tag1p1', 'tag2p1' ], | 128 | tags: [ 'tag1p1', 'tag2p1' ], |
129 | privacy: VideoPrivacy.PUBLIC, | 129 | privacy: VideoPrivacy.PUBLIC, |
130 | commentsEnabled: true, | 130 | commentsEnabled: true, |
131 | downloadEnabled: true, | ||
131 | channel: { | 132 | channel: { |
132 | displayName: 'my channel', | 133 | displayName: 'my channel', |
133 | name: 'super_channel_name', | 134 | name: 'super_channel_name', |
@@ -199,6 +200,7 @@ describe('Test multiple servers', function () { | |||
199 | }, | 200 | }, |
200 | isLocal, | 201 | isLocal, |
201 | commentsEnabled: true, | 202 | commentsEnabled: true, |
203 | downloadEnabled: true, | ||
202 | duration: 5, | 204 | duration: 5, |
203 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], | 205 | tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], |
204 | privacy: VideoPrivacy.PUBLIC, | 206 | privacy: VideoPrivacy.PUBLIC, |
@@ -307,6 +309,7 @@ describe('Test multiple servers', function () { | |||
307 | isLocal, | 309 | isLocal, |
308 | duration: 5, | 310 | duration: 5, |
309 | commentsEnabled: true, | 311 | commentsEnabled: true, |
312 | downloadEnabled: true, | ||
310 | tags: [ 'tag1p3' ], | 313 | tags: [ 'tag1p3' ], |
311 | privacy: VideoPrivacy.PUBLIC, | 314 | privacy: VideoPrivacy.PUBLIC, |
312 | channel: { | 315 | channel: { |
@@ -338,6 +341,7 @@ describe('Test multiple servers', function () { | |||
338 | host: 'localhost:9003' | 341 | host: 'localhost:9003' |
339 | }, | 342 | }, |
340 | commentsEnabled: true, | 343 | commentsEnabled: true, |
344 | downloadEnabled: true, | ||
341 | isLocal, | 345 | isLocal, |
342 | duration: 5, | 346 | duration: 5, |
343 | tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ], | 347 | tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ], |
@@ -655,6 +659,7 @@ describe('Test multiple servers', function () { | |||
655 | isLocal, | 659 | isLocal, |
656 | duration: 5, | 660 | duration: 5, |
657 | commentsEnabled: true, | 661 | commentsEnabled: true, |
662 | downloadEnabled: true, | ||
658 | tags: [ 'tag_up_1', 'tag_up_2' ], | 663 | tags: [ 'tag_up_1', 'tag_up_2' ], |
659 | privacy: VideoPrivacy.PUBLIC, | 664 | privacy: VideoPrivacy.PUBLIC, |
660 | channel: { | 665 | channel: { |
@@ -914,11 +919,12 @@ describe('Test multiple servers', function () { | |||
914 | } | 919 | } |
915 | }) | 920 | }) |
916 | 921 | ||
917 | it('Should disable comments', async function () { | 922 | it('Should disable comments and download', async function () { |
918 | this.timeout(20000) | 923 | this.timeout(20000) |
919 | 924 | ||
920 | const attributes = { | 925 | const attributes = { |
921 | commentsEnabled: false | 926 | commentsEnabled: false, |
927 | downloadEnabled: false | ||
922 | } | 928 | } |
923 | 929 | ||
924 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, attributes) | 930 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, attributes) |
@@ -928,6 +934,7 @@ describe('Test multiple servers', function () { | |||
928 | for (const server of servers) { | 934 | for (const server of servers) { |
929 | const res = await getVideo(server.url, videoUUID) | 935 | const res = await getVideo(server.url, videoUUID) |
930 | expect(res.body.commentsEnabled).to.be.false | 936 | expect(res.body.commentsEnabled).to.be.false |
937 | expect(res.body.downloadEnabled).to.be.false | ||
931 | 938 | ||
932 | const text = 'my super forbidden comment' | 939 | const text = 'my super forbidden comment' |
933 | await addVideoCommentThread(server.url, server.accessToken, videoUUID, text, 409) | 940 | await addVideoCommentThread(server.url, server.accessToken, videoUUID, text, 409) |
@@ -976,6 +983,7 @@ describe('Test multiple servers', function () { | |||
976 | isLocal, | 983 | isLocal, |
977 | duration: 5, | 984 | duration: 5, |
978 | commentsEnabled: false, | 985 | commentsEnabled: false, |
986 | downloadEnabled: false, | ||
979 | tags: [ ], | 987 | tags: [ ], |
980 | privacy: VideoPrivacy.PUBLIC, | 988 | privacy: VideoPrivacy.PUBLIC, |
981 | channel: { | 989 | channel: { |
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index 069dec67c..cfdcbaf3f 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -55,6 +55,7 @@ describe('Test a single server', function () { | |||
55 | tags: [ 'tag1', 'tag2', 'tag3' ], | 55 | tags: [ 'tag1', 'tag2', 'tag3' ], |
56 | privacy: VideoPrivacy.PUBLIC, | 56 | privacy: VideoPrivacy.PUBLIC, |
57 | commentsEnabled: true, | 57 | commentsEnabled: true, |
58 | downloadEnabled: true, | ||
58 | channel: { | 59 | channel: { |
59 | displayName: 'Main root channel', | 60 | displayName: 'Main root channel', |
60 | name: 'root_channel', | 61 | name: 'root_channel', |
@@ -87,6 +88,7 @@ describe('Test a single server', function () { | |||
87 | privacy: VideoPrivacy.PUBLIC, | 88 | privacy: VideoPrivacy.PUBLIC, |
88 | duration: 5, | 89 | duration: 5, |
89 | commentsEnabled: false, | 90 | commentsEnabled: false, |
91 | downloadEnabled: false, | ||
90 | channel: { | 92 | channel: { |
91 | name: 'root_channel', | 93 | name: 'root_channel', |
92 | displayName: 'Main root channel', | 94 | displayName: 'Main root channel', |
@@ -356,6 +358,7 @@ describe('Test a single server', function () { | |||
356 | nsfw: false, | 358 | nsfw: false, |
357 | description: 'my super description updated', | 359 | description: 'my super description updated', |
358 | commentsEnabled: false, | 360 | commentsEnabled: false, |
361 | downloadEnabled: false, | ||
359 | tags: [ 'tagup1', 'tagup2' ] | 362 | tags: [ 'tagup1', 'tagup2' ] |
360 | } | 363 | } |
361 | await updateVideo(server.url, server.accessToken, videoId, attributes) | 364 | await updateVideo(server.url, server.accessToken, videoId, attributes) |
diff --git a/server/tests/api/videos/video-blacklist-management.ts b/server/tests/api/videos/video-blacklist-management.ts deleted file mode 100644 index 61411e30d..000000000 --- a/server/tests/api/videos/video-blacklist-management.ts +++ /dev/null | |||
@@ -1,192 +0,0 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import { orderBy } from 'lodash' | ||
5 | import 'mocha' | ||
6 | import { | ||
7 | addVideoToBlacklist, | ||
8 | flushAndRunMultipleServers, | ||
9 | getBlacklistedVideosList, | ||
10 | getMyVideos, | ||
11 | getSortedBlacklistedVideosList, | ||
12 | getVideosList, | ||
13 | killallServers, | ||
14 | removeVideoFromBlacklist, | ||
15 | ServerInfo, | ||
16 | setAccessTokensToServers, | ||
17 | updateVideoBlacklist, | ||
18 | uploadVideo | ||
19 | } from '../../../../shared/utils/index' | ||
20 | import { doubleFollow } from '../../../../shared/utils/server/follows' | ||
21 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
22 | import { VideoAbuse } from '../../../../shared/models/videos' | ||
23 | |||
24 | const expect = chai.expect | ||
25 | |||
26 | describe('Test video blacklist management', function () { | ||
27 | let servers: ServerInfo[] = [] | ||
28 | let videoId: number | ||
29 | |||
30 | async function blacklistVideosOnServer (server: ServerInfo) { | ||
31 | const res = await getVideosList(server.url) | ||
32 | |||
33 | const videos = res.body.data | ||
34 | for (let video of videos) { | ||
35 | await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason') | ||
36 | } | ||
37 | } | ||
38 | |||
39 | before(async function () { | ||
40 | this.timeout(50000) | ||
41 | |||
42 | // Run servers | ||
43 | servers = await flushAndRunMultipleServers(2) | ||
44 | |||
45 | // Get the access tokens | ||
46 | await setAccessTokensToServers(servers) | ||
47 | |||
48 | // Server 1 and server 2 follow each other | ||
49 | await doubleFollow(servers[0], servers[1]) | ||
50 | |||
51 | // Upload 2 videos on server 2 | ||
52 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 1st video', description: 'A video on server 2' }) | ||
53 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 2nd video', description: 'A video on server 2' }) | ||
54 | |||
55 | // Wait videos propagation, server 2 has transcoding enabled | ||
56 | await waitJobs(servers) | ||
57 | |||
58 | // Blacklist the two videos on server 1 | ||
59 | await blacklistVideosOnServer(servers[0]) | ||
60 | }) | ||
61 | |||
62 | describe('When listing blacklisted videos', function () { | ||
63 | it('Should display all the blacklisted videos', async function () { | ||
64 | const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) | ||
65 | |||
66 | expect(res.body.total).to.equal(2) | ||
67 | |||
68 | const blacklistedVideos = res.body.data | ||
69 | expect(blacklistedVideos).to.be.an('array') | ||
70 | expect(blacklistedVideos.length).to.equal(2) | ||
71 | |||
72 | for (const blacklistedVideo of blacklistedVideos) { | ||
73 | expect(blacklistedVideo.reason).to.equal('super reason') | ||
74 | videoId = blacklistedVideo.video.id | ||
75 | } | ||
76 | }) | ||
77 | |||
78 | it('Should get the correct sort when sorting by descending id', async function () { | ||
79 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') | ||
80 | expect(res.body.total).to.equal(2) | ||
81 | |||
82 | const blacklistedVideos = res.body.data | ||
83 | expect(blacklistedVideos).to.be.an('array') | ||
84 | expect(blacklistedVideos.length).to.equal(2) | ||
85 | |||
86 | const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ]) | ||
87 | |||
88 | expect(blacklistedVideos).to.deep.equal(result) | ||
89 | }) | ||
90 | |||
91 | it('Should get the correct sort when sorting by descending video name', async function () { | ||
92 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
93 | expect(res.body.total).to.equal(2) | ||
94 | |||
95 | const blacklistedVideos = res.body.data | ||
96 | expect(blacklistedVideos).to.be.an('array') | ||
97 | expect(blacklistedVideos.length).to.equal(2) | ||
98 | |||
99 | const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ]) | ||
100 | |||
101 | expect(blacklistedVideos).to.deep.equal(result) | ||
102 | }) | ||
103 | |||
104 | it('Should get the correct sort when sorting by ascending creation date', async function () { | ||
105 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') | ||
106 | expect(res.body.total).to.equal(2) | ||
107 | |||
108 | const blacklistedVideos = res.body.data | ||
109 | expect(blacklistedVideos).to.be.an('array') | ||
110 | expect(blacklistedVideos.length).to.equal(2) | ||
111 | |||
112 | const result = orderBy(res.body.data, [ 'createdAt' ]) | ||
113 | |||
114 | expect(blacklistedVideos).to.deep.equal(result) | ||
115 | }) | ||
116 | }) | ||
117 | |||
118 | describe('When updating blacklisted videos', function () { | ||
119 | it('Should change the reason', async function () { | ||
120 | await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated') | ||
121 | |||
122 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
123 | const video = res.body.data.find(b => b.video.id === videoId) | ||
124 | |||
125 | expect(video.reason).to.equal('my super reason updated') | ||
126 | }) | ||
127 | }) | ||
128 | |||
129 | describe('When listing my videos', function () { | ||
130 | it('Should display blacklisted videos', async function () { | ||
131 | await blacklistVideosOnServer(servers[1]) | ||
132 | |||
133 | const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5) | ||
134 | |||
135 | expect(res.body.total).to.equal(2) | ||
136 | expect(res.body.data).to.have.lengthOf(2) | ||
137 | |||
138 | for (const video of res.body.data) { | ||
139 | expect(video.blacklisted).to.be.true | ||
140 | expect(video.blacklistedReason).to.equal('super reason') | ||
141 | } | ||
142 | }) | ||
143 | }) | ||
144 | |||
145 | describe('When removing a blacklisted video', function () { | ||
146 | let videoToRemove: VideoAbuse | ||
147 | let blacklist = [] | ||
148 | |||
149 | it('Should not have any video in videos list on server 1', async function () { | ||
150 | const res = await getVideosList(servers[0].url) | ||
151 | expect(res.body.total).to.equal(0) | ||
152 | expect(res.body.data).to.be.an('array') | ||
153 | expect(res.body.data.length).to.equal(0) | ||
154 | }) | ||
155 | |||
156 | it('Should remove a video from the blacklist on server 1', async function () { | ||
157 | // Get one video in the blacklist | ||
158 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
159 | videoToRemove = res.body.data[0] | ||
160 | blacklist = res.body.data.slice(1) | ||
161 | |||
162 | // Remove it | ||
163 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id) | ||
164 | }) | ||
165 | |||
166 | it('Should have the ex-blacklisted video in videos list on server 1', async function () { | ||
167 | const res = await getVideosList(servers[0].url) | ||
168 | expect(res.body.total).to.equal(1) | ||
169 | |||
170 | const videos = res.body.data | ||
171 | expect(videos).to.be.an('array') | ||
172 | expect(videos.length).to.equal(1) | ||
173 | |||
174 | expect(videos[0].name).to.equal(videoToRemove.video.name) | ||
175 | expect(videos[0].id).to.equal(videoToRemove.video.id) | ||
176 | }) | ||
177 | |||
178 | it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { | ||
179 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
180 | expect(res.body.total).to.equal(1) | ||
181 | |||
182 | const videos = res.body.data | ||
183 | expect(videos).to.be.an('array') | ||
184 | expect(videos.length).to.equal(1) | ||
185 | expect(videos).to.deep.equal(blacklist) | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | after(async function () { | ||
190 | killallServers(servers) | ||
191 | }) | ||
192 | }) | ||
diff --git a/server/tests/api/videos/video-blacklist.ts b/server/tests/api/videos/video-blacklist.ts index 1cce82d2a..d39ad63b4 100644 --- a/server/tests/api/videos/video-blacklist.ts +++ b/server/tests/api/videos/video-blacklist.ts | |||
@@ -1,24 +1,43 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | 1 | /* tslint:disable:no-unused-expression */ |
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import { orderBy } from 'lodash' | ||
4 | import 'mocha' | 5 | import 'mocha' |
5 | import { | 6 | import { |
6 | addVideoToBlacklist, | 7 | addVideoToBlacklist, |
7 | flushAndRunMultipleServers, | 8 | flushAndRunMultipleServers, |
9 | getBlacklistedVideosList, | ||
10 | getMyVideos, | ||
11 | getSortedBlacklistedVideosList, | ||
8 | getVideosList, | 12 | getVideosList, |
9 | killallServers, | 13 | killallServers, |
14 | removeVideoFromBlacklist, | ||
10 | searchVideo, | 15 | searchVideo, |
11 | ServerInfo, | 16 | ServerInfo, |
12 | setAccessTokensToServers, | 17 | setAccessTokensToServers, |
13 | uploadVideo | 18 | updateVideo, |
19 | updateVideoBlacklist, | ||
20 | uploadVideo, | ||
21 | viewVideo | ||
14 | } from '../../../../shared/utils/index' | 22 | } from '../../../../shared/utils/index' |
15 | import { doubleFollow } from '../../../../shared/utils/server/follows' | 23 | import { doubleFollow } from '../../../../shared/utils/server/follows' |
16 | import { waitJobs } from '../../../../shared/utils/server/jobs' | 24 | import { waitJobs } from '../../../../shared/utils/server/jobs' |
25 | import { VideoBlacklist } from '../../../../shared/models/videos' | ||
17 | 26 | ||
18 | const expect = chai.expect | 27 | const expect = chai.expect |
19 | 28 | ||
20 | describe('Test video blacklists', function () { | 29 | describe('Test video blacklist management', function () { |
21 | let servers: ServerInfo[] = [] | 30 | let servers: ServerInfo[] = [] |
31 | let videoId: number | ||
32 | |||
33 | async function blacklistVideosOnServer (server: ServerInfo) { | ||
34 | const res = await getVideosList(server.url) | ||
35 | |||
36 | const videos = res.body.data | ||
37 | for (let video of videos) { | ||
38 | await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason') | ||
39 | } | ||
40 | } | ||
22 | 41 | ||
23 | before(async function () { | 42 | before(async function () { |
24 | this.timeout(50000) | 43 | this.timeout(50000) |
@@ -32,58 +51,270 @@ describe('Test video blacklists', function () { | |||
32 | // Server 1 and server 2 follow each other | 51 | // Server 1 and server 2 follow each other |
33 | await doubleFollow(servers[0], servers[1]) | 52 | await doubleFollow(servers[0], servers[1]) |
34 | 53 | ||
35 | // Upload a video on server 2 | 54 | // Upload 2 videos on server 2 |
36 | const videoAttributes = { | 55 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 1st video', description: 'A video on server 2' }) |
37 | name: 'my super name for server 2', | 56 | await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'My 2nd video', description: 'A video on server 2' }) |
38 | description: 'my super description for server 2' | ||
39 | } | ||
40 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes) | ||
41 | 57 | ||
42 | // Wait videos propagation, server 2 has transcoding enabled | 58 | // Wait videos propagation, server 2 has transcoding enabled |
43 | await waitJobs(servers) | 59 | await waitJobs(servers) |
44 | 60 | ||
45 | const res = await getVideosList(servers[0].url) | 61 | // Blacklist the two videos on server 1 |
46 | const videos = res.body.data | 62 | await blacklistVideosOnServer(servers[0]) |
63 | }) | ||
64 | |||
65 | describe('When listing/searching videos', function () { | ||
47 | 66 | ||
48 | expect(videos.length).to.equal(1) | 67 | it('Should not have the video blacklisted in videos list/search on server 1', async function () { |
68 | { | ||
69 | const res = await getVideosList(servers[ 0 ].url) | ||
49 | 70 | ||
50 | servers[0].remoteVideo = videos.find(video => video.name === 'my super name for server 2') | 71 | expect(res.body.total).to.equal(0) |
72 | expect(res.body.data).to.be.an('array') | ||
73 | expect(res.body.data.length).to.equal(0) | ||
74 | } | ||
75 | |||
76 | { | ||
77 | const res = await searchVideo(servers[ 0 ].url, 'name') | ||
78 | |||
79 | expect(res.body.total).to.equal(0) | ||
80 | expect(res.body.data).to.be.an('array') | ||
81 | expect(res.body.data.length).to.equal(0) | ||
82 | } | ||
83 | }) | ||
84 | |||
85 | it('Should have the blacklisted video in videos list/search on server 2', async function () { | ||
86 | { | ||
87 | const res = await getVideosList(servers[ 1 ].url) | ||
88 | |||
89 | expect(res.body.total).to.equal(2) | ||
90 | expect(res.body.data).to.be.an('array') | ||
91 | expect(res.body.data.length).to.equal(2) | ||
92 | } | ||
93 | |||
94 | { | ||
95 | const res = await searchVideo(servers[ 1 ].url, 'video') | ||
96 | |||
97 | expect(res.body.total).to.equal(2) | ||
98 | expect(res.body.data).to.be.an('array') | ||
99 | expect(res.body.data.length).to.equal(2) | ||
100 | } | ||
101 | }) | ||
51 | }) | 102 | }) |
52 | 103 | ||
53 | it('Should blacklist a remote video on server 1', async function () { | 104 | describe('When listing blacklisted videos', function () { |
54 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, servers[0].remoteVideo.id) | 105 | it('Should display all the blacklisted videos', async function () { |
106 | const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) | ||
107 | |||
108 | expect(res.body.total).to.equal(2) | ||
109 | |||
110 | const blacklistedVideos = res.body.data | ||
111 | expect(blacklistedVideos).to.be.an('array') | ||
112 | expect(blacklistedVideos.length).to.equal(2) | ||
113 | |||
114 | for (const blacklistedVideo of blacklistedVideos) { | ||
115 | expect(blacklistedVideo.reason).to.equal('super reason') | ||
116 | videoId = blacklistedVideo.video.id | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | it('Should get the correct sort when sorting by descending id', async function () { | ||
121 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-id') | ||
122 | expect(res.body.total).to.equal(2) | ||
123 | |||
124 | const blacklistedVideos = res.body.data | ||
125 | expect(blacklistedVideos).to.be.an('array') | ||
126 | expect(blacklistedVideos.length).to.equal(2) | ||
127 | |||
128 | const result = orderBy(res.body.data, [ 'id' ], [ 'desc' ]) | ||
129 | |||
130 | expect(blacklistedVideos).to.deep.equal(result) | ||
131 | }) | ||
132 | |||
133 | it('Should get the correct sort when sorting by descending video name', async function () { | ||
134 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
135 | expect(res.body.total).to.equal(2) | ||
136 | |||
137 | const blacklistedVideos = res.body.data | ||
138 | expect(blacklistedVideos).to.be.an('array') | ||
139 | expect(blacklistedVideos.length).to.equal(2) | ||
140 | |||
141 | const result = orderBy(res.body.data, [ 'name' ], [ 'desc' ]) | ||
142 | |||
143 | expect(blacklistedVideos).to.deep.equal(result) | ||
144 | }) | ||
145 | |||
146 | it('Should get the correct sort when sorting by ascending creation date', async function () { | ||
147 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') | ||
148 | expect(res.body.total).to.equal(2) | ||
149 | |||
150 | const blacklistedVideos = res.body.data | ||
151 | expect(blacklistedVideos).to.be.an('array') | ||
152 | expect(blacklistedVideos.length).to.equal(2) | ||
153 | |||
154 | const result = orderBy(res.body.data, [ 'createdAt' ]) | ||
155 | |||
156 | expect(blacklistedVideos).to.deep.equal(result) | ||
157 | }) | ||
55 | }) | 158 | }) |
56 | 159 | ||
57 | it('Should not have the video blacklisted in videos list on server 1', async function () { | 160 | describe('When updating blacklisted videos', function () { |
58 | const res = await getVideosList(servers[0].url) | 161 | it('Should change the reason', async function () { |
162 | await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated') | ||
163 | |||
164 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
165 | const video = res.body.data.find(b => b.video.id === videoId) | ||
59 | 166 | ||
60 | expect(res.body.total).to.equal(0) | 167 | expect(video.reason).to.equal('my super reason updated') |
61 | expect(res.body.data).to.be.an('array') | 168 | }) |
62 | expect(res.body.data.length).to.equal(0) | ||
63 | }) | 169 | }) |
64 | 170 | ||
65 | it('Should not have the video blacklisted in videos search on server 1', async function () { | 171 | describe('When listing my videos', function () { |
66 | const res = await searchVideo(servers[0].url, 'name') | 172 | it('Should display blacklisted videos', async function () { |
173 | await blacklistVideosOnServer(servers[1]) | ||
174 | |||
175 | const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 5) | ||
67 | 176 | ||
68 | expect(res.body.total).to.equal(0) | 177 | expect(res.body.total).to.equal(2) |
69 | expect(res.body.data).to.be.an('array') | 178 | expect(res.body.data).to.have.lengthOf(2) |
70 | expect(res.body.data.length).to.equal(0) | 179 | |
180 | for (const video of res.body.data) { | ||
181 | expect(video.blacklisted).to.be.true | ||
182 | expect(video.blacklistedReason).to.equal('super reason') | ||
183 | } | ||
184 | }) | ||
71 | }) | 185 | }) |
72 | 186 | ||
73 | it('Should have the blacklisted video in videos list on server 2', async function () { | 187 | describe('When removing a blacklisted video', function () { |
74 | const res = await getVideosList(servers[1].url) | 188 | let videoToRemove: VideoBlacklist |
189 | let blacklist = [] | ||
190 | |||
191 | it('Should not have any video in videos list on server 1', async function () { | ||
192 | const res = await getVideosList(servers[0].url) | ||
193 | expect(res.body.total).to.equal(0) | ||
194 | expect(res.body.data).to.be.an('array') | ||
195 | expect(res.body.data.length).to.equal(0) | ||
196 | }) | ||
197 | |||
198 | it('Should remove a video from the blacklist on server 1', async function () { | ||
199 | // Get one video in the blacklist | ||
200 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
201 | videoToRemove = res.body.data[0] | ||
202 | blacklist = res.body.data.slice(1) | ||
203 | |||
204 | // Remove it | ||
205 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoToRemove.video.id) | ||
206 | }) | ||
207 | |||
208 | it('Should have the ex-blacklisted video in videos list on server 1', async function () { | ||
209 | const res = await getVideosList(servers[0].url) | ||
210 | expect(res.body.total).to.equal(1) | ||
211 | |||
212 | const videos = res.body.data | ||
213 | expect(videos).to.be.an('array') | ||
214 | expect(videos.length).to.equal(1) | ||
215 | |||
216 | expect(videos[0].name).to.equal(videoToRemove.video.name) | ||
217 | expect(videos[0].id).to.equal(videoToRemove.video.id) | ||
218 | }) | ||
219 | |||
220 | it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { | ||
221 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') | ||
222 | expect(res.body.total).to.equal(1) | ||
75 | 223 | ||
76 | expect(res.body.total).to.equal(1) | 224 | const videos = res.body.data |
77 | expect(res.body.data).to.be.an('array') | 225 | expect(videos).to.be.an('array') |
78 | expect(res.body.data.length).to.equal(1) | 226 | expect(videos.length).to.equal(1) |
227 | expect(videos).to.deep.equal(blacklist) | ||
228 | }) | ||
79 | }) | 229 | }) |
80 | 230 | ||
81 | it('Should have the video blacklisted in videos search on server 2', async function () { | 231 | describe('When blacklisting local videos', function () { |
82 | const res = await searchVideo(servers[1].url, 'name') | 232 | let video3UUID: string |
233 | let video4UUID: string | ||
234 | |||
235 | before(async function () { | ||
236 | this.timeout(10000) | ||
237 | |||
238 | { | ||
239 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'Video 3' }) | ||
240 | video3UUID = res.body.video.uuid | ||
241 | } | ||
242 | { | ||
243 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'Video 4' }) | ||
244 | video4UUID = res.body.video.uuid | ||
245 | } | ||
246 | |||
247 | await waitJobs(servers) | ||
248 | }) | ||
249 | |||
250 | it('Should blacklist video 3 and keep it federated', async function () { | ||
251 | this.timeout(10000) | ||
252 | |||
253 | await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video3UUID, 'super reason', false) | ||
254 | |||
255 | await waitJobs(servers) | ||
256 | |||
257 | { | ||
258 | const res = await getVideosList(servers[ 0 ].url) | ||
259 | expect(res.body.data.find(v => v.uuid === video3UUID)).to.be.undefined | ||
260 | } | ||
261 | |||
262 | { | ||
263 | const res = await getVideosList(servers[ 1 ].url) | ||
264 | expect(res.body.data.find(v => v.uuid === video3UUID)).to.not.be.undefined | ||
265 | } | ||
266 | }) | ||
267 | |||
268 | it('Should unfederate the video', async function () { | ||
269 | this.timeout(10000) | ||
270 | |||
271 | await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID, 'super reason', true) | ||
272 | |||
273 | await waitJobs(servers) | ||
274 | |||
275 | for (const server of servers) { | ||
276 | const res = await getVideosList(server.url) | ||
277 | expect(res.body.data.find(v => v.uuid === video4UUID)).to.be.undefined | ||
278 | } | ||
279 | }) | ||
280 | |||
281 | it('Should have the video unfederated even after an Update AP message', async function () { | ||
282 | this.timeout(10000) | ||
283 | |||
284 | await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID, { description: 'super description' }) | ||
285 | |||
286 | await waitJobs(servers) | ||
287 | |||
288 | for (const server of servers) { | ||
289 | const res = await getVideosList(server.url) | ||
290 | expect(res.body.data.find(v => v.uuid === video4UUID)).to.be.undefined | ||
291 | } | ||
292 | }) | ||
293 | |||
294 | it('Should have the correct video blacklist unfederate attribute', async function () { | ||
295 | const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') | ||
296 | |||
297 | const blacklistedVideos: VideoBlacklist[] = res.body.data | ||
298 | const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID) | ||
299 | const video4Blacklisted = blacklistedVideos.find(b => b.video.uuid === video4UUID) | ||
300 | |||
301 | expect(video3Blacklisted.unfederated).to.be.false | ||
302 | expect(video4Blacklisted.unfederated).to.be.true | ||
303 | }) | ||
304 | |||
305 | it('Should remove the video from blacklist and refederate the video', async function () { | ||
306 | this.timeout(10000) | ||
307 | |||
308 | await removeVideoFromBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID) | ||
309 | |||
310 | await waitJobs(servers) | ||
311 | |||
312 | for (const server of servers) { | ||
313 | const res = await getVideosList(server.url) | ||
314 | expect(res.body.data.find(v => v.uuid === video4UUID)).to.not.be.undefined | ||
315 | } | ||
316 | }) | ||
83 | 317 | ||
84 | expect(res.body.total).to.equal(1) | ||
85 | expect(res.body.data).to.be.an('array') | ||
86 | expect(res.body.data.length).to.equal(1) | ||
87 | }) | 318 | }) |
88 | 319 | ||
89 | after(async function () { | 320 | after(async function () { |
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts new file mode 100644 index 000000000..a1214bad1 --- /dev/null +++ b/server/tests/api/videos/video-hls.ts | |||
@@ -0,0 +1,139 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | checkDirectoryIsEmpty, | ||
7 | checkSegmentHash, | ||
8 | checkTmpIsEmpty, | ||
9 | doubleFollow, | ||
10 | flushAndRunMultipleServers, | ||
11 | flushTests, | ||
12 | getPlaylist, | ||
13 | getVideo, | ||
14 | killallServers, | ||
15 | removeVideo, | ||
16 | ServerInfo, | ||
17 | setAccessTokensToServers, | ||
18 | updateVideo, | ||
19 | uploadVideo, | ||
20 | waitJobs | ||
21 | } from '../../../../shared/utils' | ||
22 | import { VideoDetails } from '../../../../shared/models/videos' | ||
23 | import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' | ||
24 | import { join } from 'path' | ||
25 | |||
26 | const expect = chai.expect | ||
27 | |||
28 | async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { | ||
29 | const resolutions = [ 240, 360, 480, 720 ] | ||
30 | |||
31 | for (const server of servers) { | ||
32 | const res = await getVideo(server.url, videoUUID) | ||
33 | const videoDetails: VideoDetails = res.body | ||
34 | |||
35 | expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) | ||
36 | |||
37 | const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
38 | expect(hlsPlaylist).to.not.be.undefined | ||
39 | |||
40 | { | ||
41 | const res2 = await getPlaylist(hlsPlaylist.playlistUrl) | ||
42 | |||
43 | const masterPlaylist = res2.text | ||
44 | |||
45 | expect(masterPlaylist).to.contain('#EXT-X-STREAM-INF:BANDWIDTH=55472,RESOLUTION=640x360,FRAME-RATE=25') | ||
46 | |||
47 | for (const resolution of resolutions) { | ||
48 | expect(masterPlaylist).to.contain(`${resolution}.m3u8`) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | { | ||
53 | for (const resolution of resolutions) { | ||
54 | const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`) | ||
55 | |||
56 | const subPlaylist = res2.text | ||
57 | expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) | ||
58 | } | ||
59 | } | ||
60 | |||
61 | { | ||
62 | const baseUrl = 'http://localhost:9001/static/playlists/hls' | ||
63 | |||
64 | for (const resolution of resolutions) { | ||
65 | await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist) | ||
66 | } | ||
67 | } | ||
68 | } | ||
69 | } | ||
70 | |||
71 | describe('Test HLS videos', function () { | ||
72 | let servers: ServerInfo[] = [] | ||
73 | let videoUUID = '' | ||
74 | |||
75 | before(async function () { | ||
76 | this.timeout(120000) | ||
77 | |||
78 | servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: true, hls: { enabled: true } } }) | ||
79 | |||
80 | // Get the access tokens | ||
81 | await setAccessTokensToServers(servers) | ||
82 | |||
83 | // Server 1 and server 2 follow each other | ||
84 | await doubleFollow(servers[0], servers[1]) | ||
85 | }) | ||
86 | |||
87 | it('Should upload a video and transcode it to HLS', async function () { | ||
88 | this.timeout(120000) | ||
89 | |||
90 | { | ||
91 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) | ||
92 | videoUUID = res.body.video.uuid | ||
93 | } | ||
94 | |||
95 | await waitJobs(servers) | ||
96 | |||
97 | await checkHlsPlaylist(servers, videoUUID) | ||
98 | }) | ||
99 | |||
100 | it('Should update the video', async function () { | ||
101 | await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' }) | ||
102 | |||
103 | await waitJobs(servers) | ||
104 | |||
105 | await checkHlsPlaylist(servers, videoUUID) | ||
106 | }) | ||
107 | |||
108 | it('Should delete the video', async function () { | ||
109 | await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) | ||
110 | |||
111 | await waitJobs(servers) | ||
112 | |||
113 | for (const server of servers) { | ||
114 | await getVideo(server.url, videoUUID, 404) | ||
115 | } | ||
116 | }) | ||
117 | |||
118 | it('Should have the playlists/segment deleted from the disk', async function () { | ||
119 | for (const server of servers) { | ||
120 | await checkDirectoryIsEmpty(server, 'videos') | ||
121 | await checkDirectoryIsEmpty(server, join('playlists', 'hls')) | ||
122 | } | ||
123 | }) | ||
124 | |||
125 | it('Should have an empty tmp directory', async function () { | ||
126 | for (const server of servers) { | ||
127 | await checkTmpIsEmpty(server) | ||
128 | } | ||
129 | }) | ||
130 | |||
131 | after(async function () { | ||
132 | killallServers(servers) | ||
133 | |||
134 | // Keep the logs if the test failed | ||
135 | if (this['ok']) { | ||
136 | await flushTests() | ||
137 | } | ||
138 | }) | ||
139 | }) | ||
diff --git a/server/tests/cli/update-host.ts b/server/tests/cli/update-host.ts index 811ea6a9f..d38bb4331 100644 --- a/server/tests/cli/update-host.ts +++ b/server/tests/cli/update-host.ts | |||
@@ -86,6 +86,13 @@ describe('Test update host scripts', function () { | |||
86 | const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) | 86 | const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) |
87 | 87 | ||
88 | expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid) | 88 | expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid) |
89 | |||
90 | const res = await getVideo(server.url, video.uuid) | ||
91 | const videoDetails: VideoDetails = res.body | ||
92 | |||
93 | expect(videoDetails.trackerUrls[0]).to.include(server.host) | ||
94 | expect(videoDetails.streamingPlaylists[0].playlistUrl).to.include(server.host) | ||
95 | expect(videoDetails.streamingPlaylists[0].segmentsSha256Url).to.include(server.host) | ||
89 | } | 96 | } |
90 | }) | 97 | }) |
91 | 98 | ||
@@ -100,7 +107,7 @@ describe('Test update host scripts', function () { | |||
100 | } | 107 | } |
101 | }) | 108 | }) |
102 | 109 | ||
103 | it('Should have update accounts url', async function () { | 110 | it('Should have updated accounts url', async function () { |
104 | const res = await getAccountsList(server.url) | 111 | const res = await getAccountsList(server.url) |
105 | expect(res.body.total).to.equal(3) | 112 | expect(res.body.total).to.equal(3) |
106 | 113 | ||
@@ -112,7 +119,7 @@ describe('Test update host scripts', function () { | |||
112 | } | 119 | } |
113 | }) | 120 | }) |
114 | 121 | ||
115 | it('Should update torrent hosts', async function () { | 122 | it('Should have updated torrent hosts', async function () { |
116 | this.timeout(30000) | 123 | this.timeout(30000) |
117 | 124 | ||
118 | const res = await getVideosList(server.url) | 125 | const res = await getVideosList(server.url) |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 2874a2131..151c5a989 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -78,7 +78,11 @@ getSettings() | |||
78 | password: program['password'] | 78 | password: program['password'] |
79 | } | 79 | } |
80 | 80 | ||
81 | run(user, program['url']).catch(err => console.error(err)) | 81 | run(user, program['url']) |
82 | .catch(err => { | ||
83 | console.error(err) | ||
84 | process.exit(-1) | ||
85 | }) | ||
82 | }) | 86 | }) |
83 | 87 | ||
84 | async function promptPassword () { | 88 | async function promptPassword () { |
@@ -112,8 +116,12 @@ async function run (user, url: string) { | |||
112 | secret: res.body.client_secret | 116 | secret: res.body.client_secret |
113 | } | 117 | } |
114 | 118 | ||
115 | const res2 = await login(url, client, user) | 119 | try { |
116 | accessToken = res2.body.access_token | 120 | const res = await login(program[ 'url' ], client, user) |
121 | accessToken = res.body.access_token | ||
122 | } catch (err) { | ||
123 | throw new Error('Cannot authenticate. Please check your username/password.') | ||
124 | } | ||
117 | 125 | ||
118 | const youtubeDL = await safeGetYoutubeDL() | 126 | const youtubeDL = await safeGetYoutubeDL() |
119 | 127 | ||
@@ -216,6 +224,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st | |||
216 | nsfw: isNSFW(videoInfo), | 224 | nsfw: isNSFW(videoInfo), |
217 | waitTranscoding: true, | 225 | waitTranscoding: true, |
218 | commentsEnabled: true, | 226 | commentsEnabled: true, |
227 | downloadEnabled: true, | ||
219 | description: videoInfo.description || undefined, | 228 | description: videoInfo.description || undefined, |
220 | support: undefined, | 229 | support: undefined, |
221 | tags, | 230 | tags, |
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts index cc7bd9b4c..ebc62c965 100644 --- a/server/tools/peertube-upload.ts +++ b/server/tools/peertube-upload.ts | |||
@@ -30,6 +30,7 @@ if (!program['tags']) program['tags'] = [] | |||
30 | if (!program['nsfw']) program['nsfw'] = false | 30 | if (!program['nsfw']) program['nsfw'] = false |
31 | if (!program['privacy']) program['privacy'] = VideoPrivacy.PUBLIC | 31 | if (!program['privacy']) program['privacy'] = VideoPrivacy.PUBLIC |
32 | if (!program['commentsEnabled']) program['commentsEnabled'] = false | 32 | if (!program['commentsEnabled']) program['commentsEnabled'] = false |
33 | if (!program['downloadEnabled']) program['downloadEnabled'] = true | ||
33 | 34 | ||
34 | getSettings() | 35 | getSettings() |
35 | .then(settings => { | 36 | .then(settings => { |
@@ -116,6 +117,7 @@ async function run () { | |||
116 | description: program['videoDescription'], | 117 | description: program['videoDescription'], |
117 | tags: program['tags'], | 118 | tags: program['tags'], |
118 | commentsEnabled: program['commentsEnabled'], | 119 | commentsEnabled: program['commentsEnabled'], |
120 | downloadEnabled: program['downloadEnabled'], | ||
119 | fixture: program['file'], | 121 | fixture: program['file'], |
120 | thumbnailfile: program['thumbnail'], | 122 | thumbnailfile: program['thumbnail'], |
121 | previewfile: program['preview'], | 123 | previewfile: program['preview'], |