aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/assets/default-audio-background.jpgbin0 -> 14048 bytes
-rw-r--r--server/controllers/api/accounts.ts16
-rw-r--r--server/controllers/api/config.ts6
-rw-r--r--server/controllers/api/users/index.ts37
-rw-r--r--server/controllers/api/users/me.ts16
-rw-r--r--server/controllers/api/video-channel.ts37
-rw-r--r--server/controllers/api/video-playlist.ts6
-rw-r--r--server/controllers/api/videos/index.ts54
-rw-r--r--server/controllers/static.ts2
-rw-r--r--server/helpers/core-utils.ts5
-rw-r--r--server/helpers/custom-validators/accounts.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/video-comments.ts3
-rw-r--r--server/helpers/custom-validators/video-channels.ts9
-rw-r--r--server/helpers/express-utils.ts13
-rw-r--r--server/helpers/ffmpeg-utils.ts180
-rw-r--r--server/helpers/logger.ts27
-rw-r--r--server/initializers/checker-before-init.ts1
-rw-r--r--server/initializers/config.ts15
-rw-r--r--server/initializers/constants.ts70
-rw-r--r--server/initializers/installer.ts4
-rw-r--r--server/initializers/migrations/0100-activitypub.ts9
-rw-r--r--server/initializers/migrations/0385-remove-actor-uuid.ts19
-rw-r--r--server/initializers/migrations/0390-user-pending-email.ts25
-rw-r--r--server/lib/activitypub/actor.ts33
-rw-r--r--server/lib/activitypub/crawl.ts19
-rw-r--r--server/lib/activitypub/process/process-announce.ts15
-rw-r--r--server/lib/activitypub/process/process-create.ts29
-rw-r--r--server/lib/activitypub/process/process-delete.ts8
-rw-r--r--server/lib/activitypub/process/process-update.ts4
-rw-r--r--server/lib/activitypub/video-comments.ts4
-rw-r--r--server/lib/emailer.ts66
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts2
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts4
-rw-r--r--server/lib/job-queue/handlers/video-import.ts1
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts56
-rw-r--r--server/lib/thumbnail.ts8
-rw-r--r--server/lib/user.ts91
-rw-r--r--server/lib/video-channel.ts16
-rw-r--r--server/lib/video-transcoding.ts97
-rw-r--r--server/middlewares/validators/feeds.ts13
-rw-r--r--server/middlewares/validators/users.ts75
-rw-r--r--server/middlewares/validators/videos/video-channels.ts16
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts15
-rw-r--r--server/middlewares/validators/videos/videos.ts8
-rw-r--r--server/models/account/account.ts19
-rw-r--r--server/models/account/user.ts6
-rw-r--r--server/models/activitypub/actor.ts14
-rw-r--r--server/models/video/thumbnail.ts8
-rw-r--r--server/models/video/video-channel.ts34
-rw-r--r--server/models/video/video-file.ts5
-rw-r--r--server/models/video/video.ts23
-rw-r--r--server/tests/api/activitypub/client.ts11
-rw-r--r--server/tests/api/activitypub/fetch.ts17
-rw-r--r--server/tests/api/activitypub/refresher.ts46
-rw-r--r--server/tests/api/activitypub/security.ts106
-rw-r--r--server/tests/api/check-params/config.ts4
-rw-r--r--server/tests/api/check-params/users.ts45
-rw-r--r--server/tests/api/check-params/video-channels.ts36
-rw-r--r--server/tests/api/check-params/video-playlists.ts12
-rw-r--r--server/tests/api/index-1.ts3
-rw-r--r--server/tests/api/index-2.ts2
-rw-r--r--server/tests/api/index-3.ts1
-rw-r--r--server/tests/api/index-4.ts2
-rw-r--r--server/tests/api/index.ts12
-rw-r--r--server/tests/api/notifications/index.ts2
-rw-r--r--server/tests/api/notifications/user-notifications.ts41
-rw-r--r--server/tests/api/redundancy/redundancy.ts34
-rw-r--r--server/tests/api/search/search-activitypub-video-channels.ts39
-rw-r--r--server/tests/api/search/search-activitypub-videos.ts31
-rw-r--r--server/tests/api/search/search-videos.ts10
-rw-r--r--server/tests/api/server/config.ts38
-rw-r--r--server/tests/api/server/contact-form.ts7
-rw-r--r--server/tests/api/server/email.ts38
-rw-r--r--server/tests/api/server/follow-constraints.ts46
-rw-r--r--server/tests/api/server/follows-moderation.ts20
-rw-r--r--server/tests/api/server/follows.ts61
-rw-r--r--server/tests/api/server/handle-down.ts76
-rw-r--r--server/tests/api/server/jobs.ts2
-rw-r--r--server/tests/api/server/logs.ts2
-rw-r--r--server/tests/api/travis-1.sh10
-rw-r--r--server/tests/api/travis-2.sh9
-rw-r--r--server/tests/api/travis-3.sh8
-rw-r--r--server/tests/api/travis-4.sh9
-rw-r--r--server/tests/api/users/blocklist.ts28
-rw-r--r--server/tests/api/users/user-subscriptions.ts34
-rw-r--r--server/tests/api/users/users-multiple-servers.ts54
-rw-r--r--server/tests/api/users/users-verification.ts65
-rw-r--r--server/tests/api/users/users.ts30
-rw-r--r--server/tests/api/videos/multiple-servers.ts45
-rw-r--r--server/tests/api/videos/services.ts14
-rw-r--r--server/tests/api/videos/single-server.ts23
-rw-r--r--server/tests/api/videos/video-abuse.ts9
-rw-r--r--server/tests/api/videos/video-change-ownership.ts8
-rw-r--r--server/tests/api/videos/video-channels.ts116
-rw-r--r--server/tests/api/videos/video-comments.ts6
-rw-r--r--server/tests/api/videos/video-hls.ts54
-rw-r--r--server/tests/api/videos/video-playlists.ts37
-rw-r--r--server/tests/api/videos/video-privacy.ts26
-rw-r--r--server/tests/api/videos/video-transcoder.ts89
-rw-r--r--server/tests/api/videos/videos-views-cleaner.ts16
-rw-r--r--server/tests/cli/optimize-old-videos.ts10
-rw-r--r--server/tests/cli/peertube.ts155
-rw-r--r--server/tests/feeds/feeds.ts76
-rw-r--r--server/tests/fixtures/preview.jpgbin4215 -> 6868 bytes
-rw-r--r--server/tests/fixtures/sample.oggbin0 -> 105243 bytes
-rw-r--r--server/tests/fixtures/video_short1-preview.webm.jpgbin10181 -> 22654 bytes
-rw-r--r--server/tools/cli.ts122
-rw-r--r--server/tools/package.json14
-rw-r--r--server/tools/peertube-auth.ts17
-rw-r--r--server/tools/peertube-get-access-token.ts18
-rw-r--r--server/tools/peertube-import-videos.ts171
-rw-r--r--server/tools/peertube-repl.ts11
-rw-r--r--server/tools/peertube-upload.ts91
-rw-r--r--server/tools/peertube-watch.ts47
-rw-r--r--[-rwxr-xr-x]server/tools/peertube.ts7
-rw-r--r--server/tools/tsconfig.json4
-rw-r--r--server/tools/yarn.lock2063
117 files changed, 4345 insertions, 1149 deletions
diff --git a/server/assets/default-audio-background.jpg b/server/assets/default-audio-background.jpg
new file mode 100644
index 000000000..a19173eac
--- /dev/null
+++ b/server/assets/default-audio-background.jpg
Binary files differ
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 8d4db1e75..5a1d652f2 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -16,7 +16,8 @@ import {
16 accountNameWithHostGetValidator, 16 accountNameWithHostGetValidator,
17 accountsSortValidator, 17 accountsSortValidator,
18 ensureAuthUserOwnsAccountValidator, 18 ensureAuthUserOwnsAccountValidator,
19 videosSortValidator 19 videosSortValidator,
20 videoChannelsSortValidator
20} from '../../middlewares/validators' 21} from '../../middlewares/validators'
21import { AccountModel } from '../../models/account/account' 22import { AccountModel } from '../../models/account/account'
22import { AccountVideoRateModel } from '../../models/account/account-video-rate' 23import { AccountVideoRateModel } from '../../models/account/account-video-rate'
@@ -56,6 +57,10 @@ accountsRouter.get('/:accountName/videos',
56 57
57accountsRouter.get('/:accountName/video-channels', 58accountsRouter.get('/:accountName/video-channels',
58 asyncMiddleware(accountNameWithHostGetValidator), 59 asyncMiddleware(accountNameWithHostGetValidator),
60 paginationValidator,
61 videoChannelsSortValidator,
62 setDefaultSort,
63 setDefaultPagination,
59 asyncMiddleware(listAccountChannels) 64 asyncMiddleware(listAccountChannels)
60) 65)
61 66
@@ -108,7 +113,14 @@ async function listAccounts (req: express.Request, res: express.Response) {
108} 113}
109 114
110async function listAccountChannels (req: express.Request, res: express.Response) { 115async function listAccountChannels (req: express.Request, res: express.Response) {
111 const resultList = await VideoChannelModel.listByAccount(res.locals.account.id) 116 const options = {
117 accountId: res.locals.account.id,
118 start: req.query.start,
119 count: req.query.count,
120 sort: req.query.sort
121 }
122
123 const resultList = await VideoChannelModel.listByAccount(options)
112 124
113 return res.json(getFormattedObjects(resultList.data, resultList.total)) 125 return res.json(getFormattedObjects(resultList.data, resultList.total))
114} 126}
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 40012c03b..1d12f701b 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -51,7 +51,7 @@ async function getConfig (req: express.Request, res: express.Response) {
51 if (serverCommit === undefined) serverCommit = await getServerCommit() 51 if (serverCommit === undefined) serverCommit = await getServerCommit()
52 52
53 const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) 53 const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS)
54 .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true) 54 .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
55 .map(r => parseInt(r, 10)) 55 .map(r => parseInt(r, 10))
56 56
57 const json: ServerConfig = { 57 const json: ServerConfig = {
@@ -255,13 +255,15 @@ function customConfig (): CustomConfig {
255 transcoding: { 255 transcoding: {
256 enabled: CONFIG.TRANSCODING.ENABLED, 256 enabled: CONFIG.TRANSCODING.ENABLED,
257 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, 257 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
258 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
258 threads: CONFIG.TRANSCODING.THREADS, 259 threads: CONFIG.TRANSCODING.THREADS,
259 resolutions: { 260 resolutions: {
260 '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], 261 '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
261 '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ], 262 '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ],
262 '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], 263 '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ],
263 '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], 264 '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ],
264 '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] 265 '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ],
266 '2160p': CONFIG.TRANSCODING.RESOLUTIONS[ '2160p' ]
265 }, 267 },
266 hls: { 268 hls: {
267 enabled: CONFIG.TRANSCODING.HLS.ENABLED 269 enabled: CONFIG.TRANSCODING.HLS.ENABLED
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 0aafba66e..c1d72087c 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -6,7 +6,7 @@ import { getFormattedObjects } from '../../../helpers/utils'
6import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants' 6import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants'
7import { Emailer } from '../../../lib/emailer' 7import { Emailer } from '../../../lib/emailer'
8import { Redis } from '../../../lib/redis' 8import { Redis } from '../../../lib/redis'
9import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user' 9import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
10import { 10import {
11 asyncMiddleware, 11 asyncMiddleware,
12 asyncRetryTransactionMiddleware, 12 asyncRetryTransactionMiddleware,
@@ -46,14 +46,18 @@ import { mySubscriptionsRouter } from './my-subscriptions'
46import { CONFIG } from '../../../initializers/config' 46import { CONFIG } from '../../../initializers/config'
47import { sequelizeTypescript } from '../../../initializers/database' 47import { sequelizeTypescript } from '../../../initializers/database'
48import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 48import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
49import { UserRegister } from '../../../../shared/models/users/user-register.model'
49 50
50const auditLogger = auditLoggerFactory('users') 51const auditLogger = auditLoggerFactory('users')
51 52
52const loginRateLimiter = new RateLimit({ 53// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
54// @ts-ignore
55const loginRateLimiter = RateLimit({
53 windowMs: RATES_LIMIT.LOGIN.WINDOW_MS, 56 windowMs: RATES_LIMIT.LOGIN.WINDOW_MS,
54 max: RATES_LIMIT.LOGIN.MAX 57 max: RATES_LIMIT.LOGIN.MAX
55}) 58})
56 59
60// @ts-ignore
57const askSendEmailLimiter = new RateLimit({ 61const askSendEmailLimiter = new RateLimit({
58 windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, 62 windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
59 max: RATES_LIMIT.ASK_SEND_EMAIL.MAX 63 max: RATES_LIMIT.ASK_SEND_EMAIL.MAX
@@ -143,7 +147,7 @@ usersRouter.post('/:id/reset-password',
143usersRouter.post('/ask-send-verify-email', 147usersRouter.post('/ask-send-verify-email',
144 askSendEmailLimiter, 148 askSendEmailLimiter,
145 asyncMiddleware(usersAskSendVerifyEmailValidator), 149 asyncMiddleware(usersAskSendVerifyEmailValidator),
146 asyncMiddleware(askSendVerifyUserEmail) 150 asyncMiddleware(reSendVerifyUserEmail)
147) 151)
148 152
149usersRouter.post('/:id/verify-email', 153usersRouter.post('/:id/verify-email',
@@ -180,7 +184,7 @@ async function createUser (req: express.Request, res: express.Response) {
180 adminFlags: body.adminFlags || UserAdminFlag.NONE 184 adminFlags: body.adminFlags || UserAdminFlag.NONE
181 }) 185 })
182 186
183 const { user, account } = await createUserAccountAndChannelAndPlaylist(userToCreate) 187 const { user, account } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate })
184 188
185 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) 189 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
186 logger.info('User %s with its channel and account created.', body.username) 190 logger.info('User %s with its channel and account created.', body.username)
@@ -189,15 +193,14 @@ async function createUser (req: express.Request, res: express.Response) {
189 user: { 193 user: {
190 id: user.id, 194 id: user.id,
191 account: { 195 account: {
192 id: account.id, 196 id: account.id
193 uuid: account.Actor.uuid
194 } 197 }
195 } 198 }
196 }).end() 199 }).end()
197} 200}
198 201
199async function registerUser (req: express.Request, res: express.Response) { 202async function registerUser (req: express.Request, res: express.Response) {
200 const body: UserCreate = req.body 203 const body: UserRegister = req.body
201 204
202 const userToCreate = new UserModel({ 205 const userToCreate = new UserModel({
203 username: body.username, 206 username: body.username,
@@ -211,7 +214,11 @@ async function registerUser (req: express.Request, res: express.Response) {
211 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null 214 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
212 }) 215 })
213 216
214 const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate) 217 const { user } = await createUserAccountAndChannelAndPlaylist({
218 userToCreate: userToCreate,
219 userDisplayName: body.displayName || undefined,
220 channelNames: body.channel
221 })
215 222
216 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) 223 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
217 logger.info('User %s with its channel and account registered.', body.username) 224 logger.info('User %s with its channel and account registered.', body.username)
@@ -313,14 +320,7 @@ async function resetUserPassword (req: express.Request, res: express.Response) {
313 return res.status(204).end() 320 return res.status(204).end()
314} 321}
315 322
316async function sendVerifyUserEmail (user: UserModel) { 323async function reSendVerifyUserEmail (req: express.Request, res: express.Response) {
317 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
318 const url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
319 await Emailer.Instance.addVerifyEmailJob(user.email, url)
320 return
321}
322
323async function askSendVerifyUserEmail (req: express.Request, res: express.Response) {
324 const user = res.locals.user 324 const user = res.locals.user
325 325
326 await sendVerifyUserEmail(user) 326 await sendVerifyUserEmail(user)
@@ -332,6 +332,11 @@ async function verifyUserEmail (req: express.Request, res: express.Response) {
332 const user = res.locals.user 332 const user = res.locals.user
333 user.emailVerified = true 333 user.emailVerified = true
334 334
335 if (req.body.isPendingEmail === true) {
336 user.email = user.pendingEmail
337 user.pendingEmail = null
338 }
339
335 await user.save() 340 await user.save()
336 341
337 return res.status(204).end() 342 return res.status(204).end()
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index ddb239e7b..1750a02e9 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -28,6 +28,7 @@ import { VideoImportModel } from '../../../models/video/video-import'
28import { AccountModel } from '../../../models/account/account' 28import { AccountModel } from '../../../models/account/account'
29import { CONFIG } from '../../../initializers/config' 29import { CONFIG } from '../../../initializers/config'
30import { sequelizeTypescript } from '../../../initializers/database' 30import { sequelizeTypescript } from '../../../initializers/database'
31import { sendVerifyUserEmail } from '../../../lib/user'
31 32
32const auditLogger = auditLoggerFactory('users-me') 33const auditLogger = auditLoggerFactory('users-me')
33 34
@@ -171,17 +172,26 @@ async function deleteMe (req: express.Request, res: express.Response) {
171 172
172async function updateMe (req: express.Request, res: express.Response) { 173async function updateMe (req: express.Request, res: express.Response) {
173 const body: UserUpdateMe = req.body 174 const body: UserUpdateMe = req.body
175 let sendVerificationEmail = false
174 176
175 const user = res.locals.oauth.token.user 177 const user = res.locals.oauth.token.user
176 const oldUserAuditView = new UserAuditView(user.toFormattedJSON({})) 178 const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
177 179
178 if (body.password !== undefined) user.password = body.password 180 if (body.password !== undefined) user.password = body.password
179 if (body.email !== undefined) user.email = body.email
180 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy 181 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
181 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled 182 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
182 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo 183 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
183 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled 184 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
184 185
186 if (body.email !== undefined) {
187 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
188 user.pendingEmail = body.email
189 sendVerificationEmail = true
190 } else {
191 user.email = body.email
192 }
193 }
194
185 await sequelizeTypescript.transaction(async t => { 195 await sequelizeTypescript.transaction(async t => {
186 const userAccount = await AccountModel.load(user.Account.id) 196 const userAccount = await AccountModel.load(user.Account.id)
187 197
@@ -196,6 +206,10 @@ async function updateMe (req: express.Request, res: express.Response) {
196 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView) 206 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
197 }) 207 })
198 208
209 if (sendVerificationEmail === true) {
210 await sendVerifyUserEmail(user, true)
211 }
212
199 return res.sendStatus(204) 213 return res.sendStatus(204)
200} 214}
201 215
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 3d6dbfe70..81a03a62b 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -19,7 +19,7 @@ import { VideoChannelModel } from '../../models/video/video-channel'
19import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' 19import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
20import { sendUpdateActor } from '../../lib/activitypub/send' 20import { sendUpdateActor } from '../../lib/activitypub/send'
21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
22import { createVideoChannel } from '../../lib/video-channel' 22import { createVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
23import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 23import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
24import { setAsyncActorKeys } from '../../lib/activitypub' 24import { setAsyncActorKeys } from '../../lib/activitypub'
25import { AccountModel } from '../../models/account/account' 25import { AccountModel } from '../../models/account/account'
@@ -143,15 +143,14 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
143 }) 143 })
144 144
145 setAsyncActorKeys(videoChannelCreated.Actor) 145 setAsyncActorKeys(videoChannelCreated.Actor)
146 .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err })) 146 .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.url, { err }))
147 147
148 auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON())) 148 auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON()))
149 logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid) 149 logger.info('Video channel %s created.', videoChannelCreated.Actor.url)
150 150
151 return res.json({ 151 return res.json({
152 videoChannel: { 152 videoChannel: {
153 id: videoChannelCreated.id, 153 id: videoChannelCreated.id
154 uuid: videoChannelCreated.Actor.uuid
155 } 154 }
156 }).end() 155 }).end()
157} 156}
@@ -161,6 +160,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
161 const videoChannelFieldsSave = videoChannelInstance.toJSON() 160 const videoChannelFieldsSave = videoChannelInstance.toJSON()
162 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()) 161 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
163 const videoChannelInfoToUpdate = req.body as VideoChannelUpdate 162 const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
163 let doBulkVideoUpdate = false
164 164
165 try { 165 try {
166 await sequelizeTypescript.transaction(async t => { 166 await sequelizeTypescript.transaction(async t => {
@@ -168,9 +168,18 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
168 transaction: t 168 transaction: t
169 } 169 }
170 170
171 if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.displayName) 171 if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName
172 if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) 172 if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description
173 if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support) 173
174 if (videoChannelInfoToUpdate.support !== undefined) {
175 const oldSupportField = videoChannelInstance.support
176 videoChannelInstance.support = videoChannelInfoToUpdate.support
177
178 if (videoChannelInfoToUpdate.bulkVideosSupportUpdate === true && oldSupportField !== videoChannelInfoToUpdate.support) {
179 doBulkVideoUpdate = true
180 await VideoModel.bulkUpdateSupportField(videoChannelInstance, t)
181 }
182 }
174 183
175 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) 184 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions)
176 await sendUpdateActor(videoChannelInstanceUpdated, t) 185 await sendUpdateActor(videoChannelInstanceUpdated, t)
@@ -180,7 +189,8 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
180 new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()), 189 new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()),
181 oldVideoChannelAuditKeys 190 oldVideoChannelAuditKeys
182 ) 191 )
183 logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) 192
193 logger.info('Video channel %s updated.', videoChannelInstance.Actor.url)
184 }) 194 })
185 } catch (err) { 195 } catch (err) {
186 logger.debug('Cannot update the video channel.', { err }) 196 logger.debug('Cannot update the video channel.', { err })
@@ -193,7 +203,12 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
193 throw err 203 throw err
194 } 204 }
195 205
196 return res.type('json').status(204).end() 206 res.type('json').status(204).end()
207
208 // Don't process in a transaction, and after the response because it could be long
209 if (doBulkVideoUpdate) {
210 await federateAllVideosOfChannel(videoChannelInstance)
211 }
197} 212}
198 213
199async function removeVideoChannel (req: express.Request, res: express.Response) { 214async function removeVideoChannel (req: express.Request, res: express.Response) {
@@ -205,7 +220,7 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
205 await videoChannelInstance.destroy({ transaction: t }) 220 await videoChannelInstance.destroy({ transaction: t })
206 221
207 auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())) 222 auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
208 logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) 223 logger.info('Video channel %s deleted.', videoChannelInstance.Actor.url)
209 }) 224 })
210 225
211 return res.type('json').status(204).end() 226 return res.type('json').status(204).end()
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
index a17136401..62490e63b 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -203,7 +203,9 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
203 const videoPlaylistInstance = res.locals.videoPlaylist 203 const videoPlaylistInstance = res.locals.videoPlaylist
204 const videoPlaylistFieldsSave = videoPlaylistInstance.toJSON() 204 const videoPlaylistFieldsSave = videoPlaylistInstance.toJSON()
205 const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate 205 const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate
206
206 const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE 207 const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE
208 const wasNotPrivatePlaylist = videoPlaylistInstance.privacy !== VideoPlaylistPrivacy.PRIVATE
207 209
208 const thumbnailField = req.files['thumbnailfile'] 210 const thumbnailField = req.files['thumbnailfile']
209 const thumbnailModel = thumbnailField 211 const thumbnailModel = thumbnailField
@@ -232,6 +234,10 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
232 234
233 if (videoPlaylistInfoToUpdate.privacy !== undefined) { 235 if (videoPlaylistInfoToUpdate.privacy !== undefined) {
234 videoPlaylistInstance.privacy = parseInt(videoPlaylistInfoToUpdate.privacy.toString(), 10) 236 videoPlaylistInstance.privacy = parseInt(videoPlaylistInfoToUpdate.privacy.toString(), 10)
237
238 if (wasNotPrivatePlaylist === true && videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE) {
239 await sendDeleteVideoPlaylist(videoPlaylistInstance, t)
240 }
235 } 241 }
236 242
237 const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) 243 const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 1a18a8ae8..5ebd8fbc4 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -6,7 +6,14 @@ import { logger } from '../../../helpers/logger'
6import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 6import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
7import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 7import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
8import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' 8import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
9import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' 9import {
10 DEFAULT_AUDIO_RESOLUTION,
11 MIMETYPES,
12 VIDEO_CATEGORIES,
13 VIDEO_LANGUAGES,
14 VIDEO_LICENCES,
15 VIDEO_PRIVACIES
16} from '../../../initializers/constants'
10import { 17import {
11 changeVideoChannelShare, 18 changeVideoChannelShare,
12 federateVideoIfNeeded, 19 federateVideoIfNeeded,
@@ -54,6 +61,7 @@ import { CONFIG } from '../../../initializers/config'
54import { sequelizeTypescript } from '../../../initializers/database' 61import { sequelizeTypescript } from '../../../initializers/database'
55import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' 62import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
56import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 63import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
64import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
57 65
58const auditLogger = auditLoggerFactory('videos') 66const auditLogger = auditLoggerFactory('videos')
59const videosRouter = express.Router() 67const videosRouter = express.Router()
@@ -191,17 +199,17 @@ async function addVideo (req: express.Request, res: express.Response) {
191 const video = new VideoModel(videoData) 199 const video = new VideoModel(videoData)
192 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 200 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
193 201
194 // Build the file object 202 const videoFile = new VideoFileModel({
195 const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
196 const fps = await getVideoFileFPS(videoPhysicalFile.path)
197
198 const videoFileData = {
199 extname: extname(videoPhysicalFile.filename), 203 extname: extname(videoPhysicalFile.filename),
200 resolution: videoFileResolution, 204 size: videoPhysicalFile.size
201 size: videoPhysicalFile.size, 205 })
202 fps 206
207 if (videoFile.isAudio()) {
208 videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
209 } else {
210 videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
211 videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
203 } 212 }
204 const videoFile = new VideoFileModel(videoFileData)
205 213
206 // Move physical file 214 // Move physical file
207 const videoDir = CONFIG.STORAGE.VIDEOS_DIR 215 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
@@ -279,9 +287,21 @@ async function addVideo (req: express.Request, res: express.Response) {
279 287
280 if (video.state === VideoState.TO_TRANSCODE) { 288 if (video.state === VideoState.TO_TRANSCODE) {
281 // Put uuid because we don't have id auto incremented for now 289 // Put uuid because we don't have id auto incremented for now
282 const dataInput = { 290 let dataInput: VideoTranscodingPayload
283 videoUUID: videoCreated.uuid, 291
284 isNewVideo: true 292 if (videoFile.isAudio()) {
293 dataInput = {
294 type: 'merge-audio' as 'merge-audio',
295 resolution: DEFAULT_AUDIO_RESOLUTION,
296 videoUUID: videoCreated.uuid,
297 isNewVideo: true
298 }
299 } else {
300 dataInput = {
301 type: 'optimize' as 'optimize',
302 videoUUID: videoCreated.uuid,
303 isNewVideo: true
304 }
285 } 305 }
286 306
287 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) 307 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
@@ -300,7 +320,9 @@ async function updateVideo (req: express.Request, res: express.Response) {
300 const videoFieldsSave = videoInstance.toJSON() 320 const videoFieldsSave = videoInstance.toJSON()
301 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) 321 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
302 const videoInfoToUpdate: VideoUpdate = req.body 322 const videoInfoToUpdate: VideoUpdate = req.body
323
303 const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE 324 const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
325 const wasNotPrivateVideo = videoInstance.privacy !== VideoPrivacy.PRIVATE
304 const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED 326 const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
305 327
306 // Process thumbnail or create it from the video 328 // Process thumbnail or create it from the video
@@ -336,9 +358,15 @@ async function updateVideo (req: express.Request, res: express.Response) {
336 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) 358 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
337 videoInstance.privacy = newPrivacy 359 videoInstance.privacy = newPrivacy
338 360
361 // The video was private, and is not anymore -> publish it
339 if (wasPrivateVideo === true && newPrivacy !== VideoPrivacy.PRIVATE) { 362 if (wasPrivateVideo === true && newPrivacy !== VideoPrivacy.PRIVATE) {
340 videoInstance.publishedAt = new Date() 363 videoInstance.publishedAt = new Date()
341 } 364 }
365
366 // The video was not private, but now it is -> we need to unfederate it
367 if (wasNotPrivateVideo === true && newPrivacy === VideoPrivacy.PRIVATE) {
368 await VideoModel.sendDelete(videoInstance, { transaction: t })
369 }
342 } 370 }
343 371
344 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) 372 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index fb2e7742a..a6b462443 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -194,7 +194,7 @@ async function getVideoCaption (req: express.Request, res: express.Response) {
194 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE }) 194 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
195} 195}
196 196
197async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) { 197async function generateNodeinfo (req: express.Request, res: express.Response) {
198 const { totalVideos } = await VideoModel.getStats() 198 const { totalVideos } = await VideoModel.getStats()
199 const { totalLocalVideoComments } = await VideoCommentModel.getStats() 199 const { totalLocalVideoComments } = await VideoCommentModel.getStats()
200 const { totalUsers } = await UserModel.getStats() 200 const { totalUsers } = await UserModel.getStats()
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 305d3b71e..b1e9af0a1 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -134,6 +134,10 @@ function isProdInstance () {
134 return process.env.NODE_ENV === 'production' 134 return process.env.NODE_ENV === 'production'
135} 135}
136 136
137function getAppNumber () {
138 return process.env.NODE_APP_INSTANCE
139}
140
137function root () { 141function root () {
138 // We are in /helpers/utils.js 142 // We are in /helpers/utils.js
139 const paths = [ __dirname, '..', '..' ] 143 const paths = [ __dirname, '..', '..' ]
@@ -256,6 +260,7 @@ const execPromise = promisify1<string, string>(exec)
256export { 260export {
257 isTestInstance, 261 isTestInstance,
258 isProdInstance, 262 isProdInstance,
263 getAppNumber,
259 264
260 objectConverter, 265 objectConverter,
261 root, 266 root,
diff --git a/server/helpers/custom-validators/accounts.ts b/server/helpers/custom-validators/accounts.ts
index 146c7708e..31a2de5ca 100644
--- a/server/helpers/custom-validators/accounts.ts
+++ b/server/helpers/custom-validators/accounts.ts
@@ -1,7 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { Response } from 'express' 2import { Response } from 'express'
3import 'express-validator' 3import 'express-validator'
4import * as validator from 'validator'
5import { AccountModel } from '../../models/account/account' 4import { AccountModel } from '../../models/account/account'
6import { isUserDescriptionValid, isUserUsernameValid } from './users' 5import { isUserDescriptionValid, isUserUsernameValid } from './users'
7import { exists } from './misc' 6import { exists } from './misc'
@@ -18,14 +17,8 @@ function isAccountDescriptionValid (value: string) {
18 return isUserDescriptionValid(value) 17 return isUserDescriptionValid(value)
19} 18}
20 19
21function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { 20function doesAccountIdExist (id: number, res: Response, sendNotFound = true) {
22 let promise: Bluebird<AccountModel> 21 const promise = AccountModel.load(id)
23
24 if (validator.isInt('' + id)) {
25 promise = AccountModel.load(+id)
26 } else { // UUID
27 promise = AccountModel.loadByUUID('' + id)
28 }
29 22
30 return doesAccountExist(promise, res, sendNotFound) 23 return doesAccountExist(promise, res, sendNotFound)
31} 24}
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts
index 26c8c4cc6..e04c5388f 100644
--- a/server/helpers/custom-validators/activitypub/video-comments.ts
+++ b/server/helpers/custom-validators/activitypub/video-comments.ts
@@ -36,7 +36,8 @@ function normalizeComment (comment: any) {
36 if (!comment) return 36 if (!comment) return
37 37
38 if (typeof comment.url !== 'string') { 38 if (typeof comment.url !== 'string') {
39 comment.url = comment.url.href || comment.url.url 39 if (typeof comment.url === 'object') comment.url = comment.url.href || comment.url.url
40 else comment.url = comment.id
40 } 41 }
41 42
42 return 43 return
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts
index fd56b9a70..f818ce8f1 100644
--- a/server/helpers/custom-validators/video-channels.ts
+++ b/server/helpers/custom-validators/video-channels.ts
@@ -26,13 +26,8 @@ async function doesLocalVideoChannelNameExist (name: string, res: express.Respon
26 return processVideoChannelExist(videoChannel, res) 26 return processVideoChannelExist(videoChannel, res)
27} 27}
28 28
29async function doesVideoChannelIdExist (id: number | string, res: express.Response) { 29async function doesVideoChannelIdExist (id: number, res: express.Response) {
30 let videoChannel: VideoChannelModel 30 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
31 if (validator.isInt('' + id)) {
32 videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
33 } else { // UUID
34 videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount('' + id)
35 }
36 31
37 return processVideoChannelExist(videoChannel, res) 32 return processVideoChannelExist(videoChannel, res)
38} 33}
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index e0a1d56a5..00f3f198b 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -74,7 +74,18 @@ function createReqFiles (
74 }, 74 },
75 75
76 filename: async (req, file, cb) => { 76 filename: async (req, file, cb) => {
77 const extension = mimeTypes[ file.mimetype ] || extname(file.originalname) 77 let extension: string
78 const fileExtension = extname(file.originalname)
79 const extensionFromMimetype = mimeTypes[ file.mimetype ]
80
81 // Take the file extension if we don't understand the mime type
82 // We have the OGG/OGV exception too because firefox sends a bad mime type when sending an OGG file
83 if (fileExtension === '.ogg' || fileExtension === '.ogv' || !extensionFromMimetype) {
84 extension = fileExtension
85 } else {
86 extension = extensionFromMimetype
87 }
88
78 let randomString = '' 89 let randomString = ''
79 90
80 try { 91 try {
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 76b744de8..8041e7b3b 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,6 +1,6 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { dirname, join } from 'path' 2import { dirname, join } from 'path'
3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, getMaxBitrate, VideoResolution } from '../../shared/models/videos'
4import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 4import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
@@ -18,7 +18,8 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
18 VideoResolution.H_360P, 18 VideoResolution.H_360P,
19 VideoResolution.H_720P, 19 VideoResolution.H_720P,
20 VideoResolution.H_240P, 20 VideoResolution.H_240P,
21 VideoResolution.H_1080P 21 VideoResolution.H_1080P,
22 VideoResolution.H_4K
22 ] 23 ]
23 24
24 for (const resolution of resolutions) { 25 for (const resolution of resolutions) {
@@ -31,7 +32,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
31} 32}
32 33
33async function getVideoFileSize (path: string) { 34async function getVideoFileSize (path: string) {
34 const videoStream = await getVideoFileStream(path) 35 const videoStream = await getVideoStreamFromFile(path)
35 36
36 return { 37 return {
37 width: videoStream.width, 38 width: videoStream.width,
@@ -49,7 +50,7 @@ async function getVideoFileResolution (path: string) {
49} 50}
50 51
51async function getVideoFileFPS (path: string) { 52async function getVideoFileFPS (path: string) {
52 const videoStream = await getVideoFileStream(path) 53 const videoStream = await getVideoStreamFromFile(path)
53 54
54 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { 55 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
55 const valuesText: string = videoStream[key] 56 const valuesText: string = videoStream[key]
@@ -117,25 +118,50 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
117 } 118 }
118} 119}
119 120
120type TranscodeOptions = { 121type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio'
122
123interface BaseTranscodeOptions {
124 type: TranscodeOptionsType
121 inputPath: string 125 inputPath: string
122 outputPath: string 126 outputPath: string
123 resolution: VideoResolution 127 resolution: VideoResolution
124 isPortraitMode?: boolean 128 isPortraitMode?: boolean
129}
125 130
126 hlsPlaylist?: { 131interface HLSTranscodeOptions extends BaseTranscodeOptions {
132 type: 'hls'
133 hlsPlaylist: {
127 videoFilename: string 134 videoFilename: string
128 } 135 }
129} 136}
130 137
138interface QuickTranscodeOptions extends BaseTranscodeOptions {
139 type: 'quick-transcode'
140}
141
142interface VideoTranscodeOptions extends BaseTranscodeOptions {
143 type: 'video'
144}
145
146interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
147 type: 'merge-audio'
148 audioPath: string
149}
150
151type TranscodeOptions = HLSTranscodeOptions | VideoTranscodeOptions | MergeAudioTranscodeOptions | QuickTranscodeOptions
152
131function transcode (options: TranscodeOptions) { 153function transcode (options: TranscodeOptions) {
132 return new Promise<void>(async (res, rej) => { 154 return new Promise<void>(async (res, rej) => {
133 try { 155 try {
134 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 156 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
135 .output(options.outputPath) 157 .output(options.outputPath)
136 158
137 if (options.hlsPlaylist) { 159 if (options.type === 'quick-transcode') {
160 command = await buildQuickTranscodeCommand(command)
161 } else if (options.type === 'hls') {
138 command = await buildHLSCommand(command, options) 162 command = await buildHLSCommand(command, options)
163 } else if (options.type === 'merge-audio') {
164 command = await buildAudioMergeCommand(command, options)
139 } else { 165 } else {
140 command = await buildx264Command(command, options) 166 command = await buildx264Command(command, options)
141 } 167 }
@@ -151,7 +177,7 @@ function transcode (options: TranscodeOptions) {
151 return rej(err) 177 return rej(err)
152 }) 178 })
153 .on('end', () => { 179 .on('end', () => {
154 return onTranscodingSuccess(options) 180 return fixHLSPlaylistIfNeeded(options)
155 .then(() => res()) 181 .then(() => res())
156 .catch(err => rej(err)) 182 .catch(err => rej(err))
157 }) 183 })
@@ -162,6 +188,30 @@ function transcode (options: TranscodeOptions) {
162 }) 188 })
163} 189}
164 190
191async function canDoQuickTranscode (path: string): Promise<boolean> {
192 // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway)
193 const videoStream = await getVideoStreamFromFile(path)
194 const parsedAudio = await audio.get(path)
195 const fps = await getVideoFileFPS(path)
196 const bitRate = await getVideoFileBitrate(path)
197 const resolution = await getVideoFileResolution(path)
198
199 // check video params
200 if (videoStream[ 'codec_name' ] !== 'h264') return false
201 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
202 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
203
204 // check audio params (if audio stream exists)
205 if (parsedAudio.audioStream) {
206 if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false
207
208 const maxAudioBitrate = audio.bitrate[ 'aac' ](parsedAudio.audioStream[ 'bit_rate' ])
209 if (maxAudioBitrate !== -1 && parsedAudio.audioStream[ 'bit_rate' ] > maxAudioBitrate) return false
210 }
211
212 return true
213}
214
165// --------------------------------------------------------------------------- 215// ---------------------------------------------------------------------------
166 216
167export { 217export {
@@ -169,16 +219,19 @@ export {
169 getVideoFileResolution, 219 getVideoFileResolution,
170 getDurationFromVideoFile, 220 getDurationFromVideoFile,
171 generateImageFromVideoFile, 221 generateImageFromVideoFile,
222 TranscodeOptions,
223 TranscodeOptionsType,
172 transcode, 224 transcode,
173 getVideoFileFPS, 225 getVideoFileFPS,
174 computeResolutionsToTranscode, 226 computeResolutionsToTranscode,
175 audio, 227 audio,
176 getVideoFileBitrate 228 getVideoFileBitrate,
229 canDoQuickTranscode
177} 230}
178 231
179// --------------------------------------------------------------------------- 232// ---------------------------------------------------------------------------
180 233
181async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { 234async function buildx264Command (command: ffmpeg.FfmpegCommand, options: VideoTranscodeOptions) {
182 let fps = await getVideoFileFPS(options.inputPath) 235 let fps = await getVideoFileFPS(options.inputPath)
183 // On small/medium resolutions, limit FPS 236 // On small/medium resolutions, limit FPS
184 if ( 237 if (
@@ -189,7 +242,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
189 fps = VIDEO_TRANSCODING_FPS.AVERAGE 242 fps = VIDEO_TRANSCODING_FPS.AVERAGE
190 } 243 }
191 244
192 command = await presetH264(command, options.resolution, fps) 245 command = await presetH264(command, options.inputPath, options.resolution, fps)
193 246
194 if (options.resolution !== undefined) { 247 if (options.resolution !== undefined) {
195 // '?x720' or '720x?' for example 248 // '?x720' or '720x?' for example
@@ -208,7 +261,29 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
208 return command 261 return command
209} 262}
210 263
211async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { 264async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
265 command = command.loop(undefined)
266
267 command = await presetH264VeryFast(command, options.audioPath, options.resolution)
268
269 command = command.input(options.audioPath)
270 .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
271 .outputOption('-tune stillimage')
272 .outputOption('-shortest')
273
274 return command
275}
276
277async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
278 command = await presetCopy(command)
279
280 command = command.outputOption('-map_metadata -1') // strip all metadata
281 .outputOption('-movflags faststart')
282
283 return command
284}
285
286async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
212 const videoPath = getHLSVideoPath(options) 287 const videoPath = getHLSVideoPath(options)
213 288
214 command = await presetCopy(command) 289 command = await presetCopy(command)
@@ -224,26 +299,26 @@ async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: Transcod
224 return command 299 return command
225} 300}
226 301
227function getHLSVideoPath (options: TranscodeOptions) { 302function getHLSVideoPath (options: HLSTranscodeOptions) {
228 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` 303 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
229} 304}
230 305
231async function onTranscodingSuccess (options: TranscodeOptions) { 306async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
232 if (!options.hlsPlaylist) return 307 if (options.type !== 'hls') return
233 308
234 // Fix wrong mapping with some ffmpeg versions
235 const fileContent = await readFile(options.outputPath) 309 const fileContent = await readFile(options.outputPath)
236 310
237 const videoFileName = options.hlsPlaylist.videoFilename 311 const videoFileName = options.hlsPlaylist.videoFilename
238 const videoFilePath = getHLSVideoPath(options) 312 const videoFilePath = getHLSVideoPath(options)
239 313
314 // Fix wrong mapping with some ffmpeg versions
240 const newContent = fileContent.toString() 315 const newContent = fileContent.toString()
241 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`) 316 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
242 317
243 await writeFile(options.outputPath, newContent) 318 await writeFile(options.outputPath, newContent)
244} 319}
245 320
246function getVideoFileStream (path: string) { 321function getVideoStreamFromFile (path: string) {
247 return new Promise<any>((res, rej) => { 322 return new Promise<any>((res, rej) => {
248 ffmpeg.ffprobe(path, (err, metadata) => { 323 ffmpeg.ffprobe(path, (err, metadata) => {
249 if (err) return rej(err) 324 if (err) return rej(err)
@@ -263,44 +338,27 @@ function getVideoFileStream (path: string) {
263 * and quality. Superfast and ultrafast will give you better 338 * and quality. Superfast and ultrafast will give you better
264 * performance, but then quality is noticeably worse. 339 * performance, but then quality is noticeably worse.
265 */ 340 */
266async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { 341async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
267 let localCommand = await presetH264(command, resolution, fps) 342 let localCommand = await presetH264(command, input, resolution, fps)
343
268 localCommand = localCommand.outputOption('-preset:v veryfast') 344 localCommand = localCommand.outputOption('-preset:v veryfast')
269 .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) 345
270 /* 346 /*
271 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html 347 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
272 Our target situation is closer to a livestream than a stream, 348 Our target situation is closer to a livestream than a stream,
273 since we want to reduce as much a possible the encoding burden, 349 since we want to reduce as much a possible the encoding burden,
274 altough not to the point of a livestream where there is a hard 350 although not to the point of a livestream where there is a hard
275 constraint on the frames per second to be encoded. 351 constraint on the frames per second to be encoded.
276
277 why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'?
278 Make up for most of the loss of grain and macroblocking
279 with less computing power.
280 */ 352 */
281 353
282 return localCommand 354 return localCommand
283} 355}
284 356
285/** 357/**
286 * A preset optimised for a stillimage audio video
287 */
288async function presetStillImageWithAudio (
289 command: ffmpeg.FfmpegCommand,
290 resolution: VideoResolution,
291 fps: number
292): Promise<ffmpeg.FfmpegCommand> {
293 let localCommand = await presetH264VeryFast(command, resolution, fps)
294 localCommand = localCommand.outputOption('-tune stillimage')
295
296 return localCommand
297}
298
299/**
300 * A toolbox to play with audio 358 * A toolbox to play with audio
301 */ 359 */
302namespace audio { 360namespace audio {
303 export const get = (option: ffmpeg.FfmpegCommand | string) => { 361 export const get = (option: string) => {
304 // without position, ffprobe considers the last input only 362 // without position, ffprobe considers the last input only
305 // we make it consider the first input only 363 // we make it consider the first input only
306 // if you pass a file path to pos, then ffprobe acts on that file directly 364 // if you pass a file path to pos, then ffprobe acts on that file directly
@@ -322,11 +380,7 @@ namespace audio {
322 return res({ absolutePath: data.format.filename }) 380 return res({ absolutePath: data.format.filename })
323 } 381 }
324 382
325 if (typeof option === 'string') { 383 return ffmpeg.ffprobe(option, parseFfprobe)
326 return ffmpeg.ffprobe(option, parseFfprobe)
327 }
328
329 return option.ffprobe(parseFfprobe)
330 }) 384 })
331 } 385 }
332 386
@@ -368,7 +422,7 @@ namespace audio {
368 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 422 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
369 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr 423 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
370 */ 424 */
371async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { 425async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
372 let localCommand = command 426 let localCommand = command
373 .format('mp4') 427 .format('mp4')
374 .videoCodec('libx264') 428 .videoCodec('libx264')
@@ -379,7 +433,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
379 .outputOption('-map_metadata -1') // strip all metadata 433 .outputOption('-map_metadata -1') // strip all metadata
380 .outputOption('-movflags faststart') 434 .outputOption('-movflags faststart')
381 435
382 const parsedAudio = await audio.get(localCommand) 436 const parsedAudio = await audio.get(input)
383 437
384 if (!parsedAudio.audioStream) { 438 if (!parsedAudio.audioStream) {
385 localCommand = localCommand.noAudio() 439 localCommand = localCommand.noAudio()
@@ -388,28 +442,30 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
388 .audioCodec('libfdk_aac') 442 .audioCodec('libfdk_aac')
389 .audioQuality(5) 443 .audioQuality(5)
390 } else { 444 } else {
391 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates 445 // we try to reduce the ceiling bitrate by making rough matches of bitrates
392 // of course this is far from perfect, but it might save some space in the end 446 // of course this is far from perfect, but it might save some space in the end
447 localCommand = localCommand.audioCodec('aac')
448
393 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] 449 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
394 let bitrate: number
395 if (audio.bitrate[ audioCodecName ]) {
396 localCommand = localCommand.audioCodec('aac')
397 450
398 bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) 451 if (audio.bitrate[ audioCodecName ]) {
452 const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
399 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) 453 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
400 } 454 }
401 } 455 }
402 456
403 // Constrained Encoding (VBV) 457 if (fps) {
404 // https://slhck.info/video/2017/03/01/rate-control.html 458 // Constrained Encoding (VBV)
405 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate 459 // https://slhck.info/video/2017/03/01/rate-control.html
406 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) 460 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
407 localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) 461 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
408 462 localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ])
409 // Keyframe interval of 2 seconds for faster seeking and resolution switching. 463
410 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html 464 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
411 // https://superuser.com/a/908325 465 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
412 localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) 466 // https://superuser.com/a/908325
467 localCommand = localCommand.outputOption(`-g ${fps * 2}`)
468 }
413 469
414 return localCommand 470 return localCommand
415} 471}
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 734523b01..8603dd761 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -2,6 +2,7 @@
2import { mkdirpSync } from 'fs-extra' 2import { mkdirpSync } from 'fs-extra'
3import * as path from 'path' 3import * as path from 'path'
4import * as winston from 'winston' 4import * as winston from 'winston'
5import { FileTransportOptions } from 'winston/lib/winston/transports'
5import { CONFIG } from '../initializers/config' 6import { CONFIG } from '../initializers/config'
6import { omit } from 'lodash' 7import { omit } from 'lodash'
7 8
@@ -45,6 +46,21 @@ const labelFormatter = winston.format.label({
45 label 46 label
46}) 47})
47 48
49const fileLoggerOptions: FileTransportOptions = {
50
51 filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'),
52 handleExceptions: true,
53 format: winston.format.combine(
54 winston.format.timestamp(),
55 jsonLoggerFormat
56 )
57}
58
59if (CONFIG.LOG.ROTATION) {
60 fileLoggerOptions.maxsize = 1024 * 1024 * 12
61 fileLoggerOptions.maxFiles = 20
62}
63
48const logger = winston.createLogger({ 64const logger = winston.createLogger({
49 level: CONFIG.LOG.LEVEL, 65 level: CONFIG.LOG.LEVEL,
50 format: winston.format.combine( 66 format: winston.format.combine(
@@ -52,16 +68,7 @@ const logger = winston.createLogger({
52 winston.format.splat() 68 winston.format.splat()
53 ), 69 ),
54 transports: [ 70 transports: [
55 new winston.transports.File({ 71 new winston.transports.File(fileLoggerOptions),
56 filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'),
57 handleExceptions: true,
58 maxsize: 1024 * 1024 * 12,
59 maxFiles: 20,
60 format: winston.format.combine(
61 winston.format.timestamp(),
62 jsonLoggerFormat
63 )
64 }),
65 new winston.transports.Console({ 72 new winston.transports.Console({
66 handleExceptions: true, 73 handleExceptions: true,
67 format: winston.format.combine( 74 format: winston.format.combine(
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 622ad7d6b..c211d725c 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -10,6 +10,7 @@ function checkMissedConfig () {
10 'trust_proxy', 10 'trust_proxy',
11 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', 11 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max',
12 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', 12 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
13 'email.body.signature', 'email.object.prefix',
13 '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',
14 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists', 15 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists',
15 'log.level', 16 'log.level',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 4f77e144d..bb278ba43 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -44,6 +44,14 @@ const CONFIG = {
44 CA_FILE: config.get<string>('smtp.ca_file'), 44 CA_FILE: config.get<string>('smtp.ca_file'),
45 FROM_ADDRESS: config.get<string>('smtp.from_address') 45 FROM_ADDRESS: config.get<string>('smtp.from_address')
46 }, 46 },
47 EMAIL: {
48 BODY: {
49 SIGNATURE: config.get<string>('email.body.signature')
50 },
51 OBJECT: {
52 PREFIX: config.get<string>('email.object.prefix') + ' '
53 }
54 },
47 STORAGE: { 55 STORAGE: {
48 TMP_DIR: buildPath(config.get<string>('storage.tmp')), 56 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
49 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), 57 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')),
@@ -75,7 +83,8 @@ const CONFIG = {
75 }, 83 },
76 TRUST_PROXY: config.get<string[]>('trust_proxy'), 84 TRUST_PROXY: config.get<string[]>('trust_proxy'),
77 LOG: { 85 LOG: {
78 LEVEL: config.get<string>('log.level') 86 LEVEL: config.get<string>('log.level'),
87 ROTATION: config.get<boolean>('log.rotation.enabled')
79 }, 88 },
80 SEARCH: { 89 SEARCH: {
81 REMOTE_URI: { 90 REMOTE_URI: {
@@ -140,13 +149,15 @@ const CONFIG = {
140 TRANSCODING: { 149 TRANSCODING: {
141 get ENABLED () { return config.get<boolean>('transcoding.enabled') }, 150 get ENABLED () { return config.get<boolean>('transcoding.enabled') },
142 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') }, 151 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
152 get ALLOW_AUDIO_FILES () { return config.get<boolean>('transcoding.allow_audio_files') },
143 get THREADS () { return config.get<number>('transcoding.threads') }, 153 get THREADS () { return config.get<number>('transcoding.threads') },
144 RESOLUTIONS: { 154 RESOLUTIONS: {
145 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') }, 155 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
146 get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') }, 156 get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') },
147 get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') }, 157 get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') },
148 get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') }, 158 get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') },
149 get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') } 159 get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') },
160 get '2160p' () { return config.get<boolean>('transcoding.resolutions.2160p') }
150 }, 161 },
151 HLS: { 162 HLS: {
152 get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') } 163 get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index b5f8fc0bc..c2b8eff95 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -1,10 +1,10 @@
1import { join } from 'path' 1import { join } from 'path'
2import { JobType, VideoRateType, VideoState } from '../../shared/models' 2import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
3import { ActivityPubActorType } from '../../shared/models/activitypub' 3import { ActivityPubActorType } from '../../shared/models/activitypub'
4import { FollowState } from '../../shared/models/actors' 4import { FollowState } from '../../shared/models/actors'
5import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' 5import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
6// Do not use barrels, remain constants as independent as possible 6// Do not use barrels, remain constants as independent as possible
7import { isTestInstance, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 7import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
8import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 8import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
9import { invert } from 'lodash' 9import { invert } from 'lodash'
10import { CronRepeatOptions, EveryRepeatOptions } from 'bull' 10import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 380 17const LAST_MIGRATION_VERSION = 390
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -228,7 +228,7 @@ let CONSTRAINTS_FIELDS = {
228 max: 2 * 1024 * 1024 // 2MB 228 max: 2 * 1024 * 1024 // 2MB
229 } 229 }
230 }, 230 },
231 EXTNAME: buildVideosExtname(), 231 EXTNAME: [] as string[],
232 INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2 232 INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
233 DURATION: { min: 0 }, // Number 233 DURATION: { min: 0 }, // Number
234 TAGS: { min: 0, max: 5 }, // Number of total tags 234 TAGS: { min: 0, max: 5 }, // Number of total tags
@@ -300,6 +300,8 @@ const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
300 KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) 300 KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
301} 301}
302 302
303const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
304
303const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = { 305const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
304 LIKE: 'like', 306 LIKE: 'like',
305 DISLIKE: 'dislike' 307 DISLIKE: 'dislike'
@@ -380,8 +382,18 @@ const VIDEO_PLAYLIST_TYPES = {
380} 382}
381 383
382const MIMETYPES = { 384const MIMETYPES = {
385 AUDIO: {
386 MIMETYPE_EXT: {
387 'audio/mpeg': '.mp3',
388 'audio/mp3': '.mp3',
389 'application/ogg': '.ogg',
390 'audio/ogg': '.ogg',
391 'audio/flac': '.flac'
392 },
393 EXT_MIMETYPE: null as { [ id: string ]: string }
394 },
383 VIDEO: { 395 VIDEO: {
384 MIMETYPE_EXT: buildVideoMimetypeExt(), 396 MIMETYPE_EXT: null as { [ id: string ]: string },
385 EXT_MIMETYPE: null as { [ id: string ]: string } 397 EXT_MIMETYPE: null as { [ id: string ]: string }
386 }, 398 },
387 IMAGE: { 399 IMAGE: {
@@ -403,7 +415,7 @@ const MIMETYPES = {
403 } 415 }
404 } 416 }
405} 417}
406MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) 418MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT)
407 419
408// --------------------------------------------------------------------------- 420// ---------------------------------------------------------------------------
409 421
@@ -429,7 +441,7 @@ const ACTIVITY_PUB = {
429 COLLECTION_ITEMS_PER_PAGE: 10, 441 COLLECTION_ITEMS_PER_PAGE: 10,
430 FETCH_PAGE_LIMIT: 100, 442 FETCH_PAGE_LIMIT: 100,
431 URL_MIME_TYPES: { 443 URL_MIME_TYPES: {
432 VIDEO: Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT), 444 VIDEO: [] as string[],
433 TORRENT: [ 'application/x-bittorrent' ], 445 TORRENT: [ 'application/x-bittorrent' ],
434 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] 446 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
435 }, 447 },
@@ -497,8 +509,8 @@ const THUMBNAILS_SIZE = {
497 height: 122 509 height: 122
498} 510}
499const PREVIEWS_SIZE = { 511const PREVIEWS_SIZE = {
500 width: 560, 512 width: 850,
501 height: 315 513 height: 480
502} 514}
503const AVATARS_SIZE = { 515const AVATARS_SIZE = {
504 width: 120, 516 width: 120,
@@ -543,6 +555,10 @@ const REDUNDANCY = {
543 555
544const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS) 556const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
545 557
558const ASSETS_PATH = {
559 DEFAULT_AUDIO_BACKGROUND: join(root(), 'server', 'assets', 'default-audio-background.jpg')
560}
561
546// --------------------------------------------------------------------------- 562// ---------------------------------------------------------------------------
547 563
548const CUSTOM_HTML_TAG_COMMENTS = { 564const CUSTOM_HTML_TAG_COMMENTS = {
@@ -612,6 +628,7 @@ if (isTestInstance() === true) {
612} 628}
613 629
614updateWebserverUrls() 630updateWebserverUrls()
631updateWebserverConfig()
615 632
616registerConfigChangedHandler(() => { 633registerConfigChangedHandler(() => {
617 updateWebserverUrls() 634 updateWebserverUrls()
@@ -681,12 +698,14 @@ export {
681 RATES_LIMIT, 698 RATES_LIMIT,
682 MIMETYPES, 699 MIMETYPES,
683 CRAWL_REQUEST_CONCURRENCY, 700 CRAWL_REQUEST_CONCURRENCY,
701 DEFAULT_AUDIO_RESOLUTION,
684 JOB_COMPLETED_LIFETIME, 702 JOB_COMPLETED_LIFETIME,
685 HTTP_SIGNATURE, 703 HTTP_SIGNATURE,
686 VIDEO_IMPORT_STATES, 704 VIDEO_IMPORT_STATES,
687 VIDEO_VIEW_LIFETIME, 705 VIDEO_VIEW_LIFETIME,
688 CONTACT_FORM_LIFETIME, 706 CONTACT_FORM_LIFETIME,
689 VIDEO_PLAYLIST_PRIVACIES, 707 VIDEO_PLAYLIST_PRIVACIES,
708 ASSETS_PATH,
690 loadLanguages, 709 loadLanguages,
691 buildLanguages 710 buildLanguages
692} 711}
@@ -700,15 +719,21 @@ function buildVideoMimetypeExt () {
700 'video/mp4': '.mp4' 719 'video/mp4': '.mp4'
701 } 720 }
702 721
703 if (CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) { 722 if (CONFIG.TRANSCODING.ENABLED) {
704 Object.assign(data, { 723 if (CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) {
705 'video/quicktime': '.mov', 724 Object.assign(data, {
706 'video/x-msvideo': '.avi', 725 'video/quicktime': '.mov',
707 'video/x-flv': '.flv', 726 'video/x-msvideo': '.avi',
708 'video/x-matroska': '.mkv', 727 'video/x-flv': '.flv',
709 'application/octet-stream': '.mkv', 728 'video/x-matroska': '.mkv',
710 'video/avi': '.avi' 729 'application/octet-stream': '.mkv',
711 }) 730 'video/avi': '.avi'
731 })
732 }
733
734 if (CONFIG.TRANSCODING.ALLOW_AUDIO_FILES) {
735 Object.assign(data, MIMETYPES.AUDIO.MIMETYPE_EXT)
736 }
712 } 737 }
713 738
714 return data 739 return data
@@ -724,16 +749,15 @@ function updateWebserverUrls () {
724} 749}
725 750
726function updateWebserverConfig () { 751function updateWebserverConfig () {
727 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname()
728
729 MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt() 752 MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt()
730 MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT) 753 MIMETYPES.VIDEO.EXT_MIMETYPE = invert(MIMETYPES.VIDEO.MIMETYPE_EXT)
754 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
755
756 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = buildVideosExtname()
731} 757}
732 758
733function buildVideosExtname () { 759function buildVideosExtname () {
734 return CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS 760 return Object.keys(MIMETYPES.VIDEO.EXT_MIMETYPE)
735 ? [ '.mp4', '.ogv', '.webm', '.mkv', '.mov', '.avi', '.flv' ]
736 : [ '.mp4', '.ogv', '.webm' ]
737} 761}
738 762
739function loadLanguages () { 763function loadLanguages () {
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 127449577..cb58454cb 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -128,6 +128,8 @@ async function createOAuthAdminIfNotExist () {
128 128
129 // Our password is weak so do not validate it 129 // Our password is weak so do not validate it
130 validatePassword = false 130 validatePassword = false
131 } else if (process.env.PT_INITIAL_ROOT_PASSWORD) {
132 password = process.env.PT_INITIAL_ROOT_PASSWORD
131 } else { 133 } else {
132 password = passwordGenerator(16, true) 134 password = passwordGenerator(16, true)
133 } 135 }
@@ -144,7 +146,7 @@ async function createOAuthAdminIfNotExist () {
144 } 146 }
145 const user = new UserModel(userData) 147 const user = new UserModel(userData)
146 148
147 await createUserAccountAndChannelAndPlaylist(user, validatePassword) 149 await createUserAccountAndChannelAndPlaylist({ userToCreate: user, channelNames: undefined, validateUser: validatePassword })
148 logger.info('Username: ' + username) 150 logger.info('Username: ' + username)
149 logger.info('User password: ' + password) 151 logger.info('User password: ' + password)
150} 152}
diff --git a/server/initializers/migrations/0100-activitypub.ts b/server/initializers/migrations/0100-activitypub.ts
index 2880a97d9..96d44a7ce 100644
--- a/server/initializers/migrations/0100-activitypub.ts
+++ b/server/initializers/migrations/0100-activitypub.ts
@@ -65,7 +65,12 @@ async function up (utils: {
65 // Create application account 65 // Create application account
66 { 66 {
67 const applicationInstance = await ApplicationModel.findOne() 67 const applicationInstance = await ApplicationModel.findOne()
68 const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationInstance.id, undefined) 68 const accountCreated = await createLocalAccountWithoutKeys({
69 name: SERVER_ACTOR_NAME,
70 userId: null,
71 applicationId: applicationInstance.id,
72 t: undefined
73 })
69 74
70 const { publicKey, privateKey } = await createPrivateAndPublicKeys() 75 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
71 accountCreated.Actor.publicKey = publicKey 76 accountCreated.Actor.publicKey = publicKey
@@ -83,7 +88,7 @@ async function up (utils: {
83 // Recreate accounts for each user 88 // Recreate accounts for each user
84 const users = await db.User.findAll() 89 const users = await db.User.findAll()
85 for (const user of users) { 90 for (const user of users) {
86 const account = await createLocalAccountWithoutKeys(user.username, user.id, null, undefined) 91 const account = await createLocalAccountWithoutKeys({ name: user.username, userId: user.id, applicationId: null, t: undefined })
87 92
88 const { publicKey, privateKey } = await createPrivateAndPublicKeys() 93 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
89 account.Actor.publicKey = publicKey 94 account.Actor.publicKey = publicKey
diff --git a/server/initializers/migrations/0385-remove-actor-uuid.ts b/server/initializers/migrations/0385-remove-actor-uuid.ts
new file mode 100644
index 000000000..032c0562b
--- /dev/null
+++ b/server/initializers/migrations/0385-remove-actor-uuid.ts
@@ -0,0 +1,19 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 await utils.queryInterface.removeColumn('actor', 'uuid')
10}
11
12function down (options) {
13 throw new Error('Not implemented.')
14}
15
16export {
17 up,
18 down
19}
diff --git a/server/initializers/migrations/0390-user-pending-email.ts b/server/initializers/migrations/0390-user-pending-email.ts
new file mode 100644
index 000000000..5ca871746
--- /dev/null
+++ b/server/initializers/migrations/0390-user-pending-email.ts
@@ -0,0 +1,25 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 const data = {
10 type: Sequelize.STRING(400),
11 allowNull: true,
12 defaultValue: null
13 }
14
15 await utils.queryInterface.addColumn('user', 'pendingEmail', data)
16}
17
18function down (options) {
19 throw new Error('Not implemented.')
20}
21
22export {
23 up,
24 down
25}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 25cd40905..38eb87d1e 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -5,7 +5,7 @@ import * as uuidv4 from 'uuid/v4'
5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
8import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' 8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
@@ -33,7 +33,7 @@ function setAsyncActorKeys (actor: ActorModel) {
33 return actor.save() 33 return actor.save()
34 }) 34 })
35 .catch(err => { 35 .catch(err => {
36 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err }) 36 logger.error('Cannot set public/private keys of actor %d.', actor.url, { err })
37 return actor 37 return actor
38 }) 38 })
39} 39}
@@ -128,18 +128,17 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
128 const followersCount = await fetchActorTotalItems(attributes.followers) 128 const followersCount = await fetchActorTotalItems(attributes.followers)
129 const followingCount = await fetchActorTotalItems(attributes.following) 129 const followingCount = await fetchActorTotalItems(attributes.following)
130 130
131 actorInstance.set('type', attributes.type) 131 actorInstance.type = attributes.type
132 actorInstance.set('uuid', attributes.uuid) 132 actorInstance.preferredUsername = attributes.preferredUsername
133 actorInstance.set('preferredUsername', attributes.preferredUsername) 133 actorInstance.url = attributes.id
134 actorInstance.set('url', attributes.id) 134 actorInstance.publicKey = attributes.publicKey.publicKeyPem
135 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem) 135 actorInstance.followersCount = followersCount
136 actorInstance.set('followersCount', followersCount) 136 actorInstance.followingCount = followingCount
137 actorInstance.set('followingCount', followingCount) 137 actorInstance.inboxUrl = attributes.inbox
138 actorInstance.set('inboxUrl', attributes.inbox) 138 actorInstance.outboxUrl = attributes.outbox
139 actorInstance.set('outboxUrl', attributes.outbox) 139 actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox
140 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox) 140 actorInstance.followersUrl = attributes.followers
141 actorInstance.set('followersUrl', attributes.followers) 141 actorInstance.followingUrl = attributes.following
142 actorInstance.set('followingUrl', attributes.following)
143} 142}
144 143
145async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) { 144async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
@@ -370,10 +369,9 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
370 logger.info('Fetching remote actor %s.', actorUrl) 369 logger.info('Fetching remote actor %s.', actorUrl)
371 370
372 const requestResult = await doRequest<ActivityPubActor>(options) 371 const requestResult = await doRequest<ActivityPubActor>(options)
373 normalizeActor(requestResult.body)
374
375 const actorJSON = requestResult.body 372 const actorJSON = requestResult.body
376 if (isActorObjectValid(actorJSON) === false) { 373
374 if (sanitizeAndCheckActorObject(actorJSON) === false) {
377 logger.debug('Remote actor JSON is not valid.', { actorJSON }) 375 logger.debug('Remote actor JSON is not valid.', { actorJSON })
378 return { result: undefined, statusCode: requestResult.response.statusCode } 376 return { result: undefined, statusCode: requestResult.response.statusCode }
379 } 377 }
@@ -388,7 +386,6 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
388 386
389 const actor = new ActorModel({ 387 const actor = new ActorModel({
390 type: actorJSON.type, 388 type: actorJSON.type,
391 uuid: actorJSON.uuid,
392 preferredUsername: actorJSON.preferredUsername, 389 preferredUsername: actorJSON.preferredUsername,
393 url: actorJSON.id, 390 url: actorJSON.id,
394 publicKey: actorJSON.publicKey.publicKeyPem, 391 publicKey: actorJSON.publicKey.publicKeyPem,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 686eef04d..9e469e3e6 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -28,13 +28,22 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
28 let i = 0 28 let i = 0
29 let nextLink = firstBody.first 29 let nextLink = firstBody.first
30 while (nextLink && i < limit) { 30 while (nextLink && i < limit) {
31 // Don't crawl ourselves 31 let body: any
32 const remoteHost = parse(nextLink).host
33 if (remoteHost === WEBSERVER.HOST) continue
34 32
35 options.uri = nextLink 33 if (typeof nextLink === 'string') {
34 // Don't crawl ourselves
35 const remoteHost = parse(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue
37
38 options.uri = nextLink
39
40 const res = await doRequest<ActivityPubOrderedCollection<T>>(options)
41 body = res.body
42 } else {
43 // nextLink is already the object we want
44 body = nextLink
45 }
36 46
37 const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
38 nextLink = body.next 47 nextLink = body.next
39 i++ 48 i++
40 49
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index 23310b41e..bbf1bd3a8 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -5,8 +5,9 @@ import { ActorModel } from '../../../models/activitypub/actor'
5import { VideoShareModel } from '../../../models/video/video-share' 5import { VideoShareModel } from '../../../models/video/video-share'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { VideoPrivacy } from '../../../../shared/models/videos'
9import { Notifier } from '../../notifier' 8import { Notifier } from '../../notifier'
9import { VideoModel } from '../../../models/video/video'
10import { logger } from '../../../helpers/logger'
10 11
11async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { 12async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) {
12 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) 13 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity)
@@ -23,7 +24,17 @@ export {
23async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { 24async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
24 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id 25 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
25 26
26 const { video, created: videoCreated } = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri }) 27 let video: VideoModel
28 let videoCreated: boolean
29
30 try {
31 const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri })
32 video = result.video
33 videoCreated = result.created
34 } catch (err) {
35 logger.debug('Cannot process share of %s. Maybe this is not a video object, so just skipping.', objectUri, { err })
36 return
37 }
27 38
28 await sequelizeTypescript.transaction(async t => { 39 await sequelizeTypescript.transaction(async t => {
29 // Add share entry 40 // Add share entry
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index e882669ce..daf846513 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -9,28 +9,14 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
9import { forwardVideoRelatedActivity } from '../send/utils' 9import { forwardVideoRelatedActivity } from '../send/utils'
10import { createOrUpdateCacheFile } from '../cache-file' 10import { createOrUpdateCacheFile } from '../cache-file'
11import { Notifier } from '../../notifier' 11import { Notifier } from '../../notifier'
12import { processViewActivity } from './process-view'
13import { processDislikeActivity } from './process-dislike'
14import { processFlagActivity } from './process-flag'
15import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' 12import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
16import { createOrUpdateVideoPlaylist } from '../playlist' 13import { createOrUpdateVideoPlaylist } from '../playlist'
14import { VideoModel } from '../../../models/video/video'
17 15
18async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 16async 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') {
23 return processViewActivity(activity, byActor)
24 }
25
26 if (activityType === 'Dislike') {
27 return retryTransactionWrapper(processDislikeActivity, activity, byActor)
28 }
29
30 if (activityType === 'Flag') {
31 return retryTransactionWrapper(processFlagActivity, activity, byActor)
32 }
33
34 if (activityType === 'Video') { 20 if (activityType === 'Video') {
35 return processCreateVideo(activity) 21 return processCreateVideo(activity)
36 } 22 }
@@ -91,7 +77,18 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: Act
91 77
92 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) 78 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
93 79
94 const { video } = await resolveThread(commentObject.inReplyTo) 80 let video: VideoModel
81 try {
82 const resolveThreadResult = await resolveThread(commentObject.inReplyTo)
83 video = resolveThreadResult.video
84 } catch (err) {
85 logger.debug(
86 'Cannot process video comment because we could not resolve thread %s. Maybe it was not a video thread, so skip it.',
87 commentObject.inReplyTo,
88 { err }
89 )
90 return
91 }
95 92
96 const { comment, created } = await addVideoComment(video, commentObject.id) 93 const { comment, created } = await addVideoComment(video, commentObject.id)
97 94
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index 76f07fd8a..6f10a50bd 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -95,23 +95,23 @@ async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete:
95} 95}
96 96
97async function processDeleteAccount (accountToRemove: AccountModel) { 97async function processDeleteAccount (accountToRemove: AccountModel) {
98 logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid) 98 logger.debug('Removing remote account "%s".', accountToRemove.Actor.url)
99 99
100 await sequelizeTypescript.transaction(async t => { 100 await sequelizeTypescript.transaction(async t => {
101 await accountToRemove.destroy({ transaction: t }) 101 await accountToRemove.destroy({ transaction: t })
102 }) 102 })
103 103
104 logger.info('Remote account with uuid %s removed.', accountToRemove.Actor.uuid) 104 logger.info('Remote account %s removed.', accountToRemove.Actor.url)
105} 105}
106 106
107async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelModel) { 107async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelModel) {
108 logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.uuid) 108 logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.url)
109 109
110 await sequelizeTypescript.transaction(async t => { 110 await sequelizeTypescript.transaction(async t => {
111 await videoChannelToRemove.destroy({ transaction: t }) 111 await videoChannelToRemove.destroy({ transaction: t })
112 }) 112 })
113 113
114 logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.Actor.uuid) 114 logger.info('Remote video channel %s removed.', videoChannelToRemove.Actor.url)
115} 115}
116 116
117function processDeleteVideoComment (byActor: ActorModel, videoComment: VideoCommentModel, activity: ActivityDelete) { 117function processDeleteVideoComment (byActor: ActorModel, videoComment: VideoCommentModel, activity: ActivityDelete) {
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 54a9234bb..71a16dacc 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -95,7 +95,7 @@ async function processUpdateCacheFile (byActor: ActorModel, activity: ActivityUp
95async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) { 95async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) {
96 const actorAttributesToUpdate = activity.object as ActivityPubActor 96 const actorAttributesToUpdate = activity.object as ActivityPubActor
97 97
98 logger.debug('Updating remote account "%s".', actorAttributesToUpdate.uuid) 98 logger.debug('Updating remote account "%s".', actorAttributesToUpdate.url)
99 let accountOrChannelInstance: AccountModel | VideoChannelModel 99 let accountOrChannelInstance: AccountModel | VideoChannelModel
100 let actorFieldsSave: object 100 let actorFieldsSave: object
101 let accountOrChannelFieldsSave: object 101 let accountOrChannelFieldsSave: object
@@ -128,7 +128,7 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
128 await accountOrChannelInstance.save({ transaction: t }) 128 await accountOrChannelInstance.save({ transaction: t })
129 }) 129 })
130 130
131 logger.info('Remote account with uuid %s updated', actorAttributesToUpdate.uuid) 131 logger.info('Remote account %s updated', actorAttributesToUpdate.url)
132 } catch (err) { 132 } catch (err) {
133 if (actor !== undefined && actorFieldsSave !== undefined) { 133 if (actor !== undefined && actorFieldsSave !== undefined) {
134 resetSequelizeInstance(actor, actorFieldsSave) 134 resetSequelizeInstance(actor, actorFieldsSave)
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index 18f44d50e..c3fc6b462 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -80,7 +80,8 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
80 return { comment, created } 80 return { comment, created }
81} 81}
82 82
83async function resolveThread (url: string, comments: VideoCommentModel[] = []) { 83type ResolveThreadResult = Promise<{ video: VideoModel, parents: VideoCommentModel[] }>
84async function resolveThread (url: string, comments: VideoCommentModel[] = []): ResolveThreadResult {
84 // Already have this comment? 85 // Already have this comment?
85 const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideo(url) 86 const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideo(url)
86 if (commentFromDatabase) { 87 if (commentFromDatabase) {
@@ -161,7 +162,6 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
161 162
162 return resolveThread(body.inReplyTo, comments.concat([ comment ])) 163 return resolveThread(body.inReplyTo, comments.concat([ comment ]))
163 } 164 }
164
165} 165}
166 166
167export { 167export {
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 8c06e9751..c4a5a5853 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -100,11 +100,11 @@ class Emailer {
100 `You can view it on ${videoUrl} ` + 100 `You can view it on ${videoUrl} ` +
101 `\n\n` + 101 `\n\n` +
102 `Cheers,\n` + 102 `Cheers,\n` +
103 `PeerTube.` 103 `${CONFIG.EMAIL.BODY.SIGNATURE}`
104 104
105 const emailPayload: EmailPayload = { 105 const emailPayload: EmailPayload = {
106 to, 106 to,
107 subject: channelName + ' just published a new video', 107 subject: CONFIG.EMAIL.OBJECT.PREFIX + channelName + ' just published a new video',
108 text 108 text
109 } 109 }
110 110
@@ -119,11 +119,11 @@ class Emailer {
119 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` + 119 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
120 `\n\n` + 120 `\n\n` +
121 `Cheers,\n` + 121 `Cheers,\n` +
122 `PeerTube.` 122 `${CONFIG.EMAIL.BODY.SIGNATURE}`
123 123
124 const emailPayload: EmailPayload = { 124 const emailPayload: EmailPayload = {
125 to, 125 to,
126 subject: 'New follower on your channel ' + followingName, 126 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New follower on your channel ' + followingName,
127 text 127 text
128 } 128 }
129 129
@@ -137,11 +137,11 @@ class Emailer {
137 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` + 137 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
138 `\n\n` + 138 `\n\n` +
139 `Cheers,\n` + 139 `Cheers,\n` +
140 `PeerTube.` 140 `${CONFIG.EMAIL.BODY.SIGNATURE}`
141 141
142 const emailPayload: EmailPayload = { 142 const emailPayload: EmailPayload = {
143 to, 143 to,
144 subject: 'New instance follower', 144 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New instance follower',
145 text 145 text
146 } 146 }
147 147
@@ -157,11 +157,11 @@ class Emailer {
157 `You can view it on ${videoUrl} ` + 157 `You can view it on ${videoUrl} ` +
158 `\n\n` + 158 `\n\n` +
159 `Cheers,\n` + 159 `Cheers,\n` +
160 `PeerTube.` 160 `${CONFIG.EMAIL.BODY.SIGNATURE}`
161 161
162 const emailPayload: EmailPayload = { 162 const emailPayload: EmailPayload = {
163 to, 163 to,
164 subject: `Your video ${video.name} is published`, 164 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video ${video.name} is published`,
165 text 165 text
166 } 166 }
167 167
@@ -177,11 +177,11 @@ class Emailer {
177 `You can view the imported video on ${videoUrl} ` + 177 `You can view the imported video on ${videoUrl} ` +
178 `\n\n` + 178 `\n\n` +
179 `Cheers,\n` + 179 `Cheers,\n` +
180 `PeerTube.` 180 `${CONFIG.EMAIL.BODY.SIGNATURE}`
181 181
182 const emailPayload: EmailPayload = { 182 const emailPayload: EmailPayload = {
183 to, 183 to,
184 subject: `Your video import ${videoImport.getTargetIdentifier()} is finished`, 184 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`,
185 text 185 text
186 } 186 }
187 187
@@ -197,11 +197,11 @@ class Emailer {
197 `See your videos import dashboard for more information: ${importUrl}` + 197 `See your videos import dashboard for more information: ${importUrl}` +
198 `\n\n` + 198 `\n\n` +
199 `Cheers,\n` + 199 `Cheers,\n` +
200 `PeerTube.` 200 `${CONFIG.EMAIL.BODY.SIGNATURE}`
201 201
202 const emailPayload: EmailPayload = { 202 const emailPayload: EmailPayload = {
203 to, 203 to,
204 subject: `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, 204 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`,
205 text 205 text
206 } 206 }
207 207
@@ -219,11 +219,11 @@ class Emailer {
219 `You can view it on ${commentUrl} ` + 219 `You can view it on ${commentUrl} ` +
220 `\n\n` + 220 `\n\n` +
221 `Cheers,\n` + 221 `Cheers,\n` +
222 `PeerTube.` 222 `${CONFIG.EMAIL.BODY.SIGNATURE}`
223 223
224 const emailPayload: EmailPayload = { 224 const emailPayload: EmailPayload = {
225 to, 225 to,
226 subject: 'New comment on your video ' + video.name, 226 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New comment on your video ' + video.name,
227 text 227 text
228 } 228 }
229 229
@@ -241,11 +241,11 @@ class Emailer {
241 `You can view the comment on ${commentUrl} ` + 241 `You can view the comment on ${commentUrl} ` +
242 `\n\n` + 242 `\n\n` +
243 `Cheers,\n` + 243 `Cheers,\n` +
244 `PeerTube.` 244 `${CONFIG.EMAIL.BODY.SIGNATURE}`
245 245
246 const emailPayload: EmailPayload = { 246 const emailPayload: EmailPayload = {
247 to, 247 to,
248 subject: 'Mention on video ' + video.name, 248 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Mention on video ' + video.name,
249 text 249 text
250 } 250 }
251 251
@@ -258,11 +258,11 @@ class Emailer {
258 const text = `Hi,\n\n` + 258 const text = `Hi,\n\n` +
259 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + 259 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` +
260 `Cheers,\n` + 260 `Cheers,\n` +
261 `PeerTube.` 261 `${CONFIG.EMAIL.BODY.SIGNATURE}`
262 262
263 const emailPayload: EmailPayload = { 263 const emailPayload: EmailPayload = {
264 to, 264 to,
265 subject: '[PeerTube] Received a video abuse', 265 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Received a video abuse',
266 text 266 text
267 } 267 }
268 268
@@ -281,11 +281,11 @@ class Emailer {
281 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` + 281 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
282 `\n\n` + 282 `\n\n` +
283 `Cheers,\n` + 283 `Cheers,\n` +
284 `PeerTube.` 284 `${CONFIG.EMAIL.BODY.SIGNATURE}`
285 285
286 const emailPayload: EmailPayload = { 286 const emailPayload: EmailPayload = {
287 to, 287 to,
288 subject: '[PeerTube] An auto-blacklisted video is awaiting review', 288 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'An auto-blacklisted video is awaiting review',
289 text 289 text
290 } 290 }
291 291
@@ -296,11 +296,11 @@ class Emailer {
296 const text = `Hi,\n\n` + 296 const text = `Hi,\n\n` +
297 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` + 297 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
298 `Cheers,\n` + 298 `Cheers,\n` +
299 `PeerTube.` 299 `${CONFIG.EMAIL.BODY.SIGNATURE}`
300 300
301 const emailPayload: EmailPayload = { 301 const emailPayload: EmailPayload = {
302 to, 302 to,
303 subject: '[PeerTube] New user registration on ' + WEBSERVER.HOST, 303 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST,
304 text 304 text
305 } 305 }
306 306
@@ -318,11 +318,11 @@ class Emailer {
318 blockedString + 318 blockedString +
319 '\n\n' + 319 '\n\n' +
320 'Cheers,\n' + 320 'Cheers,\n' +
321 `PeerTube.` 321 `${CONFIG.EMAIL.BODY.SIGNATURE}`
322 322
323 const emailPayload: EmailPayload = { 323 const emailPayload: EmailPayload = {
324 to, 324 to,
325 subject: `[PeerTube] Video ${videoName} blacklisted`, 325 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Video ${videoName} blacklisted`,
326 text 326 text
327 } 327 }
328 328
@@ -336,11 +336,11 @@ class Emailer {
336 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` + 336 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
337 '\n\n' + 337 '\n\n' +
338 'Cheers,\n' + 338 'Cheers,\n' +
339 `PeerTube.` 339 `${CONFIG.EMAIL.BODY.SIGNATURE}`
340 340
341 const emailPayload: EmailPayload = { 341 const emailPayload: EmailPayload = {
342 to, 342 to,
343 subject: `[PeerTube] Video ${video.name} unblacklisted`, 343 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Video ${video.name} unblacklisted`,
344 text 344 text
345 } 345 }
346 346
@@ -353,11 +353,11 @@ class Emailer {
353 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + 353 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` +
354 `If you are not the person who initiated this request, please ignore this email.\n\n` + 354 `If you are not the person who initiated this request, please ignore this email.\n\n` +
355 `Cheers,\n` + 355 `Cheers,\n` +
356 `PeerTube.` 356 `${CONFIG.EMAIL.BODY.SIGNATURE}`
357 357
358 const emailPayload: EmailPayload = { 358 const emailPayload: EmailPayload = {
359 to: [ to ], 359 to: [ to ],
360 subject: 'Reset your PeerTube password', 360 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Reset your password',
361 text 361 text
362 } 362 }
363 363
@@ -370,11 +370,11 @@ class Emailer {
370 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + 370 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
371 `If you are not the person who initiated this request, please ignore this email.\n\n` + 371 `If you are not the person who initiated this request, please ignore this email.\n\n` +
372 `Cheers,\n` + 372 `Cheers,\n` +
373 `PeerTube.` 373 `${CONFIG.EMAIL.BODY.SIGNATURE}`
374 374
375 const emailPayload: EmailPayload = { 375 const emailPayload: EmailPayload = {
376 to: [ to ], 376 to: [ to ],
377 subject: 'Verify your PeerTube email', 377 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Verify your email',
378 text 378 text
379 } 379 }
380 380
@@ -390,12 +390,12 @@ class Emailer {
390 blockedString + 390 blockedString +
391 '\n\n' + 391 '\n\n' +
392 'Cheers,\n' + 392 'Cheers,\n' +
393 `PeerTube.` 393 `${CONFIG.EMAIL.BODY.SIGNATURE}`
394 394
395 const to = user.email 395 const to = user.email
396 const emailPayload: EmailPayload = { 396 const emailPayload: EmailPayload = {
397 to: [ to ], 397 to: [ to ],
398 subject: '[PeerTube] Account ' + blockedWord, 398 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Account ' + blockedWord,
399 text 399 text
400 } 400 }
401 401
@@ -415,7 +415,7 @@ class Emailer {
415 fromDisplayName: fromEmail, 415 fromDisplayName: fromEmail,
416 replyTo: fromEmail, 416 replyTo: fromEmail,
417 to: [ CONFIG.ADMIN.EMAIL ], 417 to: [ CONFIG.ADMIN.EMAIL ],
418 subject: '[PeerTube] Contact form submitted', 418 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Contact form submitted',
419 text 419 text
420 } 420 }
421 421
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index 14be7f24a..a68619d07 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -21,7 +21,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
21 const video = await VideoModel.loadByUUIDWithFile(videoUUID) 21 const video = await VideoModel.loadByUUIDWithFile(videoUUID)
22 if (!video) return undefined 22 if (!video) return undefined
23 23
24 if (video.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) } 24 if (video.isOwned()) return { isOwned: true, path: video.getPreview().getPath() }
25 25
26 return this.loadRemoteFile(videoUUID) 26 return this.loadRemoteFile(videoUUID)
27 } 27 }
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 921d9a083..8cacb0ef3 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -1,7 +1,7 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { VideoModel } from '../../../models/video/video' 3import { VideoModel } from '../../../models/video/video'
4import { publishVideoIfNeeded } from './video-transcoding' 4import { publishNewResolutionIfNeeded } from './video-transcoding'
5import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 5import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
6import { copy, stat } from 'fs-extra' 6import { copy, stat } from 'fs-extra'
7import { VideoFileModel } from '../../../models/video/video-file' 7import { VideoFileModel } from '../../../models/video/video-file'
@@ -25,7 +25,7 @@ async function processVideoFileImport (job: Bull.Job) {
25 25
26 await updateVideoFile(video, payload.filePath) 26 await updateVideoFile(video, payload.filePath)
27 27
28 await publishVideoIfNeeded(video) 28 await publishNewResolutionIfNeeded(video)
29 return video 29 return video
30} 30}
31 31
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 1650916a6..50e159245 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -209,6 +209,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
209 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { 209 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
210 // Put uuid because we don't have id auto incremented for now 210 // Put uuid because we don't have id auto incremented for now
211 const dataInput = { 211 const dataInput = {
212 type: 'optimize' as 'optimize',
212 videoUUID: videoImportUpdated.Video.uuid, 213 videoUUID: videoImportUpdated.Video.uuid,
213 isNewVideo: true 214 isNewVideo: true
214 } 215 }
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 48cac517e..e9b84ecd6 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -8,18 +8,39 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' 11import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding'
12import { Notifier } from '../../notifier' 12import { Notifier } from '../../notifier'
13import { CONFIG } from '../../../initializers/config' 13import { CONFIG } from '../../../initializers/config'
14 14
15export type VideoTranscodingPayload = { 15interface BaseTranscodingPayload {
16 videoUUID: string 16 videoUUID: string
17 resolution?: VideoResolution
18 isNewVideo?: boolean 17 isNewVideo?: boolean
18}
19
20interface HLSTranscodingPayload extends BaseTranscodingPayload {
21 type: 'hls'
22 isPortraitMode?: boolean
23 resolution: VideoResolution
24}
25
26interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {
27 type: 'new-resolution'
19 isPortraitMode?: boolean 28 isPortraitMode?: boolean
20 generateHlsPlaylist?: boolean 29 resolution: VideoResolution
30}
31
32interface MergeAudioTranscodingPayload extends BaseTranscodingPayload {
33 type: 'merge-audio'
34 resolution: VideoResolution
35}
36
37interface OptimizeTranscodingPayload extends BaseTranscodingPayload {
38 type: 'optimize'
21} 39}
22 40
41export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload
42 | OptimizeTranscodingPayload | MergeAudioTranscodingPayload
43
23async function processVideoTranscoding (job: Bull.Job) { 44async function processVideoTranscoding (job: Bull.Job) {
24 const payload = job.data as VideoTranscodingPayload 45 const payload = job.data as VideoTranscodingPayload
25 logger.info('Processing video file in job %d.', job.id) 46 logger.info('Processing video file in job %d.', job.id)
@@ -31,14 +52,18 @@ async function processVideoTranscoding (job: Bull.Job) {
31 return undefined 52 return undefined
32 } 53 }
33 54
34 if (payload.generateHlsPlaylist) { 55 if (payload.type === 'hls') {
35 await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false) 56 await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false)
36 57
37 await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video) 58 await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
38 } else if (payload.resolution) { // Transcoding in other resolution 59 } else if (payload.type === 'new-resolution') {
39 await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) 60 await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false)
40 61
41 await retryTransactionWrapper(publishVideoIfNeeded, video, payload) 62 await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
63 } else if (payload.type === 'merge-audio') {
64 await mergeAudioVideofile(video, payload.resolution)
65
66 await retryTransactionWrapper(publishNewResolutionIfNeeded, video, payload)
42 } else { 67 } else {
43 await optimizeVideofile(video) 68 await optimizeVideofile(video)
44 69
@@ -62,7 +87,7 @@ async function onHlsPlaylistGenerationSuccess (video: VideoModel) {
62 }) 87 })
63} 88}
64 89
65async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodingPayload) { 90async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
66 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { 91 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
67 // Maybe the video changed in database, refresh it 92 // Maybe the video changed in database, refresh it
68 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 93 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
@@ -94,7 +119,7 @@ async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodi
94 await createHlsJobIfEnabled(payload) 119 await createHlsJobIfEnabled(payload)
95} 120}
96 121
97async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoTranscodingPayload) { 122async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: OptimizeTranscodingPayload) {
98 if (videoArg === undefined) return undefined 123 if (videoArg === undefined) return undefined
99 124
100 // Outside the transaction (IO on disk) 125 // Outside the transaction (IO on disk)
@@ -120,6 +145,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video
120 145
121 for (const resolution of resolutionsEnabled) { 146 for (const resolution of resolutionsEnabled) {
122 const dataInput = { 147 const dataInput = {
148 type: 'new-resolution' as 'new-resolution',
123 videoUUID: videoDatabase.uuid, 149 videoUUID: videoDatabase.uuid,
124 resolution 150 resolution
125 } 151 }
@@ -149,27 +175,27 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: Video
149 if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) 175 if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
150 if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) 176 if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
151 177
152 await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })) 178 const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution })
179 await createHlsJobIfEnabled(hlsPayload)
153} 180}
154 181
155// --------------------------------------------------------------------------- 182// ---------------------------------------------------------------------------
156 183
157export { 184export {
158 processVideoTranscoding, 185 processVideoTranscoding,
159 publishVideoIfNeeded 186 publishNewResolutionIfNeeded
160} 187}
161 188
162// --------------------------------------------------------------------------- 189// ---------------------------------------------------------------------------
163 190
164function createHlsJobIfEnabled (payload?: VideoTranscodingPayload) { 191function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: number, isPortraitMode?: boolean }) {
165 // Generate HLS playlist? 192 // Generate HLS playlist?
166 if (payload && CONFIG.TRANSCODING.HLS.ENABLED) { 193 if (payload && CONFIG.TRANSCODING.HLS.ENABLED) {
167 const hlsTranscodingPayload = { 194 const hlsTranscodingPayload = {
195 type: 'hls' as 'hls',
168 videoUUID: payload.videoUUID, 196 videoUUID: payload.videoUUID,
169 resolution: payload.resolution, 197 resolution: payload.resolution,
170 isPortraitMode: payload.isPortraitMode, 198 isPortraitMode: payload.isPortraitMode
171
172 generateHlsPlaylist: true
173 } 199 }
174 200
175 return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) 201 return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index 950b14c3b..18bdcded4 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -1,7 +1,7 @@
1import { VideoFileModel } from '../models/video/video-file' 1import { VideoFileModel } from '../models/video/video-file'
2import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 2import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
3import { CONFIG } from '../initializers/config' 3import { CONFIG } from '../initializers/config'
4import { PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 4import { PREVIEWS_SIZE, THUMBNAILS_SIZE, ASSETS_PATH } from '../initializers/constants'
5import { VideoModel } from '../models/video/video' 5import { VideoModel } from '../models/video/video'
6import { ThumbnailModel } from '../models/video/thumbnail' 6import { ThumbnailModel } from '../models/video/thumbnail'
7import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 7import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
@@ -45,8 +45,10 @@ function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel,
45function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { 45function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
46 const input = video.getVideoFilePath(videoFile) 46 const input = video.getVideoFilePath(videoFile)
47 47
48 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type) 48 const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
49 const thumbnailCreator = () => generateImageFromVideoFile(input, basePath, filename, { height, width }) 49 const thumbnailCreator = videoFile.isAudio()
50 ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true)
51 : () => generateImageFromVideoFile(input, basePath, filename, { height, width })
50 52
51 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) 53 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
52} 54}
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 7badb3e72..0e4007770 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -1,7 +1,6 @@
1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4' 1import * as uuidv4 from 'uuid/v4'
3import { ActivityPubActorType } from '../../shared/models/activitypub' 2import { ActivityPubActorType } from '../../shared/models/activitypub'
4import { SERVER_ACTOR_NAME } from '../initializers/constants' 3import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
5import { AccountModel } from '../models/account/account' 4import { AccountModel } from '../models/account/account'
6import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
7import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' 6import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
@@ -12,8 +11,19 @@ import { UserNotificationSettingModel } from '../models/account/user-notificatio
12import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' 11import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
13import { createWatchLaterPlaylist } from './video-playlist' 12import { createWatchLaterPlaylist } from './video-playlist'
14import { sequelizeTypescript } from '../initializers/database' 13import { sequelizeTypescript } from '../initializers/database'
14import { Transaction } from 'sequelize/types'
15import { Redis } from './redis'
16import { Emailer } from './emailer'
17
18type ChannelNames = { name: string, displayName: string }
19async function createUserAccountAndChannelAndPlaylist (parameters: {
20 userToCreate: UserModel,
21 userDisplayName?: string,
22 channelNames?: ChannelNames,
23 validateUser?: boolean
24}) {
25 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
15 26
16async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, validateUser = true) {
17 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { 27 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
18 const userOptions = { 28 const userOptions = {
19 transaction: t, 29 transaction: t,
@@ -23,21 +33,17 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel,
23 const userCreated = await userToCreate.save(userOptions) 33 const userCreated = await userToCreate.save(userOptions)
24 userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t) 34 userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t)
25 35
26 const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) 36 const accountCreated = await createLocalAccountWithoutKeys({
37 name: userCreated.username,
38 displayName: userDisplayName,
39 userId: userCreated.id,
40 applicationId: null,
41 t: t
42 })
27 userCreated.Account = accountCreated 43 userCreated.Account = accountCreated
28 44
29 let channelName = userCreated.username + '_channel' 45 const channelAttributes = await buildChannelAttributes(userCreated, channelNames)
30 46 const videoChannel = await createVideoChannel(channelAttributes, accountCreated, t)
31 // Conflict, generate uuid instead
32 const actor = await ActorModel.loadLocalByName(channelName)
33 if (actor) channelName = uuidv4()
34
35 const videoChannelDisplayName = `Main ${userCreated.username} channel`
36 const videoChannelInfo = {
37 name: channelName,
38 displayName: videoChannelDisplayName
39 }
40 const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
41 47
42 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) 48 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
43 49
@@ -55,20 +61,22 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel,
55 return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel } 61 return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel }
56} 62}
57 63
58async function createLocalAccountWithoutKeys ( 64async function createLocalAccountWithoutKeys (parameters: {
59 name: string, 65 name: string,
66 displayName?: string,
60 userId: number | null, 67 userId: number | null,
61 applicationId: number | null, 68 applicationId: number | null,
62 t: Sequelize.Transaction | undefined, 69 t: Transaction | undefined,
63 type: ActivityPubActorType= 'Person' 70 type?: ActivityPubActorType
64) { 71}) {
72 const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters
65 const url = getAccountActivityPubUrl(name) 73 const url = getAccountActivityPubUrl(name)
66 74
67 const actorInstance = buildActorInstance(type, url, name) 75 const actorInstance = buildActorInstance(type, url, name)
68 const actorInstanceCreated = await actorInstance.save({ transaction: t }) 76 const actorInstanceCreated = await actorInstance.save({ transaction: t })
69 77
70 const accountInstance = new AccountModel({ 78 const accountInstance = new AccountModel({
71 name, 79 name: displayName || name,
72 userId, 80 userId,
73 applicationId, 81 applicationId,
74 actorId: actorInstanceCreated.id 82 actorId: actorInstanceCreated.id
@@ -81,24 +89,42 @@ async function createLocalAccountWithoutKeys (
81} 89}
82 90
83async function createApplicationActor (applicationId: number) { 91async function createApplicationActor (applicationId: number) {
84 const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationId, undefined, 'Application') 92 const accountCreated = await createLocalAccountWithoutKeys({
93 name: SERVER_ACTOR_NAME,
94 userId: null,
95 applicationId: applicationId,
96 t: undefined,
97 type: 'Application'
98 })
85 99
86 accountCreated.Actor = await setAsyncActorKeys(accountCreated.Actor) 100 accountCreated.Actor = await setAsyncActorKeys(accountCreated.Actor)
87 101
88 return accountCreated 102 return accountCreated
89} 103}
90 104
105async function sendVerifyUserEmail (user: UserModel, isPendingEmail = false) {
106 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
107 let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
108
109 if (isPendingEmail) url += '&isPendingEmail=true'
110
111 const email = isPendingEmail ? user.pendingEmail : user.email
112
113 await Emailer.Instance.addVerifyEmailJob(email, url)
114}
115
91// --------------------------------------------------------------------------- 116// ---------------------------------------------------------------------------
92 117
93export { 118export {
94 createApplicationActor, 119 createApplicationActor,
95 createUserAccountAndChannelAndPlaylist, 120 createUserAccountAndChannelAndPlaylist,
96 createLocalAccountWithoutKeys 121 createLocalAccountWithoutKeys,
122 sendVerifyUserEmail
97} 123}
98 124
99// --------------------------------------------------------------------------- 125// ---------------------------------------------------------------------------
100 126
101function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) { 127function createDefaultUserNotificationSettings (user: UserModel, t: Transaction | undefined) {
102 const values: UserNotificationSetting & { userId: number } = { 128 const values: UserNotificationSetting & { userId: number } = {
103 userId: user.id, 129 userId: user.id,
104 newVideoFromSubscription: UserNotificationSettingValue.WEB, 130 newVideoFromSubscription: UserNotificationSettingValue.WEB,
@@ -116,3 +142,20 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr
116 142
117 return UserNotificationSettingModel.create(values, { transaction: t }) 143 return UserNotificationSettingModel.create(values, { transaction: t })
118} 144}
145
146async function buildChannelAttributes (user: UserModel, channelNames?: ChannelNames) {
147 if (channelNames) return channelNames
148
149 let channelName = user.username + '_channel'
150
151 // Conflict, generate uuid instead
152 const actor = await ActorModel.loadLocalByName(channelName)
153 if (actor) channelName = uuidv4()
154
155 const videoChannelDisplayName = `Main ${user.username} channel`
156
157 return {
158 name: channelName,
159 displayName: videoChannelDisplayName
160 }
161}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 0fe95ca09..ee0482c36 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -3,7 +3,8 @@ import * as uuidv4 from 'uuid/v4'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { AccountModel } from '../models/account/account' 4import { AccountModel } from '../models/account/account'
5import { VideoChannelModel } from '../models/video/video-channel' 5import { VideoChannelModel } from '../models/video/video-channel'
6import { buildActorInstance, getVideoChannelActivityPubUrl } from './activitypub' 6import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub'
7import { VideoModel } from '../models/video/video'
7 8
8async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountModel, t: Sequelize.Transaction) { 9async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountModel, t: Sequelize.Transaction) {
9 const uuid = uuidv4() 10 const uuid = uuidv4()
@@ -33,8 +34,19 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
33 return videoChannelCreated 34 return videoChannelCreated
34} 35}
35 36
37async function federateAllVideosOfChannel (videoChannel: VideoChannelModel) {
38 const videoIds = await VideoModel.getAllIdsFromChannel(videoChannel)
39
40 for (const videoId of videoIds) {
41 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
42
43 await federateVideoIfNeeded(video, false)
44 }
45}
46
36// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
37 48
38export { 49export {
39 createVideoChannel 50 createVideoChannel,
51 federateAllVideosOfChannel
40} 52}
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 0fe0ff12a..8d786e0ef 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -1,6 +1,6 @@
1import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' 1import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
2import { join } from 'path' 2import { join } from 'path'
3import { getVideoFileFPS, transcode } from '../helpers/ffmpeg-utils' 3import { canDoQuickTranscode, getVideoFileFPS, transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
4import { ensureDir, move, remove, stat } from 'fs-extra' 4import { ensureDir, move, remove, stat } from 'fs-extra'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { VideoResolution } from '../../shared/models/videos' 6import { VideoResolution } from '../../shared/models/videos'
@@ -11,15 +11,24 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
11import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' 11import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
12import { CONFIG } from '../initializers/config' 12import { CONFIG } from '../initializers/config'
13 13
14/**
15 * Optimize the original video file and replace it. The resolution is not changed.
16 */
14async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { 17async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) {
15 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 18 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
19 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
16 const newExtname = '.mp4' 20 const newExtname = '.mp4'
17 21
18 const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile() 22 const inputVideoFile = inputVideoFileArg ? inputVideoFileArg : video.getOriginalFile()
19 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile)) 23 const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
20 const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname) 24 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
21 25
22 const transcodeOptions = { 26 const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath)
27 ? 'quick-transcode'
28 : 'video'
29
30 const transcodeOptions: TranscodeOptions = {
31 type: transcodeType as any, // FIXME: typing issue
23 inputPath: videoInputPath, 32 inputPath: videoInputPath,
24 outputPath: videoTranscodedPath, 33 outputPath: videoTranscodedPath,
25 resolution: inputVideoFile.resolution 34 resolution: inputVideoFile.resolution
@@ -32,18 +41,11 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
32 await remove(videoInputPath) 41 await remove(videoInputPath)
33 42
34 // Important to do this before getVideoFilename() to take in account the new file extension 43 // Important to do this before getVideoFilename() to take in account the new file extension
35 inputVideoFile.set('extname', newExtname) 44 inputVideoFile.extname = newExtname
36 45
37 const videoOutputPath = video.getVideoFilePath(inputVideoFile) 46 const videoOutputPath = video.getVideoFilePath(inputVideoFile)
38 await move(videoTranscodedPath, videoOutputPath)
39 const stats = await stat(videoOutputPath)
40 const fps = await getVideoFileFPS(videoOutputPath)
41 47
42 inputVideoFile.set('size', stats.size) 48 await onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
43 inputVideoFile.set('fps', fps)
44
45 await video.createTorrentAndSetInfoHash(inputVideoFile)
46 await inputVideoFile.save()
47 } catch (err) { 49 } catch (err) {
48 // Auto destruction... 50 // Auto destruction...
49 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) 51 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
@@ -52,8 +54,12 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
52 } 54 }
53} 55}
54 56
57/**
58 * Transcode the original video file to a lower resolution.
59 */
55async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { 60async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) {
56 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 61 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
62 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
57 const extname = '.mp4' 63 const extname = '.mp4'
58 64
59 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed 65 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
@@ -66,27 +72,49 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR
66 videoId: video.id 72 videoId: video.id
67 }) 73 })
68 const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile)) 74 const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile))
75 const videoTranscodedPath = join(transcodeDirectory, video.getVideoFilename(newVideoFile))
69 76
70 const transcodeOptions = { 77 const transcodeOptions = {
78 type: 'video' as 'video',
71 inputPath: videoInputPath, 79 inputPath: videoInputPath,
72 outputPath: videoOutputPath, 80 outputPath: videoTranscodedPath,
73 resolution, 81 resolution,
74 isPortraitMode: isPortrait 82 isPortraitMode: isPortrait
75 } 83 }
76 84
77 await transcode(transcodeOptions) 85 await transcode(transcodeOptions)
78 86
79 const stats = await stat(videoOutputPath) 87 return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
80 const fps = await getVideoFileFPS(videoOutputPath) 88}
89
90async function mergeAudioVideofile (video: VideoModel, resolution: VideoResolution) {
91 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
92 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
93 const newExtname = '.mp4'
94
95 const inputVideoFile = video.getOriginalFile()
81 96
82 newVideoFile.set('size', stats.size) 97 const audioInputPath = join(videosDirectory, video.getVideoFilename(video.getOriginalFile()))
83 newVideoFile.set('fps', fps) 98 const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
84 99
85 await video.createTorrentAndSetInfoHash(newVideoFile) 100 const transcodeOptions = {
101 type: 'merge-audio' as 'merge-audio',
102 inputPath: video.getPreview().getPath(),
103 outputPath: videoTranscodedPath,
104 audioPath: audioInputPath,
105 resolution
106 }
107
108 await transcode(transcodeOptions)
86 109
87 await newVideoFile.save() 110 await remove(audioInputPath)
88 111
89 video.VideoFiles.push(newVideoFile) 112 // Important to do this before getVideoFilename() to take in account the new file extension
113 inputVideoFile.extname = newExtname
114
115 const videoOutputPath = video.getVideoFilePath(inputVideoFile)
116
117 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
90} 118}
91 119
92async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { 120async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) {
@@ -97,6 +125,7 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti
97 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) 125 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
98 126
99 const transcodeOptions = { 127 const transcodeOptions = {
128 type: 'hls' as 'hls',
100 inputPath: videoInputPath, 129 inputPath: videoInputPath,
101 outputPath, 130 outputPath,
102 resolution, 131 resolution,
@@ -125,8 +154,34 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti
125 }) 154 })
126} 155}
127 156
157// ---------------------------------------------------------------------------
158
128export { 159export {
129 generateHlsPlaylist, 160 generateHlsPlaylist,
130 optimizeVideofile, 161 optimizeVideofile,
131 transcodeOriginalVideofile 162 transcodeOriginalVideofile,
163 mergeAudioVideofile
164}
165
166// ---------------------------------------------------------------------------
167
168async function onVideoFileTranscoding (video: VideoModel, videoFile: VideoFileModel, transcodingPath: string, outputPath: string) {
169 const stats = await stat(transcodingPath)
170 const fps = await getVideoFileFPS(transcodingPath)
171
172 await move(transcodingPath, outputPath)
173
174 videoFile.set('size', stats.size)
175 videoFile.set('fps', fps)
176
177 await video.createTorrentAndSetInfoHash(videoFile)
178
179 const updatedVideoFile = await videoFile.save()
180
181 // Add it if this is a new created file
182 if (video.VideoFiles.some(f => f.id === videoFile.id) === false) {
183 video.VideoFiles.push(updatedVideoFile)
184 }
185
186 return video
132} 187}
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
index e4f5c98fe..dd362619d 100644
--- a/server/middlewares/validators/feeds.ts
+++ b/server/middlewares/validators/feeds.ts
@@ -1,21 +1,20 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param, query } from 'express-validator/check' 2import { param, query } from 'express-validator/check'
3import { doesAccountIdExist, isAccountNameValid, doesAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' 3import { doesAccountIdExist, doesAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { areValidationErrors } from './utils' 6import { areValidationErrors } from './utils'
7import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' 7import { isValidRSSFeed } from '../../helpers/custom-validators/feeds'
8import { doesVideoChannelIdExist, doesVideoChannelNameWithHostExist } from '../../helpers/custom-validators/video-channels' 8import { doesVideoChannelIdExist, doesVideoChannelNameWithHostExist } from '../../helpers/custom-validators/video-channels'
9import { doesVideoExist } from '../../helpers/custom-validators/videos' 9import { doesVideoExist } from '../../helpers/custom-validators/videos'
10import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
11 10
12const videoFeedsValidator = [ 11const videoFeedsValidator = [
13 param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'), 12 param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
14 query('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'), 13 query('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
15 query('accountId').optional().custom(isIdOrUUIDValid), 14 query('accountId').optional().custom(isIdValid),
16 query('accountName').optional().custom(isAccountNameValid), 15 query('accountName').optional(),
17 query('videoChannelId').optional().custom(isIdOrUUIDValid), 16 query('videoChannelId').optional().custom(isIdValid),
18 query('videoChannelName').optional().custom(isActorPreferredUsernameValid), 17 query('videoChannelName').optional(),
19 18
20 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 19 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
21 logger.debug('Checking feeds parameters', { parameters: req.query }) 20 logger.debug('Checking feeds parameters', { parameters: req.query })
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 6d8cd7894..ec70fa0fd 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -25,6 +25,9 @@ import { Redis } from '../../lib/redis'
25import { UserModel } from '../../models/account/user' 25import { UserModel } from '../../models/account/user'
26import { areValidationErrors } from './utils' 26import { areValidationErrors } from './utils'
27import { ActorModel } from '../../models/activitypub/actor' 27import { ActorModel } from '../../models/activitypub/actor'
28import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
29import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
30import { UserRegister } from '../../../shared/models/users/user-register.model'
28 31
29const usersAddValidator = [ 32const usersAddValidator = [
30 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), 33 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
@@ -49,6 +52,16 @@ const usersRegisterValidator = [
49 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'), 52 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
50 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), 53 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
51 body('email').isEmail().withMessage('Should have a valid email'), 54 body('email').isEmail().withMessage('Should have a valid email'),
55 body('displayName')
56 .optional()
57 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
58
59 body('channel.name')
60 .optional()
61 .custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
62 body('channel.displayName')
63 .optional()
64 .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
52 65
53 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 66 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
54 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') }) 67 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
@@ -56,6 +69,28 @@ const usersRegisterValidator = [
56 if (areValidationErrors(req, res)) return 69 if (areValidationErrors(req, res)) return
57 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return 70 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
58 71
72 const body: UserRegister = req.body
73 if (body.channel) {
74 if (!body.channel.name || !body.channel.displayName) {
75 return res.status(400)
76 .send({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
77 .end()
78 }
79
80 if (body.channel.name === body.username) {
81 return res.status(400)
82 .send({ error: 'Channel name cannot be the same than user username.' })
83 .end()
84 }
85
86 const existing = await ActorModel.loadLocalByName(body.channel.name)
87 if (existing) {
88 return res.status(409)
89 .send({ error: `Channel with name ${body.channel.name} already exists.` })
90 .end()
91 }
92 }
93
59 return next() 94 return next()
60 } 95 }
61] 96]
@@ -142,13 +177,27 @@ const usersUpdateValidator = [
142] 177]
143 178
144const usersUpdateMeValidator = [ 179const usersUpdateMeValidator = [
145 body('displayName').optional().custom(isUserDisplayNameValid).withMessage('Should have a valid display name'), 180 body('displayName')
146 body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'), 181 .optional()
147 body('currentPassword').optional().custom(isUserPasswordValid).withMessage('Should have a valid current password'), 182 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
148 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), 183 body('description')
149 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 184 .optional()
150 body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), 185 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
151 body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), 186 body('currentPassword')
187 .optional()
188 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
189 body('password')
190 .optional()
191 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
192 body('email')
193 .optional()
194 .isEmail().withMessage('Should have a valid email attribute'),
195 body('nsfwPolicy')
196 .optional()
197 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
198 body('autoPlayVideo')
199 .optional()
200 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
152 body('videosHistoryEnabled') 201 body('videosHistoryEnabled')
153 .optional() 202 .optional()
154 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), 203 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
@@ -156,7 +205,7 @@ const usersUpdateMeValidator = [
156 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
157 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) 206 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
158 207
159 if (req.body.password) { 208 if (req.body.password || req.body.email) {
160 if (!req.body.currentPassword) { 209 if (!req.body.currentPassword) {
161 return res.status(400) 210 return res.status(400)
162 .send({ error: 'currentPassword parameter is missing.' }) 211 .send({ error: 'currentPassword parameter is missing.' })
@@ -293,8 +342,14 @@ const usersAskSendVerifyEmailValidator = [
293] 342]
294 343
295const usersVerifyEmailValidator = [ 344const usersVerifyEmailValidator = [
296 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 345 param('id')
297 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'), 346 .isInt().not().isEmpty().withMessage('Should have a valid id'),
347
348 body('verificationString')
349 .not().isEmpty().withMessage('Should have a valid verification string'),
350 body('isPendingEmail')
351 .optional()
352 .toBoolean(),
298 353
299 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 354 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
300 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params }) 355 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 4b26f0bc4..f5a59cacb 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -14,6 +14,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
14import { areValidationErrors } from '../utils' 14import { areValidationErrors } from '../utils'
15import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' 15import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
16import { ActorModel } from '../../../models/activitypub/actor' 16import { ActorModel } from '../../../models/activitypub/actor'
17import { isBooleanValid } from '../../../helpers/custom-validators/misc'
17 18
18const videoChannelsAddValidator = [ 19const videoChannelsAddValidator = [
19 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), 20 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
@@ -40,9 +41,18 @@ const videoChannelsAddValidator = [
40 41
41const videoChannelsUpdateValidator = [ 42const videoChannelsUpdateValidator = [
42 param('nameWithHost').exists().withMessage('Should have an video channel name with host'), 43 param('nameWithHost').exists().withMessage('Should have an video channel name with host'),
43 body('displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'), 44 body('displayName')
44 body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), 45 .optional()
45 body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'), 46 .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
47 body('description')
48 .optional()
49 .custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'),
50 body('support')
51 .optional()
52 .custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'),
53 body('bulkVideosSupportUpdate')
54 .optional()
55 .custom(isBooleanValid).withMessage('Should have a valid bulkVideosSupportUpdate boolean field'),
46 56
47 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
48 logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) 58 logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index f68eeeeb3..9c88dd291 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -24,6 +24,9 @@ import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/
24import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model' 24import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
25 25
26const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ 26const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
27 body('displayName')
28 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
29
27 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 30 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
28 logger.debug('Checking videoPlaylistsAddValidator parameters', { parameters: req.body }) 31 logger.debug('Checking videoPlaylistsAddValidator parameters', { parameters: req.body })
29 32
@@ -46,6 +49,10 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
46 param('playlistId') 49 param('playlistId')
47 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), 50 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
48 51
52 body('displayName')
53 .optional()
54 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
55
49 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 56 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
50 logger.debug('Checking videoPlaylistsUpdateValidator parameters', { parameters: req.body }) 57 logger.debug('Checking videoPlaylistsUpdateValidator parameters', { parameters: req.body })
51 58
@@ -61,12 +68,6 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
61 68
62 const body: VideoPlaylistUpdate = req.body 69 const body: VideoPlaylistUpdate = req.body
63 70
64 if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PRIVATE && body.privacy === VideoPlaylistPrivacy.PRIVATE) {
65 cleanUpReqFiles(req)
66 return res.status(400)
67 .json({ error: 'Cannot set "private" a video playlist that was not private.' })
68 }
69
70 const newPrivacy = body.privacy || videoPlaylist.privacy 71 const newPrivacy = body.privacy || videoPlaylist.privacy
71 if (newPrivacy === VideoPlaylistPrivacy.PUBLIC && 72 if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
72 ( 73 (
@@ -368,8 +369,6 @@ function getCommonPlaylistEditAttributes () {
368 + CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ') 369 + CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
369 ), 370 ),
370 371
371 body('displayName')
372 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
373 body('description') 372 body('description')
374 .optional() 373 .optional()
375 .customSanitizer(toValueOrNull) 374 .customSanitizer(toValueOrNull)
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 2b01f108d..b1c05ab2d 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -111,18 +111,10 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
111 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) 111 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
112 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) 112 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
113 113
114 const video = res.locals.video
115
116 // Check if the user who did the request is able to update the video 114 // Check if the user who did the request is able to update the video
117 const user = res.locals.oauth.token.User 115 const user = res.locals.oauth.token.User
118 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) 116 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
119 117
120 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
121 cleanUpReqFiles(req)
122 return res.status(409)
123 .json({ error: 'Cannot set "private" a video that was not private.' })
124 }
125
126 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 118 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
127 119
128 return next() 120 return next()
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 2b04acd86..09cada096 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -47,7 +47,7 @@ export enum ScopeNames {
47 attributes: [ 'id', 'name' ], 47 attributes: [ 'id', 'name' ],
48 include: [ 48 include: [
49 { 49 {
50 attributes: [ 'id', 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 50 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
51 model: ActorModel.unscoped(), 51 model: ActorModel.unscoped(),
52 required: true, 52 required: true,
53 where: whereActor, 53 where: whereActor,
@@ -180,22 +180,6 @@ export class AccountModel extends Model<AccountModel> {
180 return AccountModel.findByPk(id, { transaction }) 180 return AccountModel.findByPk(id, { transaction })
181 } 181 }
182 182
183 static loadByUUID (uuid: string) {
184 const query = {
185 include: [
186 {
187 model: ActorModel,
188 required: true,
189 where: {
190 uuid
191 }
192 }
193 ]
194 }
195
196 return AccountModel.findOne(query)
197 }
198
199 static loadByNameWithHost (nameWithHost: string) { 183 static loadByNameWithHost (nameWithHost: string) {
200 const [ accountName, host ] = nameWithHost.split('@') 184 const [ accountName, host ] = nameWithHost.split('@')
201 185
@@ -332,7 +316,6 @@ export class AccountModel extends Model<AccountModel> {
332 316
333 return { 317 return {
334 id: this.id, 318 id: this.id,
335 uuid: actor.uuid,
336 name: actor.name, 319 name: actor.name,
337 displayName: this.getDisplayName(), 320 displayName: this.getDisplayName(),
338 url: actor.url, 321 url: actor.url,
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 4a9acd703..e75039521 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -114,6 +114,11 @@ export class UserModel extends Model<UserModel> {
114 email: string 114 email: string
115 115
116 @AllowNull(true) 116 @AllowNull(true)
117 @IsEmail
118 @Column(DataType.STRING(400))
119 pendingEmail: string
120
121 @AllowNull(true)
117 @Default(null) 122 @Default(null)
118 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true)) 123 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
119 @Column 124 @Column
@@ -540,6 +545,7 @@ export class UserModel extends Model<UserModel> {
540 id: this.id, 545 id: this.id,
541 username: this.username, 546 username: this.username,
542 email: this.email, 547 email: this.email,
548 pendingEmail: this.pendingEmail,
543 emailVerified: this.emailVerified, 549 emailVerified: this.emailVerified,
544 nsfwPolicy: this.nsfwPolicy, 550 nsfwPolicy: this.nsfwPolicy,
545 webTorrentEnabled: this.webTorrentEnabled, 551 webTorrentEnabled: this.webTorrentEnabled,
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 4a466441c..bd6a2c8fd 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -7,13 +7,11 @@ import {
7 Column, 7 Column,
8 CreatedAt, 8 CreatedAt,
9 DataType, 9 DataType,
10 Default,
11 DefaultScope, 10 DefaultScope,
12 ForeignKey, 11 ForeignKey,
13 HasMany, 12 HasMany,
14 HasOne, 13 HasOne,
15 Is, 14 Is,
16 IsUUID,
17 Model, 15 Model,
18 Scopes, 16 Scopes,
19 Table, 17 Table,
@@ -120,10 +118,6 @@ export const unusedActorAttributesForAPI = [
120 fields: [ 'avatarId' ] 118 fields: [ 'avatarId' ]
121 }, 119 },
122 { 120 {
123 fields: [ 'uuid' ],
124 unique: true
125 },
126 {
127 fields: [ 'followersUrl' ] 121 fields: [ 'followersUrl' ]
128 } 122 }
129 ] 123 ]
@@ -135,12 +129,6 @@ export class ActorModel extends Model<ActorModel> {
135 type: ActivityPubActorType 129 type: ActivityPubActorType
136 130
137 @AllowNull(false) 131 @AllowNull(false)
138 @Default(DataType.UUIDV4)
139 @IsUUID(4)
140 @Column(DataType.UUID)
141 uuid: string
142
143 @AllowNull(false)
144 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username')) 132 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
145 @Column 133 @Column
146 preferredUsername: string 134 preferredUsername: string
@@ -408,7 +396,6 @@ export class ActorModel extends Model<ActorModel> {
408 return { 396 return {
409 id: this.id, 397 id: this.id,
410 url: this.url, 398 url: this.url,
411 uuid: this.uuid,
412 name: this.preferredUsername, 399 name: this.preferredUsername,
413 host: this.getHost(), 400 host: this.getHost(),
414 hostRedundancyAllowed: this.getRedundancyAllowed(), 401 hostRedundancyAllowed: this.getRedundancyAllowed(),
@@ -454,7 +441,6 @@ export class ActorModel extends Model<ActorModel> {
454 endpoints: { 441 endpoints: {
455 sharedInbox: this.sharedInboxUrl 442 sharedInbox: this.sharedInboxUrl
456 }, 443 },
457 uuid: this.uuid,
458 publicKey: { 444 publicKey: {
459 id: this.getPublicKeyUrl(), 445 id: this.getPublicKeyUrl(),
460 owner: this.url, 446 owner: this.url,
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 206e9a3d6..8faf0adba 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -107,10 +107,12 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
107 return WEBSERVER.URL + staticPath + this.filename 107 return WEBSERVER.URL + staticPath + this.filename
108 } 108 }
109 109
110 removeThumbnail () { 110 getPath () {
111 const directory = ThumbnailModel.types[this.type].directory 111 const directory = ThumbnailModel.types[this.type].directory
112 const thumbnailPath = join(directory, this.filename) 112 return join(directory, this.filename)
113 }
113 114
114 return remove(thumbnailPath) 115 removeThumbnail () {
116 return remove(this.getPath())
115 } 117 }
116} 118}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index fb70e6625..b0b261c88 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -72,7 +72,7 @@ type AvailableForListOptions = {
72 attributes: [ 'name', 'description', 'id', 'actorId' ], 72 attributes: [ 'name', 'description', 'id', 'actorId' ],
73 include: [ 73 include: [
74 { 74 {
75 attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 75 attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ],
76 model: ActorModel.unscoped(), 76 model: ActorModel.unscoped(),
77 required: true, 77 required: true,
78 include: [ 78 include: [
@@ -334,14 +334,21 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
334 }) 334 })
335 } 335 }
336 336
337 static listByAccount (accountId: number) { 337 static listByAccount (options: {
338 accountId: number,
339 start: number,
340 count: number,
341 sort: string
342 }) {
338 const query = { 343 const query = {
339 order: getSort('createdAt'), 344 offset: options.start,
345 limit: options.count,
346 order: getSort(options.sort),
340 include: [ 347 include: [
341 { 348 {
342 model: AccountModel, 349 model: AccountModel,
343 where: { 350 where: {
344 id: accountId 351 id: options.accountId
345 }, 352 },
346 required: true 353 required: true
347 } 354 }
@@ -380,24 +387,6 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
380 .findByPk(id) 387 .findByPk(id)
381 } 388 }
382 389
383 static loadByUUIDAndPopulateAccount (uuid: string) {
384 const query = {
385 include: [
386 {
387 model: ActorModel,
388 required: true,
389 where: {
390 uuid
391 }
392 }
393 ]
394 }
395
396 return VideoChannelModel
397 .scope([ ScopeNames.WITH_ACCOUNT ])
398 .findOne(query)
399 }
400
401 static loadByUrlAndPopulateAccount (url: string) { 390 static loadByUrlAndPopulateAccount (url: string) {
402 const query = { 391 const query = {
403 include: [ 392 include: [
@@ -503,7 +492,6 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
503 492
504 return { 493 return {
505 id: this.id, 494 id: this.id,
506 uuid: actor.uuid,
507 name: actor.name, 495 name: actor.name,
508 displayName: this.getDisplayName(), 496 displayName: this.getDisplayName(),
509 url: actor.url, 497 url: actor.url,
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 2203a7aba..05c490759 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -24,6 +24,7 @@ import { VideoModel } from './video'
24import { VideoRedundancyModel } from '../redundancy/video-redundancy' 24import { VideoRedundancyModel } from '../redundancy/video-redundancy'
25import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 25import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
26import { FindOptions, QueryTypes, Transaction } from 'sequelize' 26import { FindOptions, QueryTypes, Transaction } from 'sequelize'
27import { MIMETYPES } from '../../initializers/constants'
27 28
28@Table({ 29@Table({
29 tableName: 'videoFile', 30 tableName: 'videoFile',
@@ -161,6 +162,10 @@ export class VideoFileModel extends Model<VideoFileModel> {
161 })) 162 }))
162 } 163 }
163 164
165 isAudio () {
166 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
167 }
168
164 hasSameUniqueKeysThan (other: VideoFileModel) { 169 hasSameUniqueKeysThan (other: VideoFileModel) {
165 return this.fps === other.fps && 170 return this.fps === other.fps &&
166 this.resolution === other.resolution && 171 this.resolution === other.resolution &&
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index c0a7892a4..eccf0a4fa 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1515,6 +1515,29 @@ export class VideoModel extends Model<VideoModel> {
1515 .then(results => results.length === 1) 1515 .then(results => results.length === 1)
1516 } 1516 }
1517 1517
1518 static bulkUpdateSupportField (videoChannel: VideoChannelModel, t: Transaction) {
1519 const options = {
1520 where: {
1521 channelId: videoChannel.id
1522 },
1523 transaction: t
1524 }
1525
1526 return VideoModel.update({ support: videoChannel.support }, options)
1527 }
1528
1529 static getAllIdsFromChannel (videoChannel: VideoChannelModel) {
1530 const query = {
1531 attributes: [ 'id' ],
1532 where: {
1533 channelId: videoChannel.id
1534 }
1535 }
1536
1537 return VideoModel.findAll(query)
1538 .then(videos => videos.map(v => v.id))
1539 }
1540
1518 // threshold corresponds to how many video the field should have to be returned 1541 // threshold corresponds to how many video the field should have to be returned
1519 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1542 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1520 const serverActor = await getServerActor() 1543 const serverActor = await getServerActor()
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
index edf588c16..34c6be49b 100644
--- a/server/tests/api/activitypub/client.ts
+++ b/server/tests/api/activitypub/client.ts
@@ -3,6 +3,7 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 doubleFollow, 7 doubleFollow,
7 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
8 flushTests, 9 flushTests,
@@ -39,7 +40,7 @@ describe('Test activitypub', function () {
39 const object = res.body 40 const object = res.body
40 41
41 expect(object.type).to.equal('Person') 42 expect(object.type).to.equal('Person')
42 expect(object.id).to.equal('http://localhost:9001/accounts/root') 43 expect(object.id).to.equal('http://localhost:' + servers[0].port + '/accounts/root')
43 expect(object.name).to.equal('root') 44 expect(object.name).to.equal('root')
44 expect(object.preferredUsername).to.equal('root') 45 expect(object.preferredUsername).to.equal('root')
45 }) 46 })
@@ -49,17 +50,17 @@ describe('Test activitypub', function () {
49 const object = res.body 50 const object = res.body
50 51
51 expect(object.type).to.equal('Video') 52 expect(object.type).to.equal('Video')
52 expect(object.id).to.equal('http://localhost:9001/videos/watch/' + videoUUID) 53 expect(object.id).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + videoUUID)
53 expect(object.name).to.equal('video') 54 expect(object.name).to.equal('video')
54 }) 55 })
55 56
56 it('Should redirect to the origin video object', async function () { 57 it('Should redirect to the origin video object', async function () {
57 const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + videoUUID, 302) 58 const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + videoUUID, 302)
58 59
59 expect(res.header.location).to.equal('http://localhost:9001/videos/watch/' + videoUUID) 60 expect(res.header.location).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + videoUUID)
60 }) 61 })
61 62
62 after(function () { 63 after(async function () {
63 killallServers(servers) 64 await cleanupTests(servers)
64 }) 65 })
65}) 66})
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts
index 7240bb0fb..3a1c0d321 100644
--- a/server/tests/api/activitypub/fetch.ts
+++ b/server/tests/api/activitypub/fetch.ts
@@ -3,6 +3,7 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 closeAllSequelize, 7 closeAllSequelize,
7 createUser, 8 createUser,
8 doubleFollow, 9 doubleFollow,
@@ -48,8 +49,16 @@ describe('Test ActivityPub fetcher', function () {
48 const badVideoUUID = res.body.video.uuid 49 const badVideoUUID = res.body.video.uuid
49 await uploadVideo(servers[0].url, userAccessToken, { name: 'video user' }) 50 await uploadVideo(servers[0].url, userAccessToken, { name: 'video user' })
50 51
51 await setActorField(1, 'http://localhost:9001/accounts/user1', 'url', 'http://localhost:9002/accounts/user1') 52 {
52 await setVideoField(1, badVideoUUID, 'url', 'http://localhost:9003/videos/watch/' + badVideoUUID) 53 const to = 'http://localhost:' + servers[0].port + '/accounts/user1'
54 const value = 'http://localhost:' + servers[1].port + '/accounts/user1'
55 await setActorField(servers[0].internalServerNumber, to, 'url', value)
56 }
57
58 {
59 const value = 'http://localhost:' + servers[2].port + '/videos/watch/' + badVideoUUID
60 await setVideoField(servers[0].internalServerNumber, badVideoUUID, 'url', value)
61 }
53 }) 62 })
54 63
55 it('Should add only the video with a valid actor URL', async function () { 64 it('Should add only the video with a valid actor URL', async function () {
@@ -78,7 +87,9 @@ describe('Test ActivityPub fetcher', function () {
78 }) 87 })
79 88
80 after(async function () { 89 after(async function () {
81 killallServers(servers) 90 this.timeout(10000)
91
92 await cleanupTests(servers)
82 93
83 await closeAllSequelize(servers) 94 await closeAllSequelize(servers)
84 }) 95 })
diff --git a/server/tests/api/activitypub/refresher.ts b/server/tests/api/activitypub/refresher.ts
index 9be9aa495..921ee874c 100644
--- a/server/tests/api/activitypub/refresher.ts
+++ b/server/tests/api/activitypub/refresher.ts
@@ -2,13 +2,14 @@
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
5 cleanupTests, closeAllSequelize,
5 createVideoPlaylist, 6 createVideoPlaylist,
6 doubleFollow, 7 doubleFollow,
7 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
8 generateUserAccessToken, 9 generateUserAccessToken,
9 getVideo, 10 getVideo,
10 getVideoPlaylist, 11 getVideoPlaylist,
11 killallServers, rateVideo, 12 killallServers,
12 reRunServer, 13 reRunServer,
13 ServerInfo, 14 ServerInfo,
14 setAccessTokensToServers, 15 setAccessTokensToServers,
@@ -48,26 +49,26 @@ describe('Test AP refresher', function () {
48 } 49 }
49 50
50 { 51 {
51 const a1 = await generateUserAccessToken(servers[1], 'user1') 52 const a1 = await generateUserAccessToken(servers[ 1 ], 'user1')
52 await uploadVideo(servers[1].url, a1, { name: 'video4' }) 53 await uploadVideo(servers[ 1 ].url, a1, { name: 'video4' })
53 54
54 const a2 = await generateUserAccessToken(servers[1], 'user2') 55 const a2 = await generateUserAccessToken(servers[ 1 ], 'user2')
55 await uploadVideo(servers[1].url, a2, { name: 'video5' }) 56 await uploadVideo(servers[ 1 ].url, a2, { name: 'video5' })
56 } 57 }
57 58
58 { 59 {
59 const playlistAttrs = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id } 60 const playlistAttrs = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[ 1 ].videoChannel.id }
60 const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs }) 61 const res = await createVideoPlaylist({ url: servers[ 1 ].url, token: servers[ 1 ].accessToken, playlistAttrs })
61 playlistUUID1 = res.body.videoPlaylist.uuid 62 playlistUUID1 = res.body.videoPlaylist.uuid
62 } 63 }
63 64
64 { 65 {
65 const playlistAttrs = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id } 66 const playlistAttrs = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[ 1 ].videoChannel.id }
66 const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs }) 67 const res = await createVideoPlaylist({ url: servers[ 1 ].url, token: servers[ 1 ].accessToken, playlistAttrs })
67 playlistUUID2 = res.body.videoPlaylist.uuid 68 playlistUUID2 = res.body.videoPlaylist.uuid
68 } 69 }
69 70
70 await doubleFollow(servers[0], servers[1]) 71 await doubleFollow(servers[ 0 ], servers[ 1 ])
71 }) 72 })
72 73
73 describe('Videos refresher', function () { 74 describe('Videos refresher', function () {
@@ -78,7 +79,7 @@ describe('Test AP refresher', function () {
78 await wait(10000) 79 await wait(10000)
79 80
80 // Change UUID so the remote server returns a 404 81 // Change UUID so the remote server returns a 404
81 await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f') 82 await setVideoField(servers[ 1 ].internalServerNumber, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
82 83
83 await getVideo(servers[ 0 ].url, videoUUID1) 84 await getVideo(servers[ 0 ].url, videoUUID1)
84 await getVideo(servers[ 0 ].url, videoUUID2) 85 await getVideo(servers[ 0 ].url, videoUUID2)
@@ -94,7 +95,7 @@ describe('Test AP refresher', function () {
94 95
95 killallServers([ servers[ 1 ] ]) 96 killallServers([ servers[ 1 ] ])
96 97
97 await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e') 98 await setVideoField(servers[ 1 ].internalServerNumber, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
98 99
99 // Video will need a refresh 100 // Video will need a refresh
100 await wait(10000) 101 await wait(10000)
@@ -121,15 +122,16 @@ describe('Test AP refresher', function () {
121 await wait(10000) 122 await wait(10000)
122 123
123 // Change actor name so the remote server returns a 404 124 // Change actor name so the remote server returns a 404
124 await setActorField(2, 'http://localhost:9002/accounts/user2', 'preferredUsername', 'toto') 125 const to = 'http://localhost:' + servers[ 1 ].port + '/accounts/user2'
126 await setActorField(servers[ 1 ].internalServerNumber, to, 'preferredUsername', 'toto')
125 127
126 await getAccount(servers[ 0 ].url, 'user1@localhost:9002') 128 await getAccount(servers[ 0 ].url, 'user1@localhost:' + servers[ 1 ].port)
127 await getAccount(servers[ 0 ].url, 'user2@localhost:9002') 129 await getAccount(servers[ 0 ].url, 'user2@localhost:' + servers[ 1 ].port)
128 130
129 await waitJobs(servers) 131 await waitJobs(servers)
130 132
131 await getAccount(servers[ 0 ].url, 'user1@localhost:9002', 200) 133 await getAccount(servers[ 0 ].url, 'user1@localhost:' + servers[ 1 ].port, 200)
132 await getAccount(servers[ 0 ].url, 'user2@localhost:9002', 404) 134 await getAccount(servers[ 0 ].url, 'user2@localhost:' + servers[ 1 ].port, 404)
133 }) 135 })
134 }) 136 })
135 137
@@ -141,7 +143,7 @@ describe('Test AP refresher', function () {
141 await wait(10000) 143 await wait(10000)
142 144
143 // Change UUID so the remote server returns a 404 145 // Change UUID so the remote server returns a 404
144 await setPlaylistField(2, playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e') 146 await setPlaylistField(servers[ 1 ].internalServerNumber, playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
145 147
146 await getVideoPlaylist(servers[ 0 ].url, playlistUUID1) 148 await getVideoPlaylist(servers[ 0 ].url, playlistUUID1)
147 await getVideoPlaylist(servers[ 0 ].url, playlistUUID2) 149 await getVideoPlaylist(servers[ 0 ].url, playlistUUID2)
@@ -153,7 +155,11 @@ describe('Test AP refresher', function () {
153 }) 155 })
154 }) 156 })
155 157
156 after(function () { 158 after(async function () {
157 killallServers(servers) 159 this.timeout(10000)
160
161 await cleanupTests(servers)
162
163 await closeAllSequelize(servers)
158 }) 164 })
159}) 165})
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
index 11e6859bf..dc960c5c3 100644
--- a/server/tests/api/activitypub/security.ts
+++ b/server/tests/api/activitypub/security.ts
@@ -3,9 +3,9 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 closeAllSequelize, 7 closeAllSequelize,
7 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
8 flushTests,
9 killallServers, 9 killallServers,
10 ServerInfo, 10 ServerInfo,
11 setActorField 11 setActorField
@@ -18,18 +18,26 @@ import { makeFollowRequest, makePOSTAPRequest } from '../../../../shared/extra-u
18 18
19const expect = chai.expect 19const expect = chai.expect
20 20
21function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) { 21function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) {
22 return Promise.all([ 22 return Promise.all([
23 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey), 23 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'publicKey', publicKey),
24 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey) 24 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'privateKey', privateKey)
25 ]) 25 ])
26} 26}
27 27
28function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) { 28function getAnnounceWithoutContext (server2: ServerInfo) {
29 return Promise.all([ 29 const json = require('./json/peertube/announce-without-context.json')
30 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey), 30 const result: typeof json = {}
31 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey) 31
32 ]) 32 for (const key of Object.keys(json)) {
33 if (Array.isArray(json[key])) {
34 result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`))
35 } else {
36 result[ key ] = json[ key ].replace(':9002', `:${server2.port}`)
37 }
38 }
39
40 return result
33} 41}
34 42
35describe('Test ActivityPub security', function () { 43describe('Test ActivityPub security', function () {
@@ -38,13 +46,13 @@ describe('Test ActivityPub security', function () {
38 46
39 const keys = require('./json/peertube/keys.json') 47 const keys = require('./json/peertube/keys.json')
40 const invalidKeys = require('./json/peertube/invalid-keys.json') 48 const invalidKeys = require('./json/peertube/invalid-keys.json')
41 const baseHttpSignature = { 49 const baseHttpSignature = () => ({
42 algorithm: HTTP_SIGNATURE.ALGORITHM, 50 algorithm: HTTP_SIGNATURE.ALGORITHM,
43 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, 51 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
44 keyId: 'acct:peertube@localhost:9002', 52 keyId: 'acct:peertube@localhost:' + servers[1].port,
45 key: keys.privateKey, 53 key: keys.privateKey,
46 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN 54 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
47 } 55 })
48 56
49 // --------------------------------------------------------------- 57 // ---------------------------------------------------------------
50 58
@@ -55,56 +63,56 @@ describe('Test ActivityPub security', function () {
55 63
56 url = servers[0].url + '/inbox' 64 url = servers[0].url + '/inbox'
57 65
58 await setKeysOfServer2(1, keys.publicKey, keys.privateKey) 66 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
59 67
60 const to = { url: 'http://localhost:9001/accounts/peertube' } 68 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
61 const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } 69 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
62 await makeFollowRequest(to, by) 70 await makeFollowRequest(to, by)
63 }) 71 })
64 72
65 describe('When checking HTTP signature', function () { 73 describe('When checking HTTP signature', function () {
66 74
67 it('Should fail with an invalid digest', async function () { 75 it('Should fail with an invalid digest', async function () {
68 const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) 76 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
69 const headers = { 77 const headers = {
70 Digest: buildDigest({ hello: 'coucou' }) 78 Digest: buildDigest({ hello: 'coucou' })
71 } 79 }
72 80
73 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) 81 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
74 82
75 expect(response.statusCode).to.equal(403) 83 expect(response.statusCode).to.equal(403)
76 }) 84 })
77 85
78 it('Should fail with an invalid date', async function () { 86 it('Should fail with an invalid date', async function () {
79 const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) 87 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
80 const headers = buildGlobalHeaders(body) 88 const headers = buildGlobalHeaders(body)
81 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' 89 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
82 90
83 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) 91 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
84 92
85 expect(response.statusCode).to.equal(403) 93 expect(response.statusCode).to.equal(403)
86 }) 94 })
87 95
88 it('Should fail with bad keys', async function () { 96 it('Should fail with bad keys', async function () {
89 await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey) 97 await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
90 await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey) 98 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
91 99
92 const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) 100 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
93 const headers = buildGlobalHeaders(body) 101 const headers = buildGlobalHeaders(body)
94 102
95 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) 103 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
96 104
97 expect(response.statusCode).to.equal(403) 105 expect(response.statusCode).to.equal(403)
98 }) 106 })
99 107
100 it('Should succeed with a valid HTTP signature', async function () { 108 it('Should succeed with a valid HTTP signature', async function () {
101 await setKeysOfServer2(1, keys.publicKey, keys.privateKey) 109 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
102 await setKeysOfServer2(2, keys.publicKey, keys.privateKey) 110 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
103 111
104 const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) 112 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
105 const headers = buildGlobalHeaders(body) 113 const headers = buildGlobalHeaders(body)
106 114
107 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers) 115 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
108 116
109 expect(response.statusCode).to.equal(204) 117 expect(response.statusCode).to.equal(204)
110 }) 118 })
@@ -112,28 +120,28 @@ describe('Test ActivityPub security', function () {
112 120
113 describe('When checking Linked Data Signature', function () { 121 describe('When checking Linked Data Signature', function () {
114 before(async () => { 122 before(async () => {
115 await setKeysOfServer3(3, keys.publicKey, keys.privateKey) 123 await setKeysOfServer(servers[2], servers[2], keys.publicKey, keys.privateKey)
116 124
117 const to = { url: 'http://localhost:9001/accounts/peertube' } 125 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
118 const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey } 126 const by = { url: 'http://localhost:' + servers[2].port + '/accounts/peertube', privateKey: keys.privateKey }
119 await makeFollowRequest(to, by) 127 await makeFollowRequest(to, by)
120 }) 128 })
121 129
122 it('Should fail with bad keys', async function () { 130 it('Should fail with bad keys', async function () {
123 this.timeout(10000) 131 this.timeout(10000)
124 132
125 await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey) 133 await setKeysOfServer(servers[0], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
126 await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey) 134 await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
127 135
128 const body = require('./json/peertube/announce-without-context.json') 136 const body = getAnnounceWithoutContext(servers[1])
129 body.actor = 'http://localhost:9003/accounts/peertube' 137 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
130 138
131 const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' } 139 const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
132 const signedBody = await buildSignedActivity(signer, body) 140 const signedBody = await buildSignedActivity(signer, body)
133 141
134 const headers = buildGlobalHeaders(signedBody) 142 const headers = buildGlobalHeaders(signedBody)
135 143
136 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers) 144 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
137 145
138 expect(response.statusCode).to.equal(403) 146 expect(response.statusCode).to.equal(403)
139 }) 147 })
@@ -141,20 +149,20 @@ describe('Test ActivityPub security', function () {
141 it('Should fail with an altered body', async function () { 149 it('Should fail with an altered body', async function () {
142 this.timeout(10000) 150 this.timeout(10000)
143 151
144 await setKeysOfServer3(1, keys.publicKey, keys.privateKey) 152 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
145 await setKeysOfServer3(3, keys.publicKey, keys.privateKey) 153 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
146 154
147 const body = require('./json/peertube/announce-without-context.json') 155 const body = getAnnounceWithoutContext(servers[1])
148 body.actor = 'http://localhost:9003/accounts/peertube' 156 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
149 157
150 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' } 158 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
151 const signedBody = await buildSignedActivity(signer, body) 159 const signedBody = await buildSignedActivity(signer, body)
152 160
153 signedBody.actor = 'http://localhost:9003/account/peertube' 161 signedBody.actor = 'http://localhost:' + servers[2].port + '/account/peertube'
154 162
155 const headers = buildGlobalHeaders(signedBody) 163 const headers = buildGlobalHeaders(signedBody)
156 164
157 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers) 165 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
158 166
159 expect(response.statusCode).to.equal(403) 167 expect(response.statusCode).to.equal(403)
160 }) 168 })
@@ -162,22 +170,24 @@ describe('Test ActivityPub security', function () {
162 it('Should succeed with a valid signature', async function () { 170 it('Should succeed with a valid signature', async function () {
163 this.timeout(10000) 171 this.timeout(10000)
164 172
165 const body = require('./json/peertube/announce-without-context.json') 173 const body = getAnnounceWithoutContext(servers[1])
166 body.actor = 'http://localhost:9003/accounts/peertube' 174 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
167 175
168 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' } 176 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
169 const signedBody = await buildSignedActivity(signer, body) 177 const signedBody = await buildSignedActivity(signer, body)
170 178
171 const headers = buildGlobalHeaders(signedBody) 179 const headers = buildGlobalHeaders(signedBody)
172 180
173 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers) 181 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
174 182
175 expect(response.statusCode).to.equal(204) 183 expect(response.statusCode).to.equal(204)
176 }) 184 })
177 }) 185 })
178 186
179 after(async function () { 187 after(async function () {
180 killallServers(servers) 188 this.timeout(10000)
189
190 await cleanupTests(servers)
181 191
182 await closeAllSequelize(servers) 192 await closeAllSequelize(servers)
183 }) 193 })
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index 2a2ec606a..a0d9392dc 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -59,13 +59,15 @@ describe('Test config API validators', function () {
59 transcoding: { 59 transcoding: {
60 enabled: true, 60 enabled: true,
61 allowAdditionalExtensions: true, 61 allowAdditionalExtensions: true,
62 allowAudioFiles: true,
62 threads: 1, 63 threads: 1,
63 resolutions: { 64 resolutions: {
64 '240p': false, 65 '240p': false,
65 '360p': true, 66 '360p': true,
66 '480p': true, 67 '480p': true,
67 '720p': false, 68 '720p': false,
68 '1080p': false 69 '1080p': false,
70 '2160p': false
69 }, 71 },
70 hls: { 72 hls: {
71 enabled: false 73 enabled: false
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 5935104a5..2316033a1 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -6,6 +6,7 @@ import { join } from 'path'
6import { UserRole, VideoImport, VideoImportState } from '../../../../shared' 6import { UserRole, VideoImport, VideoImportState } from '../../../../shared'
7 7
8import { 8import {
9 addVideoChannel,
9 blockUser, 10 blockUser,
10 cleanupTests, 11 cleanupTests,
11 createUser, 12 createUser,
@@ -378,8 +379,7 @@ describe('Test users API validators', function () {
378 it('Should succeed without password change with the correct params', async function () { 379 it('Should succeed without password change with the correct params', async function () {
379 const fields = { 380 const fields = {
380 nsfwPolicy: 'blur', 381 nsfwPolicy: 'blur',
381 autoPlayVideo: false, 382 autoPlayVideo: false
382 email: 'super_email@example.com'
383 } 383 }
384 384
385 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 }) 385 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 })
@@ -638,10 +638,11 @@ describe('Test users API validators', function () {
638 }) 638 })
639 }) 639 })
640 640
641 describe('When register a new user', function () { 641 describe('When registering a new user', function () {
642 const registrationPath = path + '/register' 642 const registrationPath = path + '/register'
643 const baseCorrectParams = { 643 const baseCorrectParams = {
644 username: 'user3', 644 username: 'user3',
645 displayName: 'super user',
645 email: 'test3@example.com', 646 email: 'test3@example.com',
646 password: 'my super password' 647 password: 'my super password'
647 } 648 }
@@ -724,12 +725,48 @@ describe('Test users API validators', function () {
724 }) 725 })
725 }) 726 })
726 727
728 it('Should fail with a bad display name', async function () {
729 const fields = immutableAssign(baseCorrectParams, { displayName: 'a'.repeat(150) })
730
731 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
732 })
733
734 it('Should fail with a bad channel name', async function () {
735 const fields = immutableAssign(baseCorrectParams, { channel: { name: '[]azf', displayName: 'toto' } })
736
737 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
738 })
739
740 it('Should fail with a bad channel display name', async function () {
741 const fields = immutableAssign(baseCorrectParams, { channel: { name: 'toto', displayName: '' } })
742
743 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
744 })
745
746 it('Should fail with a channel name that is the same than user username', async function () {
747 const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } }
748 const fields = immutableAssign(baseCorrectParams, source)
749
750 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
751 })
752
753 it('Should fail with an existing channel', async function () {
754 const videoChannelAttributesArg = { name: 'existing_channel', displayName: 'hello', description: 'super description' }
755 await addVideoChannel(server.url, server.accessToken, videoChannelAttributesArg)
756
757 const fields = immutableAssign(baseCorrectParams, { channel: { name: 'existing_channel', displayName: 'toto' } })
758
759 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields, statusCodeExpected: 409 })
760 })
761
727 it('Should succeed with the correct params', async function () { 762 it('Should succeed with the correct params', async function () {
763 const fields = immutableAssign(baseCorrectParams, { channel: { name: 'super_channel', displayName: 'toto' } })
764
728 await makePostBodyRequest({ 765 await makePostBodyRequest({
729 url: server.url, 766 url: server.url,
730 path: registrationPath, 767 path: registrationPath,
731 token: server.accessToken, 768 token: server.accessToken,
732 fields: baseCorrectParams, 769 fields: fields,
733 statusCodeExpected: 204 770 statusCodeExpected: 204
734 }) 771 })
735 }) 772 })
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts
index 65bc20613..de88298d1 100644
--- a/server/tests/api/check-params/video-channels.ts
+++ b/server/tests/api/check-params/video-channels.ts
@@ -24,6 +24,7 @@ import {
24 checkBadStartPagination 24 checkBadStartPagination
25} from '../../../../shared/extra-utils/requests/check-api-params' 25} from '../../../../shared/extra-utils/requests/check-api-params'
26import { join } from 'path' 26import { join } from 'path'
27import { VideoChannelUpdate } from '../../../../shared/models/videos'
27 28
28const expect = chai.expect 29const expect = chai.expect
29 30
@@ -67,8 +68,30 @@ describe('Test video channels API validator', function () {
67 }) 68 })
68 69
69 describe('When listing account video channels', function () { 70 describe('When listing account video channels', function () {
71 const accountChannelPath = '/api/v1/accounts/fake/video-channels'
72
73 it('Should fail with a bad start pagination', async function () {
74 await checkBadStartPagination(server.url, accountChannelPath, server.accessToken)
75 })
76
77 it('Should fail with a bad count pagination', async function () {
78 await checkBadCountPagination(server.url, accountChannelPath, server.accessToken)
79 })
80
81 it('Should fail with an incorrect sort', async function () {
82 await checkBadSortPagination(server.url, accountChannelPath, server.accessToken)
83 })
84
70 it('Should fail with a unknown account', async function () { 85 it('Should fail with a unknown account', async function () {
71 await getAccountVideoChannelsList(server.url, 'unknown', 404) 86 await getAccountVideoChannelsList({ url: server.url, accountName: 'unknown', specialStatus: 404 })
87 })
88
89 it('Should succeed with the correct parameters', async function () {
90 await makeGetRequest({
91 url: server.url,
92 path: accountChannelPath,
93 statusCodeExpected: 200
94 })
72 }) 95 })
73 }) 96 })
74 97
@@ -147,9 +170,11 @@ describe('Test video channels API validator', function () {
147 }) 170 })
148 171
149 describe('When updating a video channel', function () { 172 describe('When updating a video channel', function () {
150 const baseCorrectParams = { 173 const baseCorrectParams: VideoChannelUpdate = {
151 displayName: 'hello', 174 displayName: 'hello',
152 description: 'super description' 175 description: 'super description',
176 support: 'toto',
177 bulkVideosSupportUpdate: false
153 } 178 }
154 let path: string 179 let path: string
155 180
@@ -192,6 +217,11 @@ describe('Test video channels API validator', function () {
192 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 217 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
193 }) 218 })
194 219
220 it('Should fail with a bad bulkVideosSupportUpdate field', async function () {
221 const fields = immutableAssign(baseCorrectParams, { bulkVideosSupportUpdate: 'super' })
222 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
223 })
224
195 it('Should succeed with the correct parameters', async function () { 225 it('Should succeed with the correct parameters', async function () {
196 await makePutBodyRequest({ 226 await makePutBodyRequest({
197 url: server.url, 227 url: server.url,
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts
index b7b94c035..8c5e44bdd 100644
--- a/server/tests/api/check-params/video-playlists.ts
+++ b/server/tests/api/check-params/video-playlists.ts
@@ -205,7 +205,6 @@ describe('Test video playlists API validator', function () {
205 const params = getBase({ displayName: undefined }) 205 const params = getBase({ displayName: undefined })
206 206
207 await createVideoPlaylist(params) 207 await createVideoPlaylist(params)
208 await updateVideoPlaylist(getUpdate(params, playlistUUID))
209 }) 208 })
210 209
211 it('Should fail with an incorrect display name', async function () { 210 it('Should fail with an incorrect display name', async function () {
@@ -269,17 +268,6 @@ describe('Test video playlists API validator', function () {
269 )) 268 ))
270 }) 269 })
271 270
272 it('Should fail to update to private a public/unlisted playlist', async function () {
273 const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC }, { expectedStatus: 200 })
274
275 const res = await createVideoPlaylist(params)
276 const playlist = res.body.videoPlaylist
277
278 const paramsUpdate = getBase({ privacy: VideoPlaylistPrivacy.PRIVATE }, { expectedStatus: 400 })
279
280 await updateVideoPlaylist(getUpdate(paramsUpdate, playlist.id))
281 })
282
283 it('Should fail to update the watch later playlist', async function () { 271 it('Should fail to update the watch later playlist', async function () {
284 await updateVideoPlaylist(getUpdate( 272 await updateVideoPlaylist(getUpdate(
285 getBase({}, { expectedStatus: 400 }), 273 getBase({}, { expectedStatus: 400 }),
diff --git a/server/tests/api/index-1.ts b/server/tests/api/index-1.ts
deleted file mode 100644
index 75cdd9025..000000000
--- a/server/tests/api/index-1.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1import './check-params'
2import './notifications'
3import './search'
diff --git a/server/tests/api/index-2.ts b/server/tests/api/index-2.ts
deleted file mode 100644
index ed93faa91..000000000
--- a/server/tests/api/index-2.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1import './server'
2import './users'
diff --git a/server/tests/api/index-3.ts b/server/tests/api/index-3.ts
deleted file mode 100644
index 39823b82c..000000000
--- a/server/tests/api/index-3.ts
+++ /dev/null
@@ -1 +0,0 @@
1import './videos'
diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts
deleted file mode 100644
index 7d8be2b3d..000000000
--- a/server/tests/api/index-4.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1import './redundancy'
2import './activitypub'
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts
index bc140f860..bac77ab2e 100644
--- a/server/tests/api/index.ts
+++ b/server/tests/api/index.ts
@@ -1,5 +1,9 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './index-1' 2import './activitypub'
3import './index-2' 3import './check-params'
4import './index-3' 4import './notifications'
5import './index-4' 5import './redundancy'
6import './search'
7import './server'
8import './users'
9import './videos'
diff --git a/server/tests/api/notifications/index.ts b/server/tests/api/notifications/index.ts
index 95ac8fc51..b573f850e 100644
--- a/server/tests/api/notifications/index.ts
+++ b/server/tests/api/notifications/index.ts
@@ -1 +1 @@
export * from './user-notifications' import './user-notifications'
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index f479e1785..662b64e05 100644
--- a/server/tests/api/notifications/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -114,11 +114,12 @@ describe('Test users notifications', function () {
114 before(async function () { 114 before(async function () {
115 this.timeout(120000) 115 this.timeout(120000)
116 116
117 await MockSmtpServer.Instance.collectEmails(emails) 117 const port = await MockSmtpServer.Instance.collectEmails(emails)
118 118
119 const overrideConfig = { 119 const overrideConfig = {
120 smtp: { 120 smtp: {
121 hostname: 'localhost' 121 hostname: 'localhost',
122 port
122 } 123 }
123 } 124 }
124 servers = await flushAndRunMultipleServers(3, overrideConfig) 125 servers = await flushAndRunMultipleServers(3, overrideConfig)
@@ -194,7 +195,7 @@ describe('Test users notifications', function () {
194 it('Should send a new video notification if the user follows the local video publisher', async function () { 195 it('Should send a new video notification if the user follows the local video publisher', async function () {
195 this.timeout(15000) 196 this.timeout(15000)
196 197
197 await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9001') 198 await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:' + servers[0].port)
198 await waitJobs(servers) 199 await waitJobs(servers)
199 200
200 const { name, uuid } = await uploadVideoByLocalAccount(servers) 201 const { name, uuid } = await uploadVideoByLocalAccount(servers)
@@ -204,7 +205,7 @@ describe('Test users notifications', function () {
204 it('Should send a new video notification from a remote account', async function () { 205 it('Should send a new video notification from a remote account', async function () {
205 this.timeout(50000) // Server 2 has transcoding enabled 206 this.timeout(50000) // Server 2 has transcoding enabled
206 207
207 await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9002') 208 await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:' + servers[1].port)
208 await waitJobs(servers) 209 await waitJobs(servers)
209 210
210 const { name, uuid } = await uploadVideoByRemoteAccount(servers) 211 const { name, uuid } = await uploadVideoByRemoteAccount(servers)
@@ -578,7 +579,9 @@ describe('Test users notifications', function () {
578 const uuid = resVideo.body.video.uuid 579 const uuid = resVideo.body.video.uuid
579 580
580 await waitJobs(servers) 581 await waitJobs(servers)
581 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'hello @user_1@localhost:9001 1') 582
583 const text1 = `hello @user_1@localhost:${servers[ 0 ].port} 1`
584 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, text1)
582 const server2ThreadId = resThread.body.comment.id 585 const server2ThreadId = resThread.body.comment.id
583 586
584 await waitJobs(servers) 587 await waitJobs(servers)
@@ -588,8 +591,8 @@ describe('Test users notifications', function () {
588 const server1ThreadId = resThread2.body.data[0].id 591 const server1ThreadId = resThread2.body.data[0].id
589 await checkCommentMention(baseParams, uuid, server1ThreadId, server1ThreadId, 'super root 2 name', 'presence') 592 await checkCommentMention(baseParams, uuid, server1ThreadId, server1ThreadId, 'super root 2 name', 'presence')
590 593
591 const text = '@user_1@localhost:9001 hello 2 @root@localhost:9001' 594 const text2 = `@user_1@localhost:${servers[ 0 ].port} hello 2 @root@localhost:${servers[ 0 ].port}`
592 await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, server2ThreadId, text) 595 await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, server2ThreadId, text2)
593 596
594 await waitJobs(servers) 597 await waitJobs(servers)
595 598
@@ -889,10 +892,10 @@ describe('Test users notifications', function () {
889 892
890 await waitJobs(servers) 893 await waitJobs(servers)
891 894
892 await checkNewInstanceFollower(baseParams, 'localhost:9003', 'presence') 895 await checkNewInstanceFollower(baseParams, 'localhost:' + servers[2].port, 'presence')
893 896
894 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } 897 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
895 await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:9003', 'absence') 898 await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence')
896 }) 899 })
897 }) 900 })
898 901
@@ -933,29 +936,29 @@ describe('Test users notifications', function () {
933 it('Should notify when a local channel is following one of our channel', async function () { 936 it('Should notify when a local channel is following one of our channel', async function () {
934 this.timeout(10000) 937 this.timeout(10000)
935 938
936 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') 939 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
937 await waitJobs(servers) 940 await waitJobs(servers)
938 941
939 await checkNewActorFollow(baseParams, 'channel', 'root', 'super root name', myChannelName, 'presence') 942 await checkNewActorFollow(baseParams, 'channel', 'root', 'super root name', myChannelName, 'presence')
940 943
941 await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') 944 await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
942 }) 945 })
943 946
944 it('Should notify when a remote channel is following one of our channel', async function () { 947 it('Should notify when a remote channel is following one of our channel', async function () {
945 this.timeout(10000) 948 this.timeout(10000)
946 949
947 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') 950 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
948 await waitJobs(servers) 951 await waitJobs(servers)
949 952
950 await checkNewActorFollow(baseParams, 'channel', 'root', 'super root 2 name', myChannelName, 'presence') 953 await checkNewActorFollow(baseParams, 'channel', 'root', 'super root 2 name', myChannelName, 'presence')
951 954
952 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') 955 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
953 }) 956 })
954 957
955 it('Should notify when a local account is following one of our channel', async function () { 958 it('Should notify when a local account is following one of our channel', async function () {
956 this.timeout(10000) 959 this.timeout(10000)
957 960
958 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') 961 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:' + servers[0].port)
959 962
960 await waitJobs(servers) 963 await waitJobs(servers)
961 964
@@ -965,7 +968,7 @@ describe('Test users notifications', function () {
965 it('Should notify when a remote account is following one of our channel', async function () { 968 it('Should notify when a remote account is following one of our channel', async function () {
966 this.timeout(10000) 969 this.timeout(10000)
967 970
968 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') 971 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:' + servers[0].port)
969 972
970 await waitJobs(servers) 973 await waitJobs(servers)
971 974
@@ -1019,8 +1022,8 @@ describe('Test users notifications', function () {
1019 autoBlacklistTestsCustomConfig.transcoding.enabled = true 1022 autoBlacklistTestsCustomConfig.transcoding.enabled = true
1020 await updateCustomConfig(servers[0].url, servers[0].accessToken, autoBlacklistTestsCustomConfig) 1023 await updateCustomConfig(servers[0].url, servers[0].accessToken, autoBlacklistTestsCustomConfig)
1021 1024
1022 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') 1025 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
1023 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') 1026 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
1024 1027
1025 }) 1028 })
1026 1029
@@ -1142,8 +1145,8 @@ describe('Test users notifications', function () {
1142 after(async () => { 1145 after(async () => {
1143 await updateCustomConfig(servers[0].url, servers[0].accessToken, currentCustomConfig) 1146 await updateCustomConfig(servers[0].url, servers[0].accessToken, currentCustomConfig)
1144 1147
1145 await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001') 1148 await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:' + servers[0].port)
1146 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001') 1149 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
1147 }) 1150 })
1148 }) 1151 })
1149 1152
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index e31329c25..6f2c59076 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -100,7 +100,7 @@ async function check1WebSeed (videoUUID?: string) {
100 if (!videoUUID) videoUUID = video1Server2UUID 100 if (!videoUUID) videoUUID = video1Server2UUID
101 101
102 const webseeds = [ 102 const webseeds = [
103 'http://localhost:9002/static/webseed/' + videoUUID 103 `http://localhost:${servers[ 1 ].port}/static/webseed/${videoUUID}`
104 ] 104 ]
105 105
106 for (const server of servers) { 106 for (const server of servers) {
@@ -118,8 +118,8 @@ async function check2Webseeds (videoUUID?: string) {
118 if (!videoUUID) videoUUID = video1Server2UUID 118 if (!videoUUID) videoUUID = video1Server2UUID
119 119
120 const webseeds = [ 120 const webseeds = [
121 'http://localhost:9001/static/redundancy/' + videoUUID, 121 `http://localhost:${servers[ 0 ].port}/static/redundancy/${videoUUID}`,
122 'http://localhost:9002/static/webseed/' + videoUUID 122 `http://localhost:${servers[ 1 ].port}/static/webseed/${videoUUID}`
123 ] 123 ]
124 124
125 for (const server of servers) { 125 for (const server of servers) {
@@ -145,7 +145,12 @@ async function check2Webseeds (videoUUID?: string) {
145 } 145 }
146 } 146 }
147 147
148 for (const directory of [ 'test1/redundancy', 'test2/videos' ]) { 148 const directories = [
149 'test' + servers[0].internalServerNumber + '/redundancy',
150 'test' + servers[1].internalServerNumber + '/videos'
151 ]
152
153 for (const directory of directories) {
149 const files = await readdir(join(root(), directory)) 154 const files = await readdir(join(root(), directory))
150 expect(files).to.have.length.at.least(4) 155 expect(files).to.have.length.at.least(4)
151 156
@@ -194,7 +199,12 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
194 await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist) 199 await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
195 } 200 }
196 201
197 for (const directory of [ 'test1/redundancy/hls', 'test2/streaming-playlists/hls' ]) { 202 const directories = [
203 'test' + servers[0].internalServerNumber + '/redundancy/hls',
204 'test' + servers[1].internalServerNumber + '/streaming-playlists/hls'
205 ]
206
207 for (const directory of directories) {
198 const files = await readdir(join(root(), directory, videoUUID)) 208 const files = await readdir(join(root(), directory, videoUUID))
199 expect(files).to.have.length.at.least(4) 209 expect(files).to.have.length.at.least(4)
200 210
@@ -239,8 +249,8 @@ async function enableRedundancyOnServer1 () {
239 249
240 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') 250 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
241 const follows: ActorFollow[] = res.body.data 251 const follows: ActorFollow[] = res.body.data
242 const server2 = follows.find(f => f.following.host === 'localhost:9002') 252 const server2 = follows.find(f => f.following.host === `localhost:${servers[ 1 ].port}`)
243 const server3 = follows.find(f => f.following.host === 'localhost:9003') 253 const server3 = follows.find(f => f.following.host === `localhost:${servers[ 2 ].port}`)
244 254
245 expect(server3).to.not.be.undefined 255 expect(server3).to.not.be.undefined
246 expect(server3.following.hostRedundancyAllowed).to.be.false 256 expect(server3.following.hostRedundancyAllowed).to.be.false
@@ -254,8 +264,8 @@ async function disableRedundancyOnServer1 () {
254 264
255 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') 265 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
256 const follows: ActorFollow[] = res.body.data 266 const follows: ActorFollow[] = res.body.data
257 const server2 = follows.find(f => f.following.host === 'localhost:9002') 267 const server2 = follows.find(f => f.following.host === `localhost:${servers[ 1 ].port}`)
258 const server3 = follows.find(f => f.following.host === 'localhost:9003') 268 const server3 = follows.find(f => f.following.host === `localhost:${servers[ 2 ].port}`)
259 269
260 expect(server3).to.not.be.undefined 270 expect(server3).to.not.be.undefined
261 expect(server3.following.hostRedundancyAllowed).to.be.false 271 expect(server3.following.hostRedundancyAllowed).to.be.false
@@ -475,12 +485,12 @@ describe('Test videos redundancy', function () {
475 await wait(10000) 485 await wait(10000)
476 486
477 try { 487 try {
478 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') 488 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
479 } catch { 489 } catch {
480 // Maybe a server deleted a redundancy in the scheduler 490 // Maybe a server deleted a redundancy in the scheduler
481 await wait(2000) 491 await wait(2000)
482 492
483 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') 493 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
484 } 494 }
485 }) 495 })
486 496
@@ -491,7 +501,7 @@ describe('Test videos redundancy', function () {
491 501
492 await wait(15000) 502 await wait(15000)
493 503
494 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') 504 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A' + servers[0].port)
495 }) 505 })
496 506
497 after(async function () { 507 after(async function () {
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts
index 4d1ceb767..8a008b8c6 100644
--- a/server/tests/api/search/search-activitypub-video-channels.ts
+++ b/server/tests/api/search/search-activitypub-video-channels.ts
@@ -3,16 +3,17 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 addVideoChannel, cleanupTests, 6 addVideoChannel,
7 cleanupTests,
7 createUser, 8 createUser,
8 deleteVideoChannel, 9 deleteVideoChannel,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
10 flushTests, 11 getVideoChannelsList,
11 getVideoChannelsList, getVideoChannelVideos, 12 getVideoChannelVideos,
12 killallServers,
13 ServerInfo, 13 ServerInfo,
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 updateMyUser, updateVideo, 15 updateMyUser,
16 updateVideo,
16 updateVideoChannel, 17 updateVideoChannel,
17 uploadVideo, 18 uploadVideo,
18 userLogin, 19 userLogin,
@@ -24,7 +25,7 @@ import { searchVideoChannel } from '../../../../shared/extra-utils/search/video-
24 25
25const expect = chai.expect 26const expect = chai.expect
26 27
27describe('Test a ActivityPub video channels search', function () { 28describe('Test ActivityPub video channels search', function () {
28 let servers: ServerInfo[] 29 let servers: ServerInfo[]
29 let userServer2Token: string 30 let userServer2Token: string
30 let videoServer2UUID: string 31 let videoServer2UUID: string
@@ -67,7 +68,7 @@ describe('Test a ActivityPub video channels search', function () {
67 68
68 it('Should not find a remote video channel', async function () { 69 it('Should not find a remote video channel', async function () {
69 { 70 {
70 const search = 'http://localhost:9002/video-channels/channel1_server3' 71 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server3'
71 const res = await searchVideoChannel(servers[ 0 ].url, search, servers[ 0 ].accessToken) 72 const res = await searchVideoChannel(servers[ 0 ].url, search, servers[ 0 ].accessToken)
72 73
73 expect(res.body.total).to.equal(0) 74 expect(res.body.total).to.equal(0)
@@ -77,7 +78,7 @@ describe('Test a ActivityPub video channels search', function () {
77 78
78 { 79 {
79 // Without token 80 // Without token
80 const search = 'http://localhost:9002/video-channels/channel1_server2' 81 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2'
81 const res = await searchVideoChannel(servers[0].url, search) 82 const res = await searchVideoChannel(servers[0].url, search)
82 83
83 expect(res.body.total).to.equal(0) 84 expect(res.body.total).to.equal(0)
@@ -88,8 +89,8 @@ describe('Test a ActivityPub video channels search', function () {
88 89
89 it('Should search a local video channel', async function () { 90 it('Should search a local video channel', async function () {
90 const searches = [ 91 const searches = [
91 'http://localhost:9001/video-channels/channel1_server1', 92 'http://localhost:' + servers[ 0 ].port + '/video-channels/channel1_server1',
92 'channel1_server1@localhost:9001' 93 'channel1_server1@localhost:' + servers[ 0 ].port
93 ] 94 ]
94 95
95 for (const search of searches) { 96 for (const search of searches) {
@@ -105,8 +106,8 @@ describe('Test a ActivityPub video channels search', function () {
105 106
106 it('Should search a remote video channel with URL or handle', async function () { 107 it('Should search a remote video channel with URL or handle', async function () {
107 const searches = [ 108 const searches = [
108 'http://localhost:9002/video-channels/channel1_server2', 109 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2',
109 'channel1_server2@localhost:9002' 110 'channel1_server2@localhost:' + servers[ 1 ].port
110 ] 111 ]
111 112
112 for (const search of searches) { 113 for (const search of searches) {
@@ -134,13 +135,13 @@ describe('Test a ActivityPub video channels search', function () {
134 135
135 await waitJobs(servers) 136 await waitJobs(servers)
136 137
137 const res = await getVideoChannelVideos(servers[0].url, null, 'channel1_server2@localhost:9002', 0, 5) 138 const res = await getVideoChannelVideos(servers[0].url, null, 'channel1_server2@localhost:' + servers[ 1 ].port, 0, 5)
138 expect(res.body.total).to.equal(0) 139 expect(res.body.total).to.equal(0)
139 expect(res.body.data).to.have.lengthOf(0) 140 expect(res.body.data).to.have.lengthOf(0)
140 }) 141 })
141 142
142 it('Should list video channel videos of server 2 with token', async function () { 143 it('Should list video channel videos of server 2 with token', async function () {
143 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:9002', 0, 5) 144 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:' + servers[ 1 ].port, 0, 5)
144 145
145 expect(res.body.total).to.equal(1) 146 expect(res.body.total).to.equal(1)
146 expect(res.body.data[0].name).to.equal('video 1 server 2') 147 expect(res.body.data[0].name).to.equal('video 1 server 2')
@@ -156,7 +157,7 @@ describe('Test a ActivityPub video channels search', function () {
156 // Expire video channel 157 // Expire video channel
157 await wait(10000) 158 await wait(10000)
158 159
159 const search = 'http://localhost:9002/video-channels/channel1_server2' 160 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2'
160 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken) 161 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
161 expect(res.body.total).to.equal(1) 162 expect(res.body.total).to.equal(1)
162 expect(res.body.data).to.have.lengthOf(1) 163 expect(res.body.data).to.have.lengthOf(1)
@@ -179,12 +180,13 @@ describe('Test a ActivityPub video channels search', function () {
179 // Expire video channel 180 // Expire video channel
180 await wait(10000) 181 await wait(10000)
181 182
182 const search = 'http://localhost:9002/video-channels/channel1_server2' 183 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2'
183 await searchVideoChannel(servers[0].url, search, servers[0].accessToken) 184 await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
184 185
185 await waitJobs(servers) 186 await waitJobs(servers)
186 187
187 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:9002', 0, 5, '-createdAt') 188 const videoChannelName = 'channel1_server2@localhost:' + servers[ 1 ].port
189 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, videoChannelName, 0, 5, '-createdAt')
188 190
189 expect(res.body.total).to.equal(2) 191 expect(res.body.total).to.equal(2)
190 expect(res.body.data[0].name).to.equal('video 2 server 2') 192 expect(res.body.data[0].name).to.equal('video 2 server 2')
@@ -200,7 +202,8 @@ describe('Test a ActivityPub video channels search', function () {
200 // Expire video 202 // Expire video
201 await wait(10000) 203 await wait(10000)
202 204
203 const res = await searchVideoChannel(servers[0].url, 'http://localhost:9002/video-channels/channel1_server2', servers[0].accessToken) 205 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2'
206 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
204 expect(res.body.total).to.equal(0) 207 expect(res.body.total).to.equal(0)
205 expect(res.body.data).to.have.lengthOf(0) 208 expect(res.body.data).to.have.lengthOf(0)
206 }) 209 })
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts
index e039961cb..dbfefadda 100644
--- a/server/tests/api/search/search-activitypub-videos.ts
+++ b/server/tests/api/search/search-activitypub-videos.ts
@@ -4,25 +4,24 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 addVideoChannel, 6 addVideoChannel,
7 cleanupTests,
7 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
8 flushTests,
9 getVideosList, 9 getVideosList,
10 killallServers,
11 removeVideo, 10 removeVideo,
11 searchVideo,
12 searchVideoWithToken, 12 searchVideoWithToken,
13 ServerInfo, 13 ServerInfo,
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 updateVideo, 15 updateVideo,
16 uploadVideo, 16 uploadVideo,
17 wait, 17 wait
18 searchVideo, cleanupTests
19} from '../../../../shared/extra-utils' 18} from '../../../../shared/extra-utils'
20import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 19import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21import { Video, VideoPrivacy } from '../../../../shared/models/videos' 20import { Video, VideoPrivacy } from '../../../../shared/models/videos'
22 21
23const expect = chai.expect 22const expect = chai.expect
24 23
25describe('Test a ActivityPub videos search', function () { 24describe('Test ActivityPub videos search', function () {
26 let servers: ServerInfo[] 25 let servers: ServerInfo[]
27 let videoServer1UUID: string 26 let videoServer1UUID: string
28 let videoServer2UUID: string 27 let videoServer2UUID: string
@@ -49,7 +48,8 @@ describe('Test a ActivityPub videos search', function () {
49 48
50 it('Should not find a remote video', async function () { 49 it('Should not find a remote video', async function () {
51 { 50 {
52 const res = await searchVideoWithToken(servers[ 0 ].url, 'http://localhost:9002/videos/watch/43', servers[ 0 ].accessToken) 51 const search = 'http://localhost:' + servers[1].port + '/videos/watch/43'
52 const res = await searchVideoWithToken(servers[ 0 ].url, search, servers[ 0 ].accessToken)
53 53
54 expect(res.body.total).to.equal(0) 54 expect(res.body.total).to.equal(0)
55 expect(res.body.data).to.be.an('array') 55 expect(res.body.data).to.be.an('array')
@@ -58,7 +58,8 @@ describe('Test a ActivityPub videos search', function () {
58 58
59 { 59 {
60 // Without token 60 // Without token
61 const res = await searchVideo(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID) 61 const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID
62 const res = await searchVideo(servers[0].url, search)
62 63
63 expect(res.body.total).to.equal(0) 64 expect(res.body.total).to.equal(0)
64 expect(res.body.data).to.be.an('array') 65 expect(res.body.data).to.be.an('array')
@@ -67,7 +68,8 @@ describe('Test a ActivityPub videos search', function () {
67 }) 68 })
68 69
69 it('Should search a local video', async function () { 70 it('Should search a local video', async function () {
70 const res = await searchVideo(servers[0].url, 'http://localhost:9001/videos/watch/' + videoServer1UUID) 71 const search = 'http://localhost:' + servers[0].port + '/videos/watch/' + videoServer1UUID
72 const res = await searchVideo(servers[0].url, search)
71 73
72 expect(res.body.total).to.equal(1) 74 expect(res.body.total).to.equal(1)
73 expect(res.body.data).to.be.an('array') 75 expect(res.body.data).to.be.an('array')
@@ -76,7 +78,8 @@ describe('Test a ActivityPub videos search', function () {
76 }) 78 })
77 79
78 it('Should search a remote video', async function () { 80 it('Should search a remote video', async function () {
79 const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken) 81 const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID
82 const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
80 83
81 expect(res.body.total).to.equal(1) 84 expect(res.body.total).to.equal(1)
82 expect(res.body.data).to.be.an('array') 85 expect(res.body.data).to.be.an('array')
@@ -114,12 +117,13 @@ describe('Test a ActivityPub videos search', function () {
114 await wait(10000) 117 await wait(10000)
115 118
116 // Will run refresh async 119 // Will run refresh async
117 await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken) 120 const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID
121 await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
118 122
119 // Wait refresh 123 // Wait refresh
120 await wait(5000) 124 await wait(5000)
121 125
122 const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken) 126 const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
123 expect(res.body.total).to.equal(1) 127 expect(res.body.total).to.equal(1)
124 expect(res.body.data).to.have.lengthOf(1) 128 expect(res.body.data).to.have.lengthOf(1)
125 129
@@ -139,12 +143,13 @@ describe('Test a ActivityPub videos search', function () {
139 await wait(10000) 143 await wait(10000)
140 144
141 // Will run refresh async 145 // Will run refresh async
142 await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken) 146 const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID
147 await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
143 148
144 // Wait refresh 149 // Wait refresh
145 await wait(5000) 150 await wait(5000)
146 151
147 const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken) 152 const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
148 expect(res.body.total).to.equal(0) 153 expect(res.body.total).to.equal(0)
149 expect(res.body.data).to.have.lengthOf(0) 154 expect(res.body.data).to.have.lengthOf(0)
150 }) 155 })
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts
index 1a086b33a..92cc0dc71 100644
--- a/server/tests/api/search/search-videos.ts
+++ b/server/tests/api/search/search-videos.ts
@@ -4,21 +4,19 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 advancedVideosSearch, 6 advancedVideosSearch,
7 flushTests, 7 cleanupTests,
8 killallServers,
9 flushAndRunServer, 8 flushAndRunServer,
9 immutableAssign,
10 searchVideo, 10 searchVideo,
11 ServerInfo, 11 ServerInfo,
12 setAccessTokensToServers, 12 setAccessTokensToServers,
13 uploadVideo, 13 uploadVideo,
14 wait, 14 wait
15 immutableAssign,
16 cleanupTests
17} from '../../../../shared/extra-utils' 15} from '../../../../shared/extra-utils'
18 16
19const expect = chai.expect 17const expect = chai.expect
20 18
21describe('Test a videos search', function () { 19describe('Test videos search', function () {
22 let server: ServerInfo = null 20 let server: ServerInfo = null
23 let startDate: string 21 let startDate: string
24 22
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index c0d11914b..c39516dee 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -11,17 +11,17 @@ import {
11 getAbout, 11 getAbout,
12 getConfig, 12 getConfig,
13 getCustomConfig, 13 getCustomConfig,
14 killallServers, 14 killallServers, parallelTests,
15 registerUser, 15 registerUser,
16 reRunServer, 16 reRunServer, ServerInfo,
17 setAccessTokensToServers, 17 setAccessTokensToServers,
18 updateCustomConfig 18 updateCustomConfig, uploadVideo
19} from '../../../../shared/extra-utils' 19} from '../../../../shared/extra-utils'
20import { ServerConfig } from '../../../../shared/models' 20import { ServerConfig } from '../../../../shared/models'
21 21
22const expect = chai.expect 22const expect = chai.expect
23 23
24function checkInitialConfig (data: CustomConfig) { 24function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
25 expect(data.instance.name).to.equal('PeerTube') 25 expect(data.instance.name).to.equal('PeerTube')
26 expect(data.instance.shortDescription).to.equal( 26 expect(data.instance.shortDescription).to.equal(
27 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser ' + 27 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser ' +
@@ -45,19 +45,21 @@ function checkInitialConfig (data: CustomConfig) {
45 expect(data.signup.limit).to.equal(4) 45 expect(data.signup.limit).to.equal(4)
46 expect(data.signup.requiresEmailVerification).to.be.false 46 expect(data.signup.requiresEmailVerification).to.be.false
47 47
48 expect(data.admin.email).to.equal('admin1@example.com') 48 expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com')
49 expect(data.contactForm.enabled).to.be.true 49 expect(data.contactForm.enabled).to.be.true
50 50
51 expect(data.user.videoQuota).to.equal(5242880) 51 expect(data.user.videoQuota).to.equal(5242880)
52 expect(data.user.videoQuotaDaily).to.equal(-1) 52 expect(data.user.videoQuotaDaily).to.equal(-1)
53 expect(data.transcoding.enabled).to.be.false 53 expect(data.transcoding.enabled).to.be.false
54 expect(data.transcoding.allowAdditionalExtensions).to.be.false 54 expect(data.transcoding.allowAdditionalExtensions).to.be.false
55 expect(data.transcoding.allowAudioFiles).to.be.false
55 expect(data.transcoding.threads).to.equal(2) 56 expect(data.transcoding.threads).to.equal(2)
56 expect(data.transcoding.resolutions['240p']).to.be.true 57 expect(data.transcoding.resolutions['240p']).to.be.true
57 expect(data.transcoding.resolutions['360p']).to.be.true 58 expect(data.transcoding.resolutions['360p']).to.be.true
58 expect(data.transcoding.resolutions['480p']).to.be.true 59 expect(data.transcoding.resolutions['480p']).to.be.true
59 expect(data.transcoding.resolutions['720p']).to.be.true 60 expect(data.transcoding.resolutions['720p']).to.be.true
60 expect(data.transcoding.resolutions['1080p']).to.be.true 61 expect(data.transcoding.resolutions['1080p']).to.be.true
62 expect(data.transcoding.resolutions['2160p']).to.be.true
61 expect(data.transcoding.hls.enabled).to.be.true 63 expect(data.transcoding.hls.enabled).to.be.true
62 64
63 expect(data.import.videos.http.enabled).to.be.true 65 expect(data.import.videos.http.enabled).to.be.true
@@ -89,7 +91,11 @@ function checkUpdatedConfig (data: CustomConfig) {
89 expect(data.signup.limit).to.equal(5) 91 expect(data.signup.limit).to.equal(5)
90 expect(data.signup.requiresEmailVerification).to.be.false 92 expect(data.signup.requiresEmailVerification).to.be.false
91 93
92 expect(data.admin.email).to.equal('superadmin1@example.com') 94 // We override admin email in parallel tests, so skip this exception
95 if (parallelTests() === false) {
96 expect(data.admin.email).to.equal('superadmin1@example.com')
97 }
98
93 expect(data.contactForm.enabled).to.be.false 99 expect(data.contactForm.enabled).to.be.false
94 100
95 expect(data.user.videoQuota).to.equal(5242881) 101 expect(data.user.videoQuota).to.equal(5242881)
@@ -98,11 +104,13 @@ function checkUpdatedConfig (data: CustomConfig) {
98 expect(data.transcoding.enabled).to.be.true 104 expect(data.transcoding.enabled).to.be.true
99 expect(data.transcoding.threads).to.equal(1) 105 expect(data.transcoding.threads).to.equal(1)
100 expect(data.transcoding.allowAdditionalExtensions).to.be.true 106 expect(data.transcoding.allowAdditionalExtensions).to.be.true
107 expect(data.transcoding.allowAudioFiles).to.be.true
101 expect(data.transcoding.resolutions['240p']).to.be.false 108 expect(data.transcoding.resolutions['240p']).to.be.false
102 expect(data.transcoding.resolutions['360p']).to.be.true 109 expect(data.transcoding.resolutions['360p']).to.be.true
103 expect(data.transcoding.resolutions['480p']).to.be.true 110 expect(data.transcoding.resolutions['480p']).to.be.true
104 expect(data.transcoding.resolutions['720p']).to.be.false 111 expect(data.transcoding.resolutions['720p']).to.be.false
105 expect(data.transcoding.resolutions['1080p']).to.be.false 112 expect(data.transcoding.resolutions['1080p']).to.be.false
113 expect(data.transcoding.resolutions['2160p']).to.be.false
106 expect(data.transcoding.hls.enabled).to.be.false 114 expect(data.transcoding.hls.enabled).to.be.false
107 115
108 expect(data.import.videos.http.enabled).to.be.false 116 expect(data.import.videos.http.enabled).to.be.false
@@ -118,6 +126,7 @@ describe('Test config', function () {
118 126
119 before(async function () { 127 before(async function () {
120 this.timeout(30000) 128 this.timeout(30000)
129
121 server = await flushAndRunServer(1) 130 server = await flushAndRunServer(1)
122 await setAccessTokensToServers([ server ]) 131 await setAccessTokensToServers([ server ])
123 }) 132 })
@@ -153,6 +162,9 @@ describe('Test config', function () {
153 expect(data.video.file.extensions).to.contain('.webm') 162 expect(data.video.file.extensions).to.contain('.webm')
154 expect(data.video.file.extensions).to.contain('.ogv') 163 expect(data.video.file.extensions).to.contain('.ogv')
155 164
165 await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.mkv' }, 400)
166 await uploadVideo(server.url, server.accessToken, { fixture: 'sample.ogg' }, 400)
167
156 expect(data.contactForm.enabled).to.be.true 168 expect(data.contactForm.enabled).to.be.true
157 }) 169 })
158 170
@@ -160,7 +172,7 @@ describe('Test config', function () {
160 const res = await getCustomConfig(server.url, server.accessToken) 172 const res = await getCustomConfig(server.url, server.accessToken)
161 const data = res.body as CustomConfig 173 const data = res.body as CustomConfig
162 174
163 checkInitialConfig(data) 175 checkInitialConfig(server, data)
164 }) 176 })
165 177
166 it('Should update the customized configuration', async function () { 178 it('Should update the customized configuration', async function () {
@@ -210,13 +222,15 @@ describe('Test config', function () {
210 transcoding: { 222 transcoding: {
211 enabled: true, 223 enabled: true,
212 allowAdditionalExtensions: true, 224 allowAdditionalExtensions: true,
225 allowAudioFiles: true,
213 threads: 1, 226 threads: 1,
214 resolutions: { 227 resolutions: {
215 '240p': false, 228 '240p': false,
216 '360p': true, 229 '360p': true,
217 '480p': true, 230 '480p': true,
218 '720p': false, 231 '720p': false,
219 '1080p': false 232 '1080p': false,
233 '2160p': false
220 }, 234 },
221 hls: { 235 hls: {
222 enabled: false 236 enabled: false
@@ -264,6 +278,12 @@ describe('Test config', function () {
264 expect(data.video.file.extensions).to.contain('.ogv') 278 expect(data.video.file.extensions).to.contain('.ogv')
265 expect(data.video.file.extensions).to.contain('.flv') 279 expect(data.video.file.extensions).to.contain('.flv')
266 expect(data.video.file.extensions).to.contain('.mkv') 280 expect(data.video.file.extensions).to.contain('.mkv')
281 expect(data.video.file.extensions).to.contain('.mp3')
282 expect(data.video.file.extensions).to.contain('.ogg')
283 expect(data.video.file.extensions).to.contain('.flac')
284
285 await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.mkv' }, 200)
286 await uploadVideo(server.url, server.accessToken, { fixture: 'sample.ogg' }, 200)
267 }) 287 })
268 288
269 it('Should have the configuration updated after a restart', async function () { 289 it('Should have the configuration updated after a restart', async function () {
@@ -297,7 +317,7 @@ describe('Test config', function () {
297 const res = await getCustomConfig(server.url, server.accessToken) 317 const res = await getCustomConfig(server.url, server.accessToken)
298 const data = res.body 318 const data = res.body
299 319
300 checkInitialConfig(data) 320 checkInitialConfig(server, data)
301 }) 321 })
302 322
303 after(async function () { 323 after(async function () {
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts
index ba51198b3..87e55060c 100644
--- a/server/tests/api/server/contact-form.ts
+++ b/server/tests/api/server/contact-form.ts
@@ -24,11 +24,12 @@ describe('Test contact form', function () {
24 before(async function () { 24 before(async function () {
25 this.timeout(30000) 25 this.timeout(30000)
26 26
27 await MockSmtpServer.Instance.collectEmails(emails) 27 const port = await MockSmtpServer.Instance.collectEmails(emails)
28 28
29 const overrideConfig = { 29 const overrideConfig = {
30 smtp: { 30 smtp: {
31 hostname: 'localhost' 31 hostname: 'localhost',
32 port
32 } 33 }
33 } 34 }
34 server = await flushAndRunServer(1, overrideConfig) 35 server = await flushAndRunServer(1, overrideConfig)
@@ -53,7 +54,7 @@ describe('Test contact form', function () {
53 54
54 expect(email['from'][0]['address']).equal('test-admin@localhost') 55 expect(email['from'][0]['address']).equal('test-admin@localhost')
55 expect(email['from'][0]['name']).equal('toto@example.com') 56 expect(email['from'][0]['name']).equal('toto@example.com')
56 expect(email['to'][0]['address']).equal('admin1@example.com') 57 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
57 expect(email['subject']).contains('Contact form') 58 expect(email['subject']).contains('Contact form')
58 expect(email['text']).contains('my super message') 59 expect(email['text']).contains('my super message')
59 }) 60 })
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index bacdf1b1b..7b7acfd12 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -7,18 +7,18 @@ import {
7 askResetPassword, 7 askResetPassword,
8 askSendVerifyEmail, 8 askSendVerifyEmail,
9 blockUser, 9 blockUser,
10 createUser, removeVideoFromBlacklist, 10 cleanupTests,
11 createUser,
12 flushAndRunServer,
13 removeVideoFromBlacklist,
11 reportVideoAbuse, 14 reportVideoAbuse,
12 resetPassword, 15 resetPassword,
13 flushAndRunServer, 16 ServerInfo,
17 setAccessTokensToServers,
14 unblockUser, 18 unblockUser,
15 uploadVideo, 19 uploadVideo,
16 userLogin, 20 userLogin,
17 verifyEmail, 21 verifyEmail
18 flushTests,
19 killallServers,
20 ServerInfo,
21 setAccessTokensToServers, cleanupTests
22} from '../../../../shared/extra-utils' 22} from '../../../../shared/extra-utils'
23import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' 23import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
24import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 24import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
@@ -37,15 +37,17 @@ describe('Test emails', function () {
37 username: 'user_1', 37 username: 'user_1',
38 password: 'super_password' 38 password: 'super_password'
39 } 39 }
40 let emailPort: number
40 41
41 before(async function () { 42 before(async function () {
42 this.timeout(30000) 43 this.timeout(30000)
43 44
44 await MockSmtpServer.Instance.collectEmails(emails) 45 emailPort = await MockSmtpServer.Instance.collectEmails(emails)
45 46
46 const overrideConfig = { 47 const overrideConfig = {
47 smtp: { 48 smtp: {
48 hostname: 'localhost' 49 hostname: 'localhost',
50 port: emailPort
49 } 51 }
50 } 52 }
51 server = await flushAndRunServer(1, overrideConfig) 53 server = await flushAndRunServer(1, overrideConfig)
@@ -87,7 +89,7 @@ describe('Test emails', function () {
87 89
88 const email = emails[0] 90 const email = emails[0]
89 91
90 expect(email['from'][0]['name']).equal('localhost:9001') 92 expect(email['from'][0]['name']).equal('localhost:' + server.port)
91 expect(email['from'][0]['address']).equal('test-admin@localhost') 93 expect(email['from'][0]['address']).equal('test-admin@localhost')
92 expect(email['to'][0]['address']).equal('user_1@example.com') 94 expect(email['to'][0]['address']).equal('user_1@example.com')
93 expect(email['subject']).contains('password') 95 expect(email['subject']).contains('password')
@@ -132,9 +134,9 @@ describe('Test emails', function () {
132 134
133 const email = emails[1] 135 const email = emails[1]
134 136
135 expect(email['from'][0]['name']).equal('localhost:9001') 137 expect(email['from'][0]['name']).equal('localhost:' + server.port)
136 expect(email['from'][0]['address']).equal('test-admin@localhost') 138 expect(email['from'][0]['address']).equal('test-admin@localhost')
137 expect(email['to'][0]['address']).equal('admin1@example.com') 139 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
138 expect(email['subject']).contains('abuse') 140 expect(email['subject']).contains('abuse')
139 expect(email['text']).contains(videoUUID) 141 expect(email['text']).contains(videoUUID)
140 }) 142 })
@@ -153,7 +155,7 @@ describe('Test emails', function () {
153 155
154 const email = emails[2] 156 const email = emails[2]
155 157
156 expect(email['from'][0]['name']).equal('localhost:9001') 158 expect(email['from'][0]['name']).equal('localhost:' + server.port)
157 expect(email['from'][0]['address']).equal('test-admin@localhost') 159 expect(email['from'][0]['address']).equal('test-admin@localhost')
158 expect(email['to'][0]['address']).equal('user_1@example.com') 160 expect(email['to'][0]['address']).equal('user_1@example.com')
159 expect(email['subject']).contains(' blocked') 161 expect(email['subject']).contains(' blocked')
@@ -171,7 +173,7 @@ describe('Test emails', function () {
171 173
172 const email = emails[3] 174 const email = emails[3]
173 175
174 expect(email['from'][0]['name']).equal('localhost:9001') 176 expect(email['from'][0]['name']).equal('localhost:' + server.port)
175 expect(email['from'][0]['address']).equal('test-admin@localhost') 177 expect(email['from'][0]['address']).equal('test-admin@localhost')
176 expect(email['to'][0]['address']).equal('user_1@example.com') 178 expect(email['to'][0]['address']).equal('user_1@example.com')
177 expect(email['subject']).contains(' unblocked') 179 expect(email['subject']).contains(' unblocked')
@@ -191,7 +193,7 @@ describe('Test emails', function () {
191 193
192 const email = emails[4] 194 const email = emails[4]
193 195
194 expect(email['from'][0]['name']).equal('localhost:9001') 196 expect(email['from'][0]['name']).equal('localhost:' + server.port)
195 expect(email['from'][0]['address']).equal('test-admin@localhost') 197 expect(email['from'][0]['address']).equal('test-admin@localhost')
196 expect(email['to'][0]['address']).equal('user_1@example.com') 198 expect(email['to'][0]['address']).equal('user_1@example.com')
197 expect(email['subject']).contains(' blacklisted') 199 expect(email['subject']).contains(' blacklisted')
@@ -209,7 +211,7 @@ describe('Test emails', function () {
209 211
210 const email = emails[5] 212 const email = emails[5]
211 213
212 expect(email['from'][0]['name']).equal('localhost:9001') 214 expect(email['from'][0]['name']).equal('localhost:' + server.port)
213 expect(email['from'][0]['address']).equal('test-admin@localhost') 215 expect(email['from'][0]['address']).equal('test-admin@localhost')
214 expect(email['to'][0]['address']).equal('user_1@example.com') 216 expect(email['to'][0]['address']).equal('user_1@example.com')
215 expect(email['subject']).contains(' unblacklisted') 217 expect(email['subject']).contains(' unblacklisted')
@@ -229,7 +231,7 @@ describe('Test emails', function () {
229 231
230 const email = emails[6] 232 const email = emails[6]
231 233
232 expect(email['from'][0]['name']).equal('localhost:9001') 234 expect(email['from'][0]['name']).equal('localhost:' + server.port)
233 expect(email['from'][0]['address']).equal('test-admin@localhost') 235 expect(email['from'][0]['address']).equal('test-admin@localhost')
234 expect(email['to'][0]['address']).equal('user_1@example.com') 236 expect(email['to'][0]['address']).equal('user_1@example.com')
235 expect(email['subject']).contains('Verify') 237 expect(email['subject']).contains('Verify')
@@ -248,7 +250,7 @@ describe('Test emails', function () {
248 }) 250 })
249 251
250 it('Should not verify the email with an invalid verification string', async function () { 252 it('Should not verify the email with an invalid verification string', async function () {
251 await verifyEmail(server.url, userId, verificationString + 'b', 403) 253 await verifyEmail(server.url, userId, verificationString + 'b', false, 403)
252 }) 254 })
253 255
254 it('Should verify the email', async function () { 256 it('Should verify the email', async function () {
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts
index 4285a9e7a..ac3ff37f0 100644
--- a/server/tests/api/server/follow-constraints.ts
+++ b/server/tests/api/server/follow-constraints.ts
@@ -3,16 +3,16 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 doubleFollow, 7 doubleFollow,
8 flushAndRunMultipleServers,
7 getAccountVideos, 9 getAccountVideos,
8 getVideo, 10 getVideo,
9 getVideoChannelVideos, 11 getVideoChannelVideos,
10 getVideoWithToken, 12 getVideoWithToken,
11 flushAndRunMultipleServers,
12 killallServers,
13 ServerInfo, 13 ServerInfo,
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 uploadVideo, cleanupTests 15 uploadVideo
16} from '../../../../shared/extra-utils' 16} from '../../../../shared/extra-utils'
17import { unfollow } from '../../../../shared/extra-utils/server/follows' 17import { unfollow } from '../../../../shared/extra-utils/server/follows'
18import { userLogin } from '../../../../shared/extra-utils/users/login' 18import { userLogin } from '../../../../shared/extra-utils/users/login'
@@ -66,28 +66,30 @@ describe('Test follow constraints', function () {
66 }) 66 })
67 67
68 it('Should list local account videos', async function () { 68 it('Should list local account videos', async function () {
69 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5) 69 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[0].port, 0, 5)
70 70
71 expect(res.body.total).to.equal(1) 71 expect(res.body.total).to.equal(1)
72 expect(res.body.data).to.have.lengthOf(1) 72 expect(res.body.data).to.have.lengthOf(1)
73 }) 73 })
74 74
75 it('Should list remote account videos', async function () { 75 it('Should list remote account videos', async function () {
76 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5) 76 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[1].port, 0, 5)
77 77
78 expect(res.body.total).to.equal(1) 78 expect(res.body.total).to.equal(1)
79 expect(res.body.data).to.have.lengthOf(1) 79 expect(res.body.data).to.have.lengthOf(1)
80 }) 80 })
81 81
82 it('Should list local channel videos', async function () { 82 it('Should list local channel videos', async function () {
83 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5) 83 const videoChannelName = 'root_channel@localhost:' + servers[0].port
84 const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
84 85
85 expect(res.body.total).to.equal(1) 86 expect(res.body.total).to.equal(1)
86 expect(res.body.data).to.have.lengthOf(1) 87 expect(res.body.data).to.have.lengthOf(1)
87 }) 88 })
88 89
89 it('Should list remote channel videos', async function () { 90 it('Should list remote channel videos', async function () {
90 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5) 91 const videoChannelName = 'root_channel@localhost:' + servers[1].port
92 const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
91 93
92 expect(res.body.total).to.equal(1) 94 expect(res.body.total).to.equal(1)
93 expect(res.body.data).to.have.lengthOf(1) 95 expect(res.body.data).to.have.lengthOf(1)
@@ -104,28 +106,30 @@ describe('Test follow constraints', function () {
104 }) 106 })
105 107
106 it('Should list local account videos', async function () { 108 it('Should list local account videos', async function () {
107 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5) 109 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[0].port, 0, 5)
108 110
109 expect(res.body.total).to.equal(1) 111 expect(res.body.total).to.equal(1)
110 expect(res.body.data).to.have.lengthOf(1) 112 expect(res.body.data).to.have.lengthOf(1)
111 }) 113 })
112 114
113 it('Should list remote account videos', async function () { 115 it('Should list remote account videos', async function () {
114 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5) 116 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[1].port, 0, 5)
115 117
116 expect(res.body.total).to.equal(1) 118 expect(res.body.total).to.equal(1)
117 expect(res.body.data).to.have.lengthOf(1) 119 expect(res.body.data).to.have.lengthOf(1)
118 }) 120 })
119 121
120 it('Should list local channel videos', async function () { 122 it('Should list local channel videos', async function () {
121 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5) 123 const videoChannelName = 'root_channel@localhost:' + servers[0].port
124 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
122 125
123 expect(res.body.total).to.equal(1) 126 expect(res.body.total).to.equal(1)
124 expect(res.body.data).to.have.lengthOf(1) 127 expect(res.body.data).to.have.lengthOf(1)
125 }) 128 })
126 129
127 it('Should list remote channel videos', async function () { 130 it('Should list remote channel videos', async function () {
128 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5) 131 const videoChannelName = 'root_channel@localhost:' + servers[1].port
132 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
129 133
130 expect(res.body.total).to.equal(1) 134 expect(res.body.total).to.equal(1)
131 expect(res.body.data).to.have.lengthOf(1) 135 expect(res.body.data).to.have.lengthOf(1)
@@ -152,28 +156,30 @@ describe('Test follow constraints', function () {
152 }) 156 })
153 157
154 it('Should list local account videos', async function () { 158 it('Should list local account videos', async function () {
155 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5) 159 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[0].port, 0, 5)
156 160
157 expect(res.body.total).to.equal(1) 161 expect(res.body.total).to.equal(1)
158 expect(res.body.data).to.have.lengthOf(1) 162 expect(res.body.data).to.have.lengthOf(1)
159 }) 163 })
160 164
161 it('Should not list remote account videos', async function () { 165 it('Should not list remote account videos', async function () {
162 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5) 166 const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:' + servers[1].port, 0, 5)
163 167
164 expect(res.body.total).to.equal(0) 168 expect(res.body.total).to.equal(0)
165 expect(res.body.data).to.have.lengthOf(0) 169 expect(res.body.data).to.have.lengthOf(0)
166 }) 170 })
167 171
168 it('Should list local channel videos', async function () { 172 it('Should list local channel videos', async function () {
169 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5) 173 const videoChannelName = 'root_channel@localhost:' + servers[0].port
174 const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
170 175
171 expect(res.body.total).to.equal(1) 176 expect(res.body.total).to.equal(1)
172 expect(res.body.data).to.have.lengthOf(1) 177 expect(res.body.data).to.have.lengthOf(1)
173 }) 178 })
174 179
175 it('Should not list remote channel videos', async function () { 180 it('Should not list remote channel videos', async function () {
176 const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5) 181 const videoChannelName = 'root_channel@localhost:' + servers[1].port
182 const res = await getVideoChannelVideos(servers[0].url, undefined, videoChannelName, 0, 5)
177 183
178 expect(res.body.total).to.equal(0) 184 expect(res.body.total).to.equal(0)
179 expect(res.body.data).to.have.lengthOf(0) 185 expect(res.body.data).to.have.lengthOf(0)
@@ -190,28 +196,30 @@ describe('Test follow constraints', function () {
190 }) 196 })
191 197
192 it('Should list local account videos', async function () { 198 it('Should list local account videos', async function () {
193 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5) 199 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[0].port, 0, 5)
194 200
195 expect(res.body.total).to.equal(1) 201 expect(res.body.total).to.equal(1)
196 expect(res.body.data).to.have.lengthOf(1) 202 expect(res.body.data).to.have.lengthOf(1)
197 }) 203 })
198 204
199 it('Should list remote account videos', async function () { 205 it('Should list remote account videos', async function () {
200 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5) 206 const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:' + servers[1].port, 0, 5)
201 207
202 expect(res.body.total).to.equal(1) 208 expect(res.body.total).to.equal(1)
203 expect(res.body.data).to.have.lengthOf(1) 209 expect(res.body.data).to.have.lengthOf(1)
204 }) 210 })
205 211
206 it('Should list local channel videos', async function () { 212 it('Should list local channel videos', async function () {
207 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5) 213 const videoChannelName = 'root_channel@localhost:' + servers[0].port
214 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
208 215
209 expect(res.body.total).to.equal(1) 216 expect(res.body.total).to.equal(1)
210 expect(res.body.data).to.have.lengthOf(1) 217 expect(res.body.data).to.have.lengthOf(1)
211 }) 218 })
212 219
213 it('Should list remote channel videos', async function () { 220 it('Should list remote channel videos', async function () {
214 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5) 221 const videoChannelName = 'root_channel@localhost:' + servers[1].port
222 const res = await getVideoChannelVideos(servers[0].url, userAccessToken, videoChannelName, 0, 5)
215 223
216 expect(res.body.total).to.equal(1) 224 expect(res.body.total).to.equal(1)
217 expect(res.body.data).to.have.lengthOf(1) 225 expect(res.body.data).to.have.lengthOf(1)
diff --git a/server/tests/api/server/follows-moderation.ts b/server/tests/api/server/follows-moderation.ts
index 2a3a4d5c8..a82acdb34 100644
--- a/server/tests/api/server/follows-moderation.ts
+++ b/server/tests/api/server/follows-moderation.ts
@@ -3,9 +3,9 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 acceptFollower, cleanupTests, 6 acceptFollower,
7 cleanupTests,
7 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
8 killallServers,
9 ServerInfo, 9 ServerInfo,
10 setAccessTokensToServers, 10 setAccessTokensToServers,
11 updateCustomSubConfig 11 updateCustomSubConfig
@@ -14,8 +14,8 @@ import {
14 follow, 14 follow,
15 getFollowersListPaginationAndSort, 15 getFollowersListPaginationAndSort,
16 getFollowingListPaginationAndSort, 16 getFollowingListPaginationAndSort,
17 removeFollower, 17 rejectFollower,
18 rejectFollower 18 removeFollower
19} from '../../../../shared/extra-utils/server/follows' 19} from '../../../../shared/extra-utils/server/follows'
20import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21import { ActorFollow } from '../../../../shared/models/actors' 21import { ActorFollow } from '../../../../shared/models/actors'
@@ -29,8 +29,8 @@ async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'acc
29 29
30 const follow = res.body.data[0] as ActorFollow 30 const follow = res.body.data[0] as ActorFollow
31 expect(follow.state).to.equal(state) 31 expect(follow.state).to.equal(state)
32 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube') 32 expect(follow.follower.url).to.equal('http://localhost:' + servers[0].port + '/accounts/peertube')
33 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube') 33 expect(follow.following.url).to.equal('http://localhost:' + servers[1].port + '/accounts/peertube')
34 } 34 }
35 35
36 { 36 {
@@ -39,8 +39,8 @@ async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'acc
39 39
40 const follow = res.body.data[0] as ActorFollow 40 const follow = res.body.data[0] as ActorFollow
41 expect(follow.state).to.equal(state) 41 expect(follow.state).to.equal(state)
42 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube') 42 expect(follow.follower.url).to.equal('http://localhost:' + servers[0].port + '/accounts/peertube')
43 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube') 43 expect(follow.following.url).to.equal('http://localhost:' + servers[1].port + '/accounts/peertube')
44 } 44 }
45} 45}
46 46
@@ -151,7 +151,7 @@ describe('Test follows moderation', function () {
151 }) 151 })
152 152
153 it('Should accept a follower', async function () { 153 it('Should accept a follower', async function () {
154 await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@localhost:9001') 154 await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@localhost:' + servers[0].port)
155 await waitJobs(servers) 155 await waitJobs(servers)
156 156
157 await checkServer1And2HasFollowers(servers) 157 await checkServer1And2HasFollowers(servers)
@@ -178,7 +178,7 @@ describe('Test follows moderation', function () {
178 expect(res.body.total).to.equal(1) 178 expect(res.body.total).to.equal(1)
179 } 179 }
180 180
181 await rejectFollower(servers[2].url, servers[2].accessToken, 'peertube@localhost:9001') 181 await rejectFollower(servers[2].url, servers[2].accessToken, 'peertube@localhost:' + servers[0].port)
182 await waitJobs(servers) 182 await waitJobs(servers)
183 183
184 await checkServer1And2HasFollowers(servers) 184 await checkServer1And2HasFollowers(servers)
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts
index 397093cdb..e8d6f5138 100644
--- a/server/tests/api/server/follows.ts
+++ b/server/tests/api/server/follows.ts
@@ -8,7 +8,6 @@ import { cleanupTests, completeVideoCheck } from '../../../../shared/extra-utils
8import { 8import {
9 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
10 getVideosList, 10 getVideosList,
11 killallServers,
12 ServerInfo, 11 ServerInfo,
13 setAccessTokensToServers, 12 setAccessTokensToServers,
14 uploadVideo 13 uploadVideo
@@ -89,8 +88,8 @@ describe('Test follows', function () {
89 res = await getFollowingListPaginationAndSort(servers[0].url, 1, 1, 'createdAt') 88 res = await getFollowingListPaginationAndSort(servers[0].url, 1, 1, 'createdAt')
90 follows = follows.concat(res.body.data) 89 follows = follows.concat(res.body.data)
91 90
92 const server2Follow = follows.find(f => f.following.host === 'localhost:9002') 91 const server2Follow = follows.find(f => f.following.host === 'localhost:' + servers[1].port)
93 const server3Follow = follows.find(f => f.following.host === 'localhost:9003') 92 const server3Follow = follows.find(f => f.following.host === 'localhost:' + servers[2].port)
94 93
95 expect(server2Follow).to.not.be.undefined 94 expect(server2Follow).to.not.be.undefined
96 expect(server3Follow).to.not.be.undefined 95 expect(server3Follow).to.not.be.undefined
@@ -100,12 +99,12 @@ describe('Test follows', function () {
100 99
101 it('Should search followings on server 1', async function () { 100 it('Should search followings on server 1', async function () {
102 { 101 {
103 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', ':9002') 102 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt', ':' + servers[1].port)
104 const follows = res.body.data 103 const follows = res.body.data
105 104
106 expect(res.body.total).to.equal(1) 105 expect(res.body.total).to.equal(1)
107 expect(follows.length).to.equal(1) 106 expect(follows.length).to.equal(1)
108 expect(follows[ 0 ].following.host).to.equal('localhost:9002') 107 expect(follows[ 0 ].following.host).to.equal('localhost:' + servers[1].port)
109 } 108 }
110 109
111 { 110 {
@@ -136,18 +135,18 @@ describe('Test follows', function () {
136 expect(res.body.total).to.equal(1) 135 expect(res.body.total).to.equal(1)
137 expect(follows).to.be.an('array') 136 expect(follows).to.be.an('array')
138 expect(follows.length).to.equal(1) 137 expect(follows.length).to.equal(1)
139 expect(follows[0].follower.host).to.equal('localhost:9001') 138 expect(follows[0].follower.host).to.equal('localhost:' + servers[0].port)
140 } 139 }
141 }) 140 })
142 141
143 it('Should search followers on server 2', async function () { 142 it('Should search followers on server 2', async function () {
144 { 143 {
145 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', '9001') 144 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt', servers[0].port + '')
146 const follows = res.body.data 145 const follows = res.body.data
147 146
148 expect(res.body.total).to.equal(1) 147 expect(res.body.total).to.equal(1)
149 expect(follows.length).to.equal(1) 148 expect(follows.length).to.equal(1)
150 expect(follows[ 0 ].following.host).to.equal('localhost:9003') 149 expect(follows[ 0 ].following.host).to.equal('localhost:' + servers[2].port)
151 } 150 }
152 151
153 { 152 {
@@ -169,16 +168,16 @@ describe('Test follows', function () {
169 }) 168 })
170 169
171 it('Should have the correct follows counts', async function () { 170 it('Should have the correct follows counts', async function () {
172 await expectAccountFollows(servers[0].url, 'peertube@localhost:9001', 0, 2) 171 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 2)
173 await expectAccountFollows(servers[0].url, 'peertube@localhost:9002', 1, 0) 172 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
174 await expectAccountFollows(servers[0].url, 'peertube@localhost:9003', 1, 0) 173 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[2].port, 1, 0)
175 174
176 // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) 175 // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
177 await expectAccountFollows(servers[1].url, 'peertube@localhost:9001', 0, 1) 176 await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
178 await expectAccountFollows(servers[1].url, 'peertube@localhost:9002', 1, 0) 177 await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
179 178
180 await expectAccountFollows(servers[2].url, 'peertube@localhost:9001', 0, 1) 179 await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 1)
181 await expectAccountFollows(servers[2].url, 'peertube@localhost:9003', 1, 0) 180 await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 1, 0)
182 }) 181 })
183 182
184 it('Should unfollow server 3 on server 1', async function () { 183 it('Should unfollow server 3 on server 1', async function () {
@@ -197,7 +196,7 @@ describe('Test follows', function () {
197 expect(follows).to.be.an('array') 196 expect(follows).to.be.an('array')
198 expect(follows.length).to.equal(1) 197 expect(follows.length).to.equal(1)
199 198
200 expect(follows[0].following.host).to.equal('localhost:9002') 199 expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
201 }) 200 })
202 201
203 it('Should not have server 1 as follower on server 3 anymore', async function () { 202 it('Should not have server 1 as follower on server 3 anymore', async function () {
@@ -210,14 +209,14 @@ describe('Test follows', function () {
210 }) 209 })
211 210
212 it('Should have the correct follows counts 2', async function () { 211 it('Should have the correct follows counts 2', async function () {
213 await expectAccountFollows(servers[0].url, 'peertube@localhost:9001', 0, 1) 212 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 1)
214 await expectAccountFollows(servers[0].url, 'peertube@localhost:9002', 1, 0) 213 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
215 214
216 await expectAccountFollows(servers[1].url, 'peertube@localhost:9001', 0, 1) 215 await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
217 await expectAccountFollows(servers[1].url, 'peertube@localhost:9002', 1, 0) 216 await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
218 217
219 await expectAccountFollows(servers[2].url, 'peertube@localhost:9001', 0, 0) 218 await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 0)
220 await expectAccountFollows(servers[2].url, 'peertube@localhost:9003', 0, 0) 219 await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 0, 0)
221 }) 220 })
222 221
223 it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { 222 it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
@@ -310,15 +309,15 @@ describe('Test follows', function () {
310 }) 309 })
311 310
312 it('Should have the correct follows counts 3', async function () { 311 it('Should have the correct follows counts 3', async function () {
313 await expectAccountFollows(servers[0].url, 'peertube@localhost:9001', 0, 2) 312 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 2)
314 await expectAccountFollows(servers[0].url, 'peertube@localhost:9002', 1, 0) 313 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
315 await expectAccountFollows(servers[0].url, 'peertube@localhost:9003', 1, 0) 314 await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[2].port, 1, 0)
316 315
317 await expectAccountFollows(servers[1].url, 'peertube@localhost:9001', 0, 1) 316 await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
318 await expectAccountFollows(servers[1].url, 'peertube@localhost:9002', 1, 0) 317 await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
319 318
320 await expectAccountFollows(servers[2].url, 'peertube@localhost:9001', 0, 2) 319 await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 2)
321 await expectAccountFollows(servers[2].url, 'peertube@localhost:9003', 1, 0) 320 await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 1, 0)
322 }) 321 })
323 322
324 it('Should have propagated videos', async function () { 323 it('Should have propagated videos', async function () {
@@ -344,7 +343,7 @@ describe('Test follows', function () {
344 support: 'my super support text', 343 support: 'my super support text',
345 account: { 344 account: {
346 name: 'root', 345 name: 'root',
347 host: 'localhost:9003' 346 host: 'localhost:' + servers[2].port
348 }, 347 },
349 isLocal, 348 isLocal,
350 commentsEnabled: true, 349 commentsEnabled: true,
@@ -384,7 +383,7 @@ describe('Test follows', function () {
384 expect(comment.videoId).to.equal(video4.id) 383 expect(comment.videoId).to.equal(video4.id)
385 expect(comment.id).to.equal(comment.threadId) 384 expect(comment.id).to.equal(comment.threadId)
386 expect(comment.account.name).to.equal('root') 385 expect(comment.account.name).to.equal('root')
387 expect(comment.account.host).to.equal('localhost:9003') 386 expect(comment.account.host).to.equal('localhost:' + servers[2].port)
388 expect(comment.totalReplies).to.equal(3) 387 expect(comment.totalReplies).to.equal(3)
389 expect(dateIsValid(comment.createdAt as string)).to.be.true 388 expect(dateIsValid(comment.createdAt as string)).to.be.true
390 expect(dateIsValid(comment.updatedAt as string)).to.be.true 389 expect(dateIsValid(comment.updatedAt as string)).to.be.true
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts
index 19010dbc1..068654d8c 100644
--- a/server/tests/api/server/handle-down.ts
+++ b/server/tests/api/server/handle-down.ts
@@ -60,48 +60,50 @@ describe('Test handle downs', function () {
60 privacy: VideoPrivacy.UNLISTED 60 privacy: VideoPrivacy.UNLISTED
61 }) 61 })
62 62
63 const checkAttributes = { 63 let checkAttributes: any
64 name: 'my super name for server 1', 64 let unlistedCheckAttributes: any
65 category: 5,
66 licence: 4,
67 language: 'ja',
68 nsfw: true,
69 description: 'my super description for server 1',
70 support: 'my super support text for server 1',
71 account: {
72 name: 'root',
73 host: 'localhost:9001'
74 },
75 isLocal: false,
76 duration: 10,
77 tags: [ 'tag1p1', 'tag2p1' ],
78 privacy: VideoPrivacy.PUBLIC,
79 commentsEnabled: true,
80 downloadEnabled: true,
81 channel: {
82 name: 'root_channel',
83 displayName: 'Main root channel',
84 description: '',
85 isLocal: false
86 },
87 fixture: 'video_short1.webm',
88 files: [
89 {
90 resolution: 720,
91 size: 572456
92 }
93 ]
94 }
95
96 const unlistedCheckAttributes = immutableAssign(checkAttributes, {
97 privacy: VideoPrivacy.UNLISTED
98 })
99 65
100 before(async function () { 66 before(async function () {
101 this.timeout(30000) 67 this.timeout(30000)
102 68
103 servers = await flushAndRunMultipleServers(3) 69 servers = await flushAndRunMultipleServers(3)
104 70
71 checkAttributes = {
72 name: 'my super name for server 1',
73 category: 5,
74 licence: 4,
75 language: 'ja',
76 nsfw: true,
77 description: 'my super description for server 1',
78 support: 'my super support text for server 1',
79 account: {
80 name: 'root',
81 host: 'localhost:' + servers[0].port
82 },
83 isLocal: false,
84 duration: 10,
85 tags: [ 'tag1p1', 'tag2p1' ],
86 privacy: VideoPrivacy.PUBLIC,
87 commentsEnabled: true,
88 downloadEnabled: true,
89 channel: {
90 name: 'root_channel',
91 displayName: 'Main root channel',
92 description: '',
93 isLocal: false
94 },
95 fixture: 'video_short1.webm',
96 files: [
97 {
98 resolution: 720,
99 size: 572456
100 }
101 ]
102 }
103 unlistedCheckAttributes = immutableAssign(checkAttributes, {
104 privacy: VideoPrivacy.UNLISTED
105 })
106
105 // Get the access tokens 107 // Get the access tokens
106 await setAccessTokensToServers(servers) 108 await setAccessTokensToServers(servers)
107 }) 109 })
@@ -172,7 +174,7 @@ describe('Test handle downs', function () {
172 const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 2, 'createdAt') 174 const res = await getFollowersListPaginationAndSort(servers[0].url, 0, 2, 'createdAt')
173 expect(res.body.data).to.be.an('array') 175 expect(res.body.data).to.be.an('array')
174 expect(res.body.data).to.have.lengthOf(1) 176 expect(res.body.data).to.have.lengthOf(1)
175 expect(res.body.data[0].follower.host).to.equal('localhost:9003') 177 expect(res.body.data[0].follower.host).to.equal('localhost:' + servers[2].port)
176 }) 178 })
177 179
178 it('Should not have pending/processing jobs anymore', async function () { 180 it('Should not have pending/processing jobs anymore', async function () {
diff --git a/server/tests/api/server/jobs.ts b/server/tests/api/server/jobs.ts
index 634654626..3ab2fe120 100644
--- a/server/tests/api/server/jobs.ts
+++ b/server/tests/api/server/jobs.ts
@@ -26,7 +26,7 @@ describe('Test jobs', function () {
26 }) 26 })
27 27
28 it('Should create some jobs', async function () { 28 it('Should create some jobs', async function () {
29 this.timeout(30000) 29 this.timeout(60000)
30 30
31 await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' }) 31 await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' })
32 await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' }) 32 await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts
index 3644fa0d3..68f442199 100644
--- a/server/tests/api/server/logs.ts
+++ b/server/tests/api/server/logs.ts
@@ -45,7 +45,7 @@ describe('Test logs', function () {
45 }) 45 })
46 46
47 it('Should get logs with an end date', async function () { 47 it('Should get logs with an end date', async function () {
48 this.timeout(10000) 48 this.timeout(20000)
49 49
50 await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) 50 await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
51 await waitJobs([ server ]) 51 await waitJobs([ server ])
diff --git a/server/tests/api/travis-1.sh b/server/tests/api/travis-1.sh
new file mode 100644
index 000000000..db4021b25
--- /dev/null
+++ b/server/tests/api/travis-1.sh
@@ -0,0 +1,10 @@
1#!/usr/bin/env sh
2
3set -eu
4
5checkParamFiles=$(find server/tests/api/check-params -type f | grep -v index.ts | xargs echo)
6notificationsFiles=$(find server/tests/api/notifications -type f | grep -v index.ts | xargs echo)
7searchFiles=$(find server/tests/api/search -type f | grep -v index.ts | xargs echo)
8
9MOCHA_PARALLEL=true mocha --timeout 5000 --exit --require ts-node/register --bail \
10 $notificationsFiles $searchFiles $checkParamFiles
diff --git a/server/tests/api/travis-2.sh b/server/tests/api/travis-2.sh
new file mode 100644
index 000000000..ba7a061b0
--- /dev/null
+++ b/server/tests/api/travis-2.sh
@@ -0,0 +1,9 @@
1#!/usr/bin/env sh
2
3set -eu
4
5serverFiles=$(find server/tests/api/server -type f | grep -v index.ts | xargs echo)
6usersFiles=$(find server/tests/api/users -type f | grep -v index.ts | xargs echo)
7
8MOCHA_PARALLEL=true mocha --timeout 5000 --exit --require ts-node/register --bail \
9 $serverFiles $usersFiles
diff --git a/server/tests/api/travis-3.sh b/server/tests/api/travis-3.sh
new file mode 100644
index 000000000..82457222c
--- /dev/null
+++ b/server/tests/api/travis-3.sh
@@ -0,0 +1,8 @@
1#!/usr/bin/env sh
2
3set -eu
4
5videosFiles=$(find server/tests/api/videos -type f | grep -v index.ts | xargs echo)
6
7MOCHA_PARALLEL=true mocha --timeout 5000 --exit --require ts-node/register --bail \
8 $videosFiles
diff --git a/server/tests/api/travis-4.sh b/server/tests/api/travis-4.sh
new file mode 100644
index 000000000..875986182
--- /dev/null
+++ b/server/tests/api/travis-4.sh
@@ -0,0 +1,9 @@
1#!/usr/bin/env sh
2
3set -eu
4
5redundancyFiles=$(find server/tests/api/redundancy -type f | grep -v index.ts | xargs echo)
6activitypubFiles=$(find server/tests/api/activitypub -type f | grep -v index.ts | xargs echo)
7
8MOCHA_PARALLEL=true mocha-parallel-tests --max-parallel $1 --timeout 5000 --exit --require ts-node/register --bail \
9 $redundancyFiles $activitypubFiles
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts
index fbc57e0ef..c25e85ada 100644
--- a/server/tests/api/users/blocklist.ts
+++ b/server/tests/api/users/blocklist.ts
@@ -144,7 +144,7 @@ describe('Test blocklist', function () {
144 }) 144 })
145 145
146 it('Should block a remote account', async function () { 146 it('Should block a remote account', async function () {
147 await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') 147 await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port)
148 }) 148 })
149 149
150 it('Should hide its videos', async function () { 150 it('Should hide its videos', async function () {
@@ -209,7 +209,7 @@ describe('Test blocklist', function () {
209 expect(block.byAccount.name).to.equal('root') 209 expect(block.byAccount.name).to.equal('root')
210 expect(block.blockedAccount.displayName).to.equal('user2') 210 expect(block.blockedAccount.displayName).to.equal('user2')
211 expect(block.blockedAccount.name).to.equal('user2') 211 expect(block.blockedAccount.name).to.equal('user2')
212 expect(block.blockedAccount.host).to.equal('localhost:9002') 212 expect(block.blockedAccount.host).to.equal('localhost:' + servers[1].port)
213 } 213 }
214 214
215 { 215 {
@@ -223,12 +223,12 @@ describe('Test blocklist', function () {
223 expect(block.byAccount.name).to.equal('root') 223 expect(block.byAccount.name).to.equal('root')
224 expect(block.blockedAccount.displayName).to.equal('user1') 224 expect(block.blockedAccount.displayName).to.equal('user1')
225 expect(block.blockedAccount.name).to.equal('user1') 225 expect(block.blockedAccount.name).to.equal('user1')
226 expect(block.blockedAccount.host).to.equal('localhost:9001') 226 expect(block.blockedAccount.host).to.equal('localhost:' + servers[0].port)
227 } 227 }
228 }) 228 })
229 229
230 it('Should unblock the remote account', async function () { 230 it('Should unblock the remote account', async function () {
231 await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') 231 await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port)
232 }) 232 })
233 233
234 it('Should display its videos', async function () { 234 it('Should display its videos', async function () {
@@ -260,7 +260,7 @@ describe('Test blocklist', function () {
260 }) 260 })
261 261
262 it('Should block a remote server', async function () { 262 it('Should block a remote server', async function () {
263 await addServerToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') 263 await addServerToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port)
264 }) 264 })
265 265
266 it('Should hide its videos', async function () { 266 it('Should hide its videos', async function () {
@@ -291,11 +291,11 @@ describe('Test blocklist', function () {
291 const block = blocks[ 0 ] 291 const block = blocks[ 0 ]
292 expect(block.byAccount.displayName).to.equal('root') 292 expect(block.byAccount.displayName).to.equal('root')
293 expect(block.byAccount.name).to.equal('root') 293 expect(block.byAccount.name).to.equal('root')
294 expect(block.blockedServer.host).to.equal('localhost:9002') 294 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
295 }) 295 })
296 296
297 it('Should unblock the remote server', async function () { 297 it('Should unblock the remote server', async function () {
298 await removeServerFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') 298 await removeServerFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port)
299 }) 299 })
300 300
301 it('Should display its videos', function () { 301 it('Should display its videos', function () {
@@ -324,7 +324,7 @@ describe('Test blocklist', function () {
324 }) 324 })
325 325
326 it('Should block a remote account', async function () { 326 it('Should block a remote account', async function () {
327 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') 327 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port)
328 }) 328 })
329 329
330 it('Should hide its videos', async function () { 330 it('Should hide its videos', async function () {
@@ -387,7 +387,7 @@ describe('Test blocklist', function () {
387 expect(block.byAccount.name).to.equal('peertube') 387 expect(block.byAccount.name).to.equal('peertube')
388 expect(block.blockedAccount.displayName).to.equal('user2') 388 expect(block.blockedAccount.displayName).to.equal('user2')
389 expect(block.blockedAccount.name).to.equal('user2') 389 expect(block.blockedAccount.name).to.equal('user2')
390 expect(block.blockedAccount.host).to.equal('localhost:9002') 390 expect(block.blockedAccount.host).to.equal('localhost:' + servers[1].port)
391 } 391 }
392 392
393 { 393 {
@@ -401,12 +401,12 @@ describe('Test blocklist', function () {
401 expect(block.byAccount.name).to.equal('peertube') 401 expect(block.byAccount.name).to.equal('peertube')
402 expect(block.blockedAccount.displayName).to.equal('user1') 402 expect(block.blockedAccount.displayName).to.equal('user1')
403 expect(block.blockedAccount.name).to.equal('user1') 403 expect(block.blockedAccount.name).to.equal('user1')
404 expect(block.blockedAccount.host).to.equal('localhost:9001') 404 expect(block.blockedAccount.host).to.equal('localhost:' + servers[0].port)
405 } 405 }
406 }) 406 })
407 407
408 it('Should unblock the remote account', async function () { 408 it('Should unblock the remote account', async function () {
409 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:9002') 409 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port)
410 }) 410 })
411 411
412 it('Should display its videos', async function () { 412 it('Should display its videos', async function () {
@@ -446,7 +446,7 @@ describe('Test blocklist', function () {
446 }) 446 })
447 447
448 it('Should block a remote server', async function () { 448 it('Should block a remote server', async function () {
449 await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') 449 await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port)
450 }) 450 })
451 451
452 it('Should hide its videos', async function () { 452 it('Should hide its videos', async function () {
@@ -478,11 +478,11 @@ describe('Test blocklist', function () {
478 const block = blocks[ 0 ] 478 const block = blocks[ 0 ]
479 expect(block.byAccount.displayName).to.equal('peertube') 479 expect(block.byAccount.displayName).to.equal('peertube')
480 expect(block.byAccount.name).to.equal('peertube') 480 expect(block.byAccount.name).to.equal('peertube')
481 expect(block.blockedServer.host).to.equal('localhost:9002') 481 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
482 }) 482 })
483 483
484 it('Should unblock the remote server', async function () { 484 it('Should unblock the remote server', async function () {
485 await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:9002') 485 await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port)
486 }) 486 })
487 487
488 it('Should list all videos', async function () { 488 it('Should list all videos', async function () {
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts
index 48811e647..c8a89d6be 100644
--- a/server/tests/api/users/user-subscriptions.ts
+++ b/server/tests/api/users/user-subscriptions.ts
@@ -71,8 +71,8 @@ describe('Test users subscriptions', function () {
71 it('User of server 1 should follow user of server 3 and root of server 1', async function () { 71 it('User of server 1 should follow user of server 3 and root of server 1', async function () {
72 this.timeout(60000) 72 this.timeout(60000)
73 73
74 await addUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:9003') 74 await addUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
75 await addUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:9001') 75 await addUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:' + servers[0].port)
76 76
77 await waitJobs(servers) 77 await waitJobs(servers)
78 78
@@ -116,22 +116,22 @@ describe('Test users subscriptions', function () {
116 116
117 it('Should get subscription', async function () { 117 it('Should get subscription', async function () {
118 { 118 {
119 const res = await getUserSubscription(servers[ 0 ].url, users[ 0 ].accessToken, 'user3_channel@localhost:9003') 119 const res = await getUserSubscription(servers[ 0 ].url, users[ 0 ].accessToken, 'user3_channel@localhost:' + servers[2].port)
120 const videoChannel: VideoChannel = res.body 120 const videoChannel: VideoChannel = res.body
121 121
122 expect(videoChannel.name).to.equal('user3_channel') 122 expect(videoChannel.name).to.equal('user3_channel')
123 expect(videoChannel.host).to.equal('localhost:9003') 123 expect(videoChannel.host).to.equal('localhost:' + servers[2].port)
124 expect(videoChannel.displayName).to.equal('Main user3 channel') 124 expect(videoChannel.displayName).to.equal('Main user3 channel')
125 expect(videoChannel.followingCount).to.equal(0) 125 expect(videoChannel.followingCount).to.equal(0)
126 expect(videoChannel.followersCount).to.equal(1) 126 expect(videoChannel.followersCount).to.equal(1)
127 } 127 }
128 128
129 { 129 {
130 const res = await getUserSubscription(servers[ 0 ].url, users[ 0 ].accessToken, 'root_channel@localhost:9001') 130 const res = await getUserSubscription(servers[ 0 ].url, users[ 0 ].accessToken, 'root_channel@localhost:' + servers[0].port)
131 const videoChannel: VideoChannel = res.body 131 const videoChannel: VideoChannel = res.body
132 132
133 expect(videoChannel.name).to.equal('root_channel') 133 expect(videoChannel.name).to.equal('root_channel')
134 expect(videoChannel.host).to.equal('localhost:9001') 134 expect(videoChannel.host).to.equal('localhost:' + servers[0].port)
135 expect(videoChannel.displayName).to.equal('Main root channel') 135 expect(videoChannel.displayName).to.equal('Main root channel')
136 expect(videoChannel.followingCount).to.equal(0) 136 expect(videoChannel.followingCount).to.equal(0)
137 expect(videoChannel.followersCount).to.equal(1) 137 expect(videoChannel.followersCount).to.equal(1)
@@ -140,19 +140,19 @@ describe('Test users subscriptions', function () {
140 140
141 it('Should return the existing subscriptions', async function () { 141 it('Should return the existing subscriptions', async function () {
142 const uris = [ 142 const uris = [
143 'user3_channel@localhost:9003', 143 'user3_channel@localhost:' + servers[2].port,
144 'root2_channel@localhost:9001', 144 'root2_channel@localhost:' + servers[0].port,
145 'root_channel@localhost:9001', 145 'root_channel@localhost:' + servers[0].port,
146 'user3_channel@localhost:9001' 146 'user3_channel@localhost:' + servers[0].port
147 ] 147 ]
148 148
149 const res = await areSubscriptionsExist(servers[ 0 ].url, users[ 0 ].accessToken, uris) 149 const res = await areSubscriptionsExist(servers[ 0 ].url, users[ 0 ].accessToken, uris)
150 const body = res.body 150 const body = res.body
151 151
152 expect(body['user3_channel@localhost:9003']).to.be.true 152 expect(body['user3_channel@localhost:' + servers[2].port]).to.be.true
153 expect(body['root2_channel@localhost:9001']).to.be.false 153 expect(body['root2_channel@localhost:' + servers[0].port]).to.be.false
154 expect(body['root_channel@localhost:9001']).to.be.true 154 expect(body['root_channel@localhost:' + servers[0].port]).to.be.true
155 expect(body['user3_channel@localhost:9001']).to.be.false 155 expect(body['user3_channel@localhost:' + servers[0].port]).to.be.false
156 }) 156 })
157 157
158 it('Should list subscription videos', async function () { 158 it('Should list subscription videos', async function () {
@@ -291,7 +291,7 @@ describe('Test users subscriptions', function () {
291 it('Should remove user of server 3 subscription', async function () { 291 it('Should remove user of server 3 subscription', async function () {
292 this.timeout(30000) 292 this.timeout(30000)
293 293
294 await removeUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:9003') 294 await removeUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
295 295
296 await waitJobs(servers) 296 await waitJobs(servers)
297 }) 297 })
@@ -312,7 +312,7 @@ describe('Test users subscriptions', function () {
312 it('Should remove the root subscription and not display the videos anymore', async function () { 312 it('Should remove the root subscription and not display the videos anymore', async function () {
313 this.timeout(30000) 313 this.timeout(30000)
314 314
315 await removeUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:9001') 315 await removeUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:' + servers[0].port)
316 316
317 await waitJobs(servers) 317 await waitJobs(servers)
318 318
@@ -340,7 +340,7 @@ describe('Test users subscriptions', function () {
340 it('Should follow user of server 3 again', async function () { 340 it('Should follow user of server 3 again', async function () {
341 this.timeout(60000) 341 this.timeout(60000)
342 342
343 await addUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:9003') 343 await addUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
344 344
345 await waitJobs(servers) 345 await waitJobs(servers)
346 346
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts
index 9a971adb3..791418318 100644
--- a/server/tests/api/users/users-multiple-servers.ts
+++ b/server/tests/api/users/users-multiple-servers.ts
@@ -5,7 +5,8 @@ import 'mocha'
5import { Account } from '../../../../shared/models/actors' 5import { Account } from '../../../../shared/models/actors'
6import { 6import {
7 checkTmpIsEmpty, 7 checkTmpIsEmpty,
8 checkVideoFilesWereRemoved, cleanupTests, 8 checkVideoFilesWereRemoved,
9 cleanupTests,
9 createUser, 10 createUser,
10 doubleFollow, 11 doubleFollow,
11 flushAndRunMultipleServers, 12 flushAndRunMultipleServers,
@@ -15,14 +16,7 @@ import {
15 updateMyUser, 16 updateMyUser,
16 userLogin 17 userLogin
17} from '../../../../shared/extra-utils' 18} from '../../../../shared/extra-utils'
18import { 19import { getMyUserInformation, ServerInfo, testImage, updateMyAvatar, uploadVideo } from '../../../../shared/extra-utils/index'
19 getMyUserInformation,
20 killallServers,
21 ServerInfo,
22 testImage,
23 updateMyAvatar,
24 uploadVideo
25} from '../../../../shared/extra-utils/index'
26import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../../../shared/extra-utils/users/accounts' 20import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../../../shared/extra-utils/users/accounts'
27import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 21import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
28import { User } from '../../../../shared/models/users' 22import { User } from '../../../../shared/models/users'
@@ -34,12 +28,10 @@ const expect = chai.expect
34describe('Test users with multiple servers', function () { 28describe('Test users with multiple servers', function () {
35 let servers: ServerInfo[] = [] 29 let servers: ServerInfo[] = []
36 let user: User 30 let user: User
37 let userAccountName: string
38 let userAccountUUID: string
39 let userVideoChannelUUID: string
40 let userId: number 31 let userId: number
41 let videoUUID: string 32 let videoUUID: string
42 let userAccessToken: string 33 let userAccessToken: string
34 let userAvatarFilename: string
43 35
44 before(async function () { 36 before(async function () {
45 this.timeout(120000) 37 this.timeout(120000)
@@ -75,19 +67,6 @@ describe('Test users with multiple servers', function () {
75 } 67 }
76 68
77 { 69 {
78 const res = await getMyUserInformation(servers[0].url, userAccessToken)
79 const account: Account = res.body.account
80 userAccountName = account.name + '@' + account.host
81 userAccountUUID = account.uuid
82 }
83
84 {
85 const res = await getMyUserInformation(servers[ 0 ].url, servers[ 0 ].accessToken)
86 const user: User = res.body
87 userVideoChannelUUID = user.videoChannels[0].uuid
88 }
89
90 {
91 const resVideo = await uploadVideo(servers[ 0 ].url, userAccessToken, {}) 70 const resVideo = await uploadVideo(servers[ 0 ].url, userAccessToken, {})
92 videoUUID = resVideo.body.video.uuid 71 videoUUID = resVideo.body.video.uuid
93 } 72 }
@@ -106,6 +85,8 @@ describe('Test users with multiple servers', function () {
106 85
107 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) 86 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
108 user = res.body 87 user = res.body
88
89 const account: Account = user.account
109 expect(user.account.displayName).to.equal('my super display name') 90 expect(user.account.displayName).to.equal('my super display name')
110 91
111 await waitJobs(servers) 92 await waitJobs(servers)
@@ -142,7 +123,9 @@ describe('Test users with multiple servers', function () {
142 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) 123 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
143 user = res.body 124 user = res.body
144 125
145 await testImage(servers[0].url, 'avatar2-resized', user.account.avatar.path, '.png') 126 userAvatarFilename = user.account.avatar.path
127
128 await testImage(servers[0].url, 'avatar2-resized', userAvatarFilename, '.png')
146 129
147 await waitJobs(servers) 130 await waitJobs(servers)
148 }) 131 })
@@ -151,13 +134,13 @@ describe('Test users with multiple servers', function () {
151 for (const server of servers) { 134 for (const server of servers) {
152 const resAccounts = await getAccountsList(server.url, '-createdAt') 135 const resAccounts = await getAccountsList(server.url, '-createdAt')
153 136
154 const rootServer1List = resAccounts.body.data.find(a => a.name === 'root' && a.host === 'localhost:9001') as Account 137 const rootServer1List = resAccounts.body.data.find(a => a.name === 'root' && a.host === 'localhost:' + servers[0].port) as Account
155 expect(rootServer1List).not.to.be.undefined 138 expect(rootServer1List).not.to.be.undefined
156 139
157 const resAccount = await getAccount(server.url, rootServer1List.name + '@' + rootServer1List.host) 140 const resAccount = await getAccount(server.url, rootServer1List.name + '@' + rootServer1List.host)
158 const rootServer1Get = resAccount.body as Account 141 const rootServer1Get = resAccount.body as Account
159 expect(rootServer1Get.name).to.equal('root') 142 expect(rootServer1Get.name).to.equal('root')
160 expect(rootServer1Get.host).to.equal('localhost:9001') 143 expect(rootServer1Get.host).to.equal('localhost:' + servers[0].port)
161 expect(rootServer1Get.displayName).to.equal('my super display name') 144 expect(rootServer1Get.displayName).to.equal('my super display name')
162 expect(rootServer1Get.description).to.equal('my super description updated') 145 expect(rootServer1Get.description).to.equal('my super description updated')
163 146
@@ -173,7 +156,7 @@ describe('Test users with multiple servers', function () {
173 156
174 it('Should list account videos', async function () { 157 it('Should list account videos', async function () {
175 for (const server of servers) { 158 for (const server of servers) {
176 const res = await getAccountVideos(server.url, server.accessToken, userAccountName, 0, 5) 159 const res = await getAccountVideos(server.url, server.accessToken, 'user1@localhost:' + servers[0].port, 0, 5)
177 160
178 expect(res.body.total).to.equal(1) 161 expect(res.body.total).to.equal(1)
179 expect(res.body.data).to.be.an('array') 162 expect(res.body.data).to.be.an('array')
@@ -188,12 +171,12 @@ describe('Test users with multiple servers', function () {
188 for (const server of servers) { 171 for (const server of servers) {
189 const resAccounts = await getAccountsList(server.url, '-createdAt') 172 const resAccounts = await getAccountsList(server.url, '-createdAt')
190 173
191 const accountDeleted = resAccounts.body.data.find(a => a.name === 'user1' && a.host === 'localhost:9001') as Account 174 const accountDeleted = resAccounts.body.data.find(a => a.name === 'user1' && a.host === 'localhost:' + servers[0].port) as Account
192 expect(accountDeleted).not.to.be.undefined 175 expect(accountDeleted).not.to.be.undefined
193 176
194 const resVideoChannels = await getVideoChannelsList(server.url, 0, 10) 177 const resVideoChannels = await getVideoChannelsList(server.url, 0, 10)
195 const videoChannelDeleted = resVideoChannels.body.data.find(a => { 178 const videoChannelDeleted = resVideoChannels.body.data.find(a => {
196 return a.displayName === 'Main user1 channel' && a.host === 'localhost:9001' 179 return a.displayName === 'Main user1 channel' && a.host === 'localhost:' + servers[0].port
197 }) as VideoChannel 180 }) as VideoChannel
198 expect(videoChannelDeleted).not.to.be.undefined 181 expect(videoChannelDeleted).not.to.be.undefined
199 } 182 }
@@ -205,12 +188,12 @@ describe('Test users with multiple servers', function () {
205 for (const server of servers) { 188 for (const server of servers) {
206 const resAccounts = await getAccountsList(server.url, '-createdAt') 189 const resAccounts = await getAccountsList(server.url, '-createdAt')
207 190
208 const accountDeleted = resAccounts.body.data.find(a => a.name === 'user1' && a.host === 'localhost:9001') as Account 191 const accountDeleted = resAccounts.body.data.find(a => a.name === 'user1' && a.host === 'localhost:' + servers[0].port) as Account
209 expect(accountDeleted).to.be.undefined 192 expect(accountDeleted).to.be.undefined
210 193
211 const resVideoChannels = await getVideoChannelsList(server.url, 0, 10) 194 const resVideoChannels = await getVideoChannelsList(server.url, 0, 10)
212 const videoChannelDeleted = resVideoChannels.body.data.find(a => { 195 const videoChannelDeleted = resVideoChannels.body.data.find(a => {
213 return a.name === 'Main user1 channel' && a.host === 'localhost:9001' 196 return a.name === 'Main user1 channel' && a.host === 'localhost:' + servers[0].port
214 }) as VideoChannel 197 }) as VideoChannel
215 expect(videoChannelDeleted).to.be.undefined 198 expect(videoChannelDeleted).to.be.undefined
216 } 199 }
@@ -218,14 +201,13 @@ describe('Test users with multiple servers', function () {
218 201
219 it('Should not have actor files', async () => { 202 it('Should not have actor files', async () => {
220 for (const server of servers) { 203 for (const server of servers) {
221 await checkActorFilesWereRemoved(userAccountUUID, server.serverNumber) 204 await checkActorFilesWereRemoved(userAvatarFilename, server.internalServerNumber)
222 await checkActorFilesWereRemoved(userVideoChannelUUID, server.serverNumber)
223 } 205 }
224 }) 206 })
225 207
226 it('Should not have video files', async () => { 208 it('Should not have video files', async () => {
227 for (const server of servers) { 209 for (const server of servers) {
228 await checkVideoFilesWereRemoved(videoUUID, server.serverNumber) 210 await checkVideoFilesWereRemoved(videoUUID, server.internalServerNumber)
229 } 211 }
230 }) 212 })
231 213
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts
index 514acf2e7..7cd61f539 100644
--- a/server/tests/api/users/users-verification.ts
+++ b/server/tests/api/users/users-verification.ts
@@ -3,18 +3,29 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers, 6 cleanupTests,
7 userLogin, login, flushAndRunServer, ServerInfo, verifyEmail, updateCustomSubConfig, wait, cleanupTests 7 flushAndRunServer,
8 getMyUserInformation,
9 getUserInformation,
10 login,
11 registerUser,
12 ServerInfo,
13 updateCustomSubConfig,
14 updateMyUser,
15 userLogin,
16 verifyEmail
8} from '../../../../shared/extra-utils' 17} from '../../../../shared/extra-utils'
9import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 18import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
10import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' 19import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
11import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21import { User } from '../../../../shared/models/users'
12 22
13const expect = chai.expect 23const expect = chai.expect
14 24
15describe('Test users account verification', function () { 25describe('Test users account verification', function () {
16 let server: ServerInfo 26 let server: ServerInfo
17 let userId: number 27 let userId: number
28 let userAccessToken: string
18 let verificationString: string 29 let verificationString: string
19 let expectedEmailsLength = 0 30 let expectedEmailsLength = 0
20 const user1 = { 31 const user1 = {
@@ -30,11 +41,12 @@ describe('Test users account verification', function () {
30 before(async function () { 41 before(async function () {
31 this.timeout(30000) 42 this.timeout(30000)
32 43
33 await MockSmtpServer.Instance.collectEmails(emails) 44 const port = await MockSmtpServer.Instance.collectEmails(emails)
34 45
35 const overrideConfig = { 46 const overrideConfig = {
36 smtp: { 47 smtp: {
37 hostname: 'localhost' 48 hostname: 'localhost',
49 port
38 } 50 }
39 } 51 }
40 server = await flushAndRunServer(1, overrideConfig) 52 server = await flushAndRunServer(1, overrideConfig)
@@ -82,11 +94,54 @@ describe('Test users account verification', function () {
82 94
83 it('Should verify the user via email and allow login', async function () { 95 it('Should verify the user via email and allow login', async function () {
84 await verifyEmail(server.url, userId, verificationString) 96 await verifyEmail(server.url, userId, verificationString)
85 await login(server.url, server.client, user1) 97
98 const res = await login(server.url, server.client, user1)
99 userAccessToken = res.body.access_token
100
86 const resUserVerified = await getUserInformation(server.url, server.accessToken, userId) 101 const resUserVerified = await getUserInformation(server.url, server.accessToken, userId)
87 expect(resUserVerified.body.emailVerified).to.be.true 102 expect(resUserVerified.body.emailVerified).to.be.true
88 }) 103 })
89 104
105 it('Should be able to change the user email', async function () {
106 let updateVerificationString: string
107
108 {
109 await updateMyUser({
110 url: server.url,
111 accessToken: userAccessToken,
112 email: 'updated@example.com',
113 currentPassword: user1.password
114 })
115
116 await waitJobs(server)
117 expectedEmailsLength++
118 expect(emails).to.have.lengthOf(expectedEmailsLength)
119
120 const email = emails[expectedEmailsLength - 1]
121
122 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
123 updateVerificationString = verificationStringMatches[1]
124 }
125
126 {
127 const res = await getMyUserInformation(server.url, userAccessToken)
128 const me: User = res.body
129
130 expect(me.email).to.equal('user_1@example.com')
131 expect(me.pendingEmail).to.equal('updated@example.com')
132 }
133
134 {
135 await verifyEmail(server.url, userId, updateVerificationString, true)
136
137 const res = await getMyUserInformation(server.url, userAccessToken)
138 const me: User = res.body
139
140 expect(me.email).to.equal('updated@example.com')
141 expect(me.pendingEmail).to.be.null
142 }
143 })
144
90 it('Should register user not requiring email verification if setting not enabled', async function () { 145 it('Should register user not requiring email verification if setting not enabled', async function () {
91 this.timeout(5000) 146 this.timeout(5000)
92 await updateCustomSubConfig(server.url, server.accessToken, { 147 await updateCustomSubConfig(server.url, server.accessToken, {
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index c8e32f3f5..403d1a089 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -17,11 +17,12 @@ import {
17 getUserInformation, 17 getUserInformation,
18 getUsersList, 18 getUsersList,
19 getUsersListPaginationAndSort, 19 getUsersListPaginationAndSort,
20 getVideoChannel,
20 getVideosList, 21 getVideosList,
21 login, 22 login,
22 makePutBodyRequest, 23 makePutBodyRequest,
23 rateVideo, 24 rateVideo,
24 registerUser, 25 registerUserWithChannel,
25 removeUser, 26 removeUser,
26 removeVideo, 27 removeVideo,
27 ServerInfo, 28 ServerInfo,
@@ -316,7 +317,7 @@ describe('Test users', function () {
316 317
317 const rootUser = users[ 1 ] 318 const rootUser = users[ 1 ]
318 expect(rootUser.username).to.equal('root') 319 expect(rootUser.username).to.equal('root')
319 expect(rootUser.email).to.equal('admin1@example.com') 320 expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com')
320 expect(user.nsfwPolicy).to.equal('display') 321 expect(user.nsfwPolicy).to.equal('display')
321 322
322 userId = user.id 323 userId = user.id
@@ -334,7 +335,7 @@ describe('Test users', function () {
334 335
335 const user = users[ 0 ] 336 const user = users[ 0 ]
336 expect(user.username).to.equal('root') 337 expect(user.username).to.equal('root')
337 expect(user.email).to.equal('admin1@example.com') 338 expect(user.email).to.equal('admin' + server.internalServerNumber + '@example.com')
338 expect(user.roleLabel).to.equal('Administrator') 339 expect(user.roleLabel).to.equal('Administrator')
339 expect(user.nsfwPolicy).to.equal('display') 340 expect(user.nsfwPolicy).to.equal('display')
340 }) 341 })
@@ -379,7 +380,7 @@ describe('Test users', function () {
379 expect(users.length).to.equal(2) 380 expect(users.length).to.equal(2)
380 381
381 expect(users[ 0 ].username).to.equal('root') 382 expect(users[ 0 ].username).to.equal('root')
382 expect(users[ 0 ].email).to.equal('admin1@example.com') 383 expect(users[ 0 ].email).to.equal('admin' + server.internalServerNumber + '@example.com')
383 expect(users[ 0 ].nsfwPolicy).to.equal('display') 384 expect(users[ 0 ].nsfwPolicy).to.equal('display')
384 385
385 expect(users[ 1 ].username).to.equal('user_1') 386 expect(users[ 1 ].username).to.equal('user_1')
@@ -467,10 +468,11 @@ describe('Test users', function () {
467 expect(user.autoPlayVideo).to.be.false 468 expect(user.autoPlayVideo).to.be.false
468 }) 469 })
469 470
470 it('Should be able to change the email display attribute', async function () { 471 it('Should be able to change the email attribute', async function () {
471 await updateMyUser({ 472 await updateMyUser({
472 url: server.url, 473 url: server.url,
473 accessToken: accessTokenUser, 474 accessToken: accessTokenUser,
475 currentPassword: 'new password',
474 email: 'updated@example.com' 476 email: 'updated@example.com'
475 }) 477 })
476 478
@@ -617,7 +619,10 @@ describe('Test users', function () {
617 619
618 describe('Registering a new user', function () { 620 describe('Registering a new user', function () {
619 it('Should register a new user', async function () { 621 it('Should register a new user', async function () {
620 await registerUser(server.url, 'user_15', 'my super password') 622 const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
623 const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
624
625 await registerUserWithChannel({ url: server.url, user, channel })
621 }) 626 })
622 627
623 it('Should be able to login with this registered user', async function () { 628 it('Should be able to login with this registered user', async function () {
@@ -629,6 +634,13 @@ describe('Test users', function () {
629 accessToken = await userLogin(server, user15) 634 accessToken = await userLogin(server, user15)
630 }) 635 })
631 636
637 it('Should have the correct display name', async function () {
638 const res = await getMyUserInformation(server.url, accessToken)
639 const user: User = res.body
640
641 expect(user.account.displayName).to.equal('super user 15')
642 })
643
632 it('Should have the correct video quota', async function () { 644 it('Should have the correct video quota', async function () {
633 const res = await getMyUserInformation(server.url, accessToken) 645 const res = await getMyUserInformation(server.url, accessToken)
634 const user = res.body 646 const user = res.body
@@ -636,6 +648,12 @@ describe('Test users', function () {
636 expect(user.videoQuota).to.equal(5 * 1024 * 1024) 648 expect(user.videoQuota).to.equal(5 * 1024 * 1024)
637 }) 649 })
638 650
651 it('Should have created the channel', async function () {
652 const res = await getVideoChannel(server.url, 'my_user_15_channel')
653
654 expect(res.body.displayName).to.equal('my channel rocks')
655 })
656
639 it('Should remove me', async function () { 657 it('Should remove me', async function () {
640 { 658 {
641 const res = await getUsersList(server.url, server.accessToken) 659 const res = await getUsersList(server.url, server.accessToken)
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index 68c1e9a8d..e9625e5f7 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -9,18 +9,17 @@ import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/
9import { 9import {
10 addVideoChannel, 10 addVideoChannel,
11 checkTmpIsEmpty, 11 checkTmpIsEmpty,
12 checkVideoFilesWereRemoved, cleanupTests, 12 checkVideoFilesWereRemoved,
13 cleanupTests,
13 completeVideoCheck, 14 completeVideoCheck,
14 createUser, 15 createUser,
15 dateIsValid, 16 dateIsValid,
16 doubleFollow, 17 doubleFollow,
17 flushAndRunMultipleServers, 18 flushAndRunMultipleServers,
18 flushTests,
19 getLocalVideos, 19 getLocalVideos,
20 getVideo, 20 getVideo,
21 getVideoChannelsList, 21 getVideoChannelsList,
22 getVideosList, 22 getVideosList,
23 killallServers,
24 rateVideo, 23 rateVideo,
25 removeVideo, 24 removeVideo,
26 ServerInfo, 25 ServerInfo,
@@ -110,7 +109,7 @@ describe('Test multiple servers', function () {
110 // All servers should have this video 109 // All servers should have this video
111 let publishedAt: string = null 110 let publishedAt: string = null
112 for (const server of servers) { 111 for (const server of servers) {
113 const isLocal = server.url === 'http://localhost:9001' 112 const isLocal = server.port === servers[0].port
114 const checkAttributes = { 113 const checkAttributes = {
115 name: 'my super name for server 1', 114 name: 'my super name for server 1',
116 category: 5, 115 category: 5,
@@ -122,7 +121,7 @@ describe('Test multiple servers', function () {
122 originallyPublishedAt: '2019-02-10T13:38:14.449Z', 121 originallyPublishedAt: '2019-02-10T13:38:14.449Z',
123 account: { 122 account: {
124 name: 'root', 123 name: 'root',
125 host: 'localhost:9001' 124 host: 'localhost:' + servers[0].port
126 }, 125 },
127 isLocal, 126 isLocal,
128 publishedAt, 127 publishedAt,
@@ -187,7 +186,7 @@ describe('Test multiple servers', function () {
187 186
188 // All servers should have this video 187 // All servers should have this video
189 for (const server of servers) { 188 for (const server of servers) {
190 const isLocal = server.url === 'http://localhost:9002' 189 const isLocal = server.url === 'http://localhost:' + servers[1].port
191 const checkAttributes = { 190 const checkAttributes = {
192 name: 'my super name for server 2', 191 name: 'my super name for server 2',
193 category: 4, 192 category: 4,
@@ -198,7 +197,7 @@ describe('Test multiple servers', function () {
198 support: 'my super support text for server 2', 197 support: 'my super support text for server 2',
199 account: { 198 account: {
200 name: 'user1', 199 name: 'user1',
201 host: 'localhost:9002' 200 host: 'localhost:' + servers[1].port
202 }, 201 },
203 isLocal, 202 isLocal,
204 commentsEnabled: true, 203 commentsEnabled: true,
@@ -216,7 +215,7 @@ describe('Test multiple servers', function () {
216 files: [ 215 files: [
217 { 216 {
218 resolution: 240, 217 resolution: 240,
219 size: 187000 218 size: 189000
220 }, 219 },
221 { 220 {
222 resolution: 360, 221 resolution: 360,
@@ -224,7 +223,7 @@ describe('Test multiple servers', function () {
224 }, 223 },
225 { 224 {
226 resolution: 480, 225 resolution: 480,
227 size: 383000 226 size: 384000
228 }, 227 },
229 { 228 {
230 resolution: 720, 229 resolution: 720,
@@ -278,7 +277,7 @@ describe('Test multiple servers', function () {
278 277
279 // All servers should have this video 278 // All servers should have this video
280 for (const server of servers) { 279 for (const server of servers) {
281 const isLocal = server.url === 'http://localhost:9003' 280 const isLocal = server.url === 'http://localhost:' + servers[2].port
282 const res = await getVideosList(server.url) 281 const res = await getVideosList(server.url)
283 282
284 const videos = res.body.data 283 const videos = res.body.data
@@ -306,7 +305,7 @@ describe('Test multiple servers', function () {
306 support: 'my super support text for server 3', 305 support: 'my super support text for server 3',
307 account: { 306 account: {
308 name: 'root', 307 name: 'root',
309 host: 'localhost:9003' 308 host: 'localhost:' + servers[2].port
310 }, 309 },
311 isLocal, 310 isLocal,
312 duration: 5, 311 duration: 5,
@@ -340,7 +339,7 @@ describe('Test multiple servers', function () {
340 support: 'my super support text for server 3-2', 339 support: 'my super support text for server 3-2',
341 account: { 340 account: {
342 name: 'root', 341 name: 'root',
343 host: 'localhost:9003' 342 host: 'localhost:' + servers[2].port
344 }, 343 },
345 commentsEnabled: true, 344 commentsEnabled: true,
346 downloadEnabled: true, 345 downloadEnabled: true,
@@ -646,7 +645,7 @@ describe('Test multiple servers', function () {
646 const videoUpdated = videos.find(video => video.name === 'my super video updated') 645 const videoUpdated = videos.find(video => video.name === 'my super video updated')
647 expect(!!videoUpdated).to.be.true 646 expect(!!videoUpdated).to.be.true
648 647
649 const isLocal = server.url === 'http://localhost:9003' 648 const isLocal = server.url === 'http://localhost:' + servers[2].port
650 const checkAttributes = { 649 const checkAttributes = {
651 name: 'my super video updated', 650 name: 'my super video updated',
652 category: 10, 651 category: 10,
@@ -658,7 +657,7 @@ describe('Test multiple servers', function () {
658 originallyPublishedAt: '2019-02-11T13:38:14.449Z', 657 originallyPublishedAt: '2019-02-11T13:38:14.449Z',
659 account: { 658 account: {
660 name: 'root', 659 name: 'root',
661 host: 'localhost:9003' 660 host: 'localhost:' + servers[2].port
662 }, 661 },
663 isLocal, 662 isLocal,
664 duration: 5, 663 duration: 5,
@@ -813,7 +812,7 @@ describe('Test multiple servers', function () {
813 expect(comment).to.not.be.undefined 812 expect(comment).to.not.be.undefined
814 expect(comment.inReplyToCommentId).to.be.null 813 expect(comment.inReplyToCommentId).to.be.null
815 expect(comment.account.name).to.equal('root') 814 expect(comment.account.name).to.equal('root')
816 expect(comment.account.host).to.equal('localhost:9001') 815 expect(comment.account.host).to.equal('localhost:' + servers[0].port)
817 expect(comment.totalReplies).to.equal(3) 816 expect(comment.totalReplies).to.equal(3)
818 expect(dateIsValid(comment.createdAt as string)).to.be.true 817 expect(dateIsValid(comment.createdAt as string)).to.be.true
819 expect(dateIsValid(comment.updatedAt as string)).to.be.true 818 expect(dateIsValid(comment.updatedAt as string)).to.be.true
@@ -824,7 +823,7 @@ describe('Test multiple servers', function () {
824 expect(comment).to.not.be.undefined 823 expect(comment).to.not.be.undefined
825 expect(comment.inReplyToCommentId).to.be.null 824 expect(comment.inReplyToCommentId).to.be.null
826 expect(comment.account.name).to.equal('root') 825 expect(comment.account.name).to.equal('root')
827 expect(comment.account.host).to.equal('localhost:9003') 826 expect(comment.account.host).to.equal('localhost:' + servers[2].port)
828 expect(comment.totalReplies).to.equal(0) 827 expect(comment.totalReplies).to.equal(0)
829 expect(dateIsValid(comment.createdAt as string)).to.be.true 828 expect(dateIsValid(comment.createdAt as string)).to.be.true
830 expect(dateIsValid(comment.updatedAt as string)).to.be.true 829 expect(dateIsValid(comment.updatedAt as string)).to.be.true
@@ -842,25 +841,25 @@ describe('Test multiple servers', function () {
842 const tree: VideoCommentThreadTree = res2.body 841 const tree: VideoCommentThreadTree = res2.body
843 expect(tree.comment.text).equal('my super first comment') 842 expect(tree.comment.text).equal('my super first comment')
844 expect(tree.comment.account.name).equal('root') 843 expect(tree.comment.account.name).equal('root')
845 expect(tree.comment.account.host).equal('localhost:9001') 844 expect(tree.comment.account.host).equal('localhost:' + servers[0].port)
846 expect(tree.children).to.have.lengthOf(2) 845 expect(tree.children).to.have.lengthOf(2)
847 846
848 const firstChild = tree.children[0] 847 const firstChild = tree.children[0]
849 expect(firstChild.comment.text).to.equal('my super answer to thread 1') 848 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
850 expect(firstChild.comment.account.name).equal('root') 849 expect(firstChild.comment.account.name).equal('root')
851 expect(firstChild.comment.account.host).equal('localhost:9002') 850 expect(firstChild.comment.account.host).equal('localhost:' + servers[1].port)
852 expect(firstChild.children).to.have.lengthOf(1) 851 expect(firstChild.children).to.have.lengthOf(1)
853 852
854 childOfFirstChild = firstChild.children[0] 853 childOfFirstChild = firstChild.children[0]
855 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') 854 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
856 expect(childOfFirstChild.comment.account.name).equal('root') 855 expect(childOfFirstChild.comment.account.name).equal('root')
857 expect(childOfFirstChild.comment.account.host).equal('localhost:9003') 856 expect(childOfFirstChild.comment.account.host).equal('localhost:' + servers[2].port)
858 expect(childOfFirstChild.children).to.have.lengthOf(0) 857 expect(childOfFirstChild.children).to.have.lengthOf(0)
859 858
860 const secondChild = tree.children[1] 859 const secondChild = tree.children[1]
861 expect(secondChild.comment.text).to.equal('my second answer to thread 1') 860 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
862 expect(secondChild.comment.account.name).equal('root') 861 expect(secondChild.comment.account.name).equal('root')
863 expect(secondChild.comment.account.host).equal('localhost:9003') 862 expect(secondChild.comment.account.host).equal('localhost:' + servers[2].port)
864 expect(secondChild.children).to.have.lengthOf(0) 863 expect(secondChild.children).to.have.lengthOf(0)
865 } 864 }
866 }) 865 })
@@ -915,7 +914,7 @@ describe('Test multiple servers', function () {
915 expect(comment).to.not.be.undefined 914 expect(comment).to.not.be.undefined
916 expect(comment.inReplyToCommentId).to.be.null 915 expect(comment.inReplyToCommentId).to.be.null
917 expect(comment.account.name).to.equal('root') 916 expect(comment.account.name).to.equal('root')
918 expect(comment.account.host).to.equal('localhost:9003') 917 expect(comment.account.host).to.equal('localhost:' + servers[2].port)
919 expect(comment.totalReplies).to.equal(0) 918 expect(comment.totalReplies).to.equal(0)
920 expect(dateIsValid(comment.createdAt as string)).to.be.true 919 expect(dateIsValid(comment.createdAt as string)).to.be.true
921 expect(dateIsValid(comment.updatedAt as string)).to.be.true 920 expect(dateIsValid(comment.updatedAt as string)).to.be.true
@@ -971,7 +970,7 @@ describe('Test multiple servers', function () {
971 const res = await getVideosList(server.url) 970 const res = await getVideosList(server.url)
972 const video = res.body.data.find(v => v.name === 'minimum parameters') 971 const video = res.body.data.find(v => v.name === 'minimum parameters')
973 972
974 const isLocal = server.url === 'http://localhost:9002' 973 const isLocal = server.url === 'http://localhost:' + servers[1].port
975 const checkAttributes = { 974 const checkAttributes = {
976 name: 'minimum parameters', 975 name: 'minimum parameters',
977 category: null, 976 category: null,
@@ -982,7 +981,7 @@ describe('Test multiple servers', function () {
982 support: null, 981 support: null,
983 account: { 982 account: {
984 name: 'root', 983 name: 'root',
985 host: 'localhost:9002' 984 host: 'localhost:' + servers[1].port
986 }, 985 },
987 isLocal, 986 isLocal,
988 duration: 5, 987 duration: 5,
diff --git a/server/tests/api/videos/services.ts b/server/tests/api/videos/services.ts
index e9ad947b2..17172331f 100644
--- a/server/tests/api/videos/services.ts
+++ b/server/tests/api/videos/services.ts
@@ -27,13 +27,13 @@ describe('Test services', function () {
27 }) 27 })
28 28
29 it('Should have a valid oEmbed response', async function () { 29 it('Should have a valid oEmbed response', async function () {
30 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 30 const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + server.video.uuid
31 31
32 const res = await getOEmbed(server.url, oembedUrl) 32 const res = await getOEmbed(server.url, oembedUrl)
33 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + 33 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
34 `src="http://localhost:9001/videos/embed/${server.video.uuid}" ` + 34 `src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` +
35 'frameborder="0" allowfullscreen></iframe>' 35 'frameborder="0" allowfullscreen></iframe>'
36 const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg' 36 const expectedThumbnailUrl = 'http://localhost:' + server.port + '/static/previews/' + server.video.uuid + '.jpg'
37 37
38 expect(res.body.html).to.equal(expectedHtml) 38 expect(res.body.html).to.equal(expectedHtml)
39 expect(res.body.title).to.equal(server.video.name) 39 expect(res.body.title).to.equal(server.video.name)
@@ -41,19 +41,19 @@ describe('Test services', function () {
41 expect(res.body.width).to.equal(560) 41 expect(res.body.width).to.equal(560)
42 expect(res.body.height).to.equal(315) 42 expect(res.body.height).to.equal(315)
43 expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) 43 expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
44 expect(res.body.thumbnail_width).to.equal(560) 44 expect(res.body.thumbnail_width).to.equal(850)
45 expect(res.body.thumbnail_height).to.equal(315) 45 expect(res.body.thumbnail_height).to.equal(480)
46 }) 46 })
47 47
48 it('Should have a valid oEmbed response with small max height query', async function () { 48 it('Should have a valid oEmbed response with small max height query', async function () {
49 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 49 const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + server.video.uuid
50 const format = 'json' 50 const format = 'json'
51 const maxHeight = 50 51 const maxHeight = 50
52 const maxWidth = 50 52 const maxWidth = 50
53 53
54 const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) 54 const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
55 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + 55 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' +
56 `src="http://localhost:9001/videos/embed/${server.video.uuid}" ` + 56 `src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` +
57 'frameborder="0" allowfullscreen></iframe>' 57 'frameborder="0" allowfullscreen></iframe>'
58 58
59 expect(res.body.html).to.equal(expectedHtml) 59 expect(res.body.html).to.equal(expectedHtml)
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index 1f366b642..d8f394ac7 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -37,7 +37,7 @@ describe('Test a single server', function () {
37 let videoUUID = '' 37 let videoUUID = ''
38 let videosListBase: any[] = null 38 let videosListBase: any[] = null
39 39
40 const getCheckAttributes = { 40 const getCheckAttributes = () => ({
41 name: 'my super name', 41 name: 'my super name',
42 category: 2, 42 category: 2,
43 licence: 6, 43 licence: 6,
@@ -47,7 +47,7 @@ describe('Test a single server', function () {
47 support: 'my super support text', 47 support: 'my super support text',
48 account: { 48 account: {
49 name: 'root', 49 name: 'root',
50 host: 'localhost:9001' 50 host: 'localhost:' + server.port
51 }, 51 },
52 isLocal: true, 52 isLocal: true,
53 duration: 5, 53 duration: 5,
@@ -68,9 +68,9 @@ describe('Test a single server', function () {
68 size: 218910 68 size: 218910
69 } 69 }
70 ] 70 ]
71 } 71 })
72 72
73 const updateCheckAttributes = { 73 const updateCheckAttributes = () => ({
74 name: 'my super video updated', 74 name: 'my super video updated',
75 category: 4, 75 category: 4,
76 licence: 2, 76 licence: 2,
@@ -80,7 +80,7 @@ describe('Test a single server', function () {
80 support: 'my super support text updated', 80 support: 'my super support text updated',
81 account: { 81 account: {
82 name: 'root', 82 name: 'root',
83 host: 'localhost:9001' 83 host: 'localhost:' + server.port
84 }, 84 },
85 isLocal: true, 85 isLocal: true,
86 tags: [ 'tagup1', 'tagup2' ], 86 tags: [ 'tagup1', 'tagup2' ],
@@ -101,7 +101,7 @@ describe('Test a single server', function () {
101 size: 292677 101 size: 292677
102 } 102 }
103 ] 103 ]
104 } 104 })
105 105
106 before(async function () { 106 before(async function () {
107 this.timeout(30000) 107 this.timeout(30000)
@@ -182,7 +182,7 @@ describe('Test a single server', function () {
182 expect(res.body.data.length).to.equal(1) 182 expect(res.body.data.length).to.equal(1)
183 183
184 const video = res.body.data[0] 184 const video = res.body.data[0]
185 await completeVideoCheck(server.url, video, getCheckAttributes) 185 await completeVideoCheck(server.url, video, getCheckAttributes())
186 }) 186 })
187 187
188 it('Should get the video by UUID', async function () { 188 it('Should get the video by UUID', async function () {
@@ -191,7 +191,7 @@ describe('Test a single server', function () {
191 const res = await getVideo(server.url, videoUUID) 191 const res = await getVideo(server.url, videoUUID)
192 192
193 const video = res.body 193 const video = res.body
194 await completeVideoCheck(server.url, video, getCheckAttributes) 194 await completeVideoCheck(server.url, video, getCheckAttributes())
195 }) 195 })
196 196
197 it('Should have the views updated', async function () { 197 it('Should have the views updated', async function () {
@@ -376,7 +376,7 @@ describe('Test a single server', function () {
376 const res = await getVideo(server.url, videoId) 376 const res = await getVideo(server.url, videoId)
377 const video = res.body 377 const video = res.body
378 378
379 await completeVideoCheck(server.url, video, updateCheckAttributes) 379 await completeVideoCheck(server.url, video, updateCheckAttributes())
380 }) 380 })
381 381
382 it('Should update only the tags of a video', async function () { 382 it('Should update only the tags of a video', async function () {
@@ -388,7 +388,7 @@ describe('Test a single server', function () {
388 const res = await getVideo(server.url, videoId) 388 const res = await getVideo(server.url, videoId)
389 const video = res.body 389 const video = res.body
390 390
391 await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes, attributes)) 391 await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes))
392 }) 392 })
393 393
394 it('Should update only the description of a video', async function () { 394 it('Should update only the description of a video', async function () {
@@ -400,7 +400,8 @@ describe('Test a single server', function () {
400 const res = await getVideo(server.url, videoId) 400 const res = await getVideo(server.url, videoId)
401 const video = res.body 401 const video = res.body
402 402
403 await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes, attributes)) 403 const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
404 await completeVideoCheck(server.url, video, expectedAttributes)
404 }) 405 })
405 406
406 it('Should like a video', async function () { 407 it('Should like a video', async function () {
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 7318497d5..a2f3ee161 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -9,7 +9,6 @@ import {
9 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
10 getVideoAbusesList, 10 getVideoAbusesList,
11 getVideosList, 11 getVideosList,
12 killallServers,
13 reportVideoAbuse, 12 reportVideoAbuse,
14 ServerInfo, 13 ServerInfo,
15 setAccessTokensToServers, 14 setAccessTokensToServers,
@@ -90,7 +89,7 @@ describe('Test video abuses', function () {
90 const abuse: VideoAbuse = res1.body.data[0] 89 const abuse: VideoAbuse = res1.body.data[0]
91 expect(abuse.reason).to.equal('my super bad reason') 90 expect(abuse.reason).to.equal('my super bad reason')
92 expect(abuse.reporterAccount.name).to.equal('root') 91 expect(abuse.reporterAccount.name).to.equal('root')
93 expect(abuse.reporterAccount.host).to.equal('localhost:9001') 92 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
94 expect(abuse.video.id).to.equal(servers[0].video.id) 93 expect(abuse.video.id).to.equal(servers[0].video.id)
95 94
96 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 95 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
@@ -118,7 +117,7 @@ describe('Test video abuses', function () {
118 const abuse1: VideoAbuse = res1.body.data[0] 117 const abuse1: VideoAbuse = res1.body.data[0]
119 expect(abuse1.reason).to.equal('my super bad reason') 118 expect(abuse1.reason).to.equal('my super bad reason')
120 expect(abuse1.reporterAccount.name).to.equal('root') 119 expect(abuse1.reporterAccount.name).to.equal('root')
121 expect(abuse1.reporterAccount.host).to.equal('localhost:9001') 120 expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
122 expect(abuse1.video.id).to.equal(servers[0].video.id) 121 expect(abuse1.video.id).to.equal(servers[0].video.id)
123 expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING) 122 expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING)
124 expect(abuse1.state.label).to.equal('Pending') 123 expect(abuse1.state.label).to.equal('Pending')
@@ -127,7 +126,7 @@ describe('Test video abuses', function () {
127 const abuse2: VideoAbuse = res1.body.data[1] 126 const abuse2: VideoAbuse = res1.body.data[1]
128 expect(abuse2.reason).to.equal('my super bad reason 2') 127 expect(abuse2.reason).to.equal('my super bad reason 2')
129 expect(abuse2.reporterAccount.name).to.equal('root') 128 expect(abuse2.reporterAccount.name).to.equal('root')
130 expect(abuse2.reporterAccount.host).to.equal('localhost:9001') 129 expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
131 expect(abuse2.video.id).to.equal(servers[1].video.id) 130 expect(abuse2.video.id).to.equal(servers[1].video.id)
132 expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING) 131 expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING)
133 expect(abuse2.state.label).to.equal('Pending') 132 expect(abuse2.state.label).to.equal('Pending')
@@ -141,7 +140,7 @@ describe('Test video abuses', function () {
141 abuseServer2 = res2.body.data[0] 140 abuseServer2 = res2.body.data[0]
142 expect(abuseServer2.reason).to.equal('my super bad reason 2') 141 expect(abuseServer2.reason).to.equal('my super bad reason 2')
143 expect(abuseServer2.reporterAccount.name).to.equal('root') 142 expect(abuseServer2.reporterAccount.name).to.equal('root')
144 expect(abuseServer2.reporterAccount.host).to.equal('localhost:9001') 143 expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
145 expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING) 144 expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING)
146 expect(abuseServer2.state.label).to.equal('Pending') 145 expect(abuseServer2.state.label).to.equal('Pending')
147 expect(abuseServer2.moderationComment).to.be.null 146 expect(abuseServer2.moderationComment).to.be.null
diff --git a/server/tests/api/videos/video-change-ownership.ts b/server/tests/api/videos/video-change-ownership.ts
index 1c0327d40..3a3add71b 100644
--- a/server/tests/api/videos/video-change-ownership.ts
+++ b/server/tests/api/videos/video-change-ownership.ts
@@ -4,7 +4,8 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 acceptChangeOwnership, 6 acceptChangeOwnership,
7 changeVideoOwnership, cleanupTests, 7 changeVideoOwnership,
8 cleanupTests,
8 createUser, 9 createUser,
9 doubleFollow, 10 doubleFollow,
10 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
@@ -13,7 +14,6 @@ import {
13 getVideo, 14 getVideo,
14 getVideoChangeOwnershipList, 15 getVideoChangeOwnershipList,
15 getVideosList, 16 getVideosList,
16 killallServers,
17 refuseChangeOwnership, 17 refuseChangeOwnership,
18 ServerInfo, 18 ServerInfo,
19 setAccessTokensToServers, 19 setAccessTokensToServers,
@@ -203,8 +203,8 @@ describe('Test video change ownership - nominal', function () {
203 } 203 }
204 }) 204 })
205 205
206 after(function () { 206 after(async function () {
207 killallServers(servers) 207 await cleanupTests(servers)
208 }) 208 })
209}) 209})
210 210
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 345e96f43..4f600cae8 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -2,12 +2,12 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { User, Video } from '../../../../shared/index' 5import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 createUser, 8 createUser,
9 doubleFollow, 9 doubleFollow,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers, getVideo,
11 getVideoChannelVideos, 11 getVideoChannelVideos,
12 testImage, 12 testImage,
13 updateVideo, 13 updateVideo,
@@ -18,12 +18,10 @@ import {
18import { 18import {
19 addVideoChannel, 19 addVideoChannel,
20 deleteVideoChannel, 20 deleteVideoChannel,
21 flushTests,
22 getAccountVideoChannelsList, 21 getAccountVideoChannelsList,
23 getMyUserInformation, 22 getMyUserInformation,
24 getVideoChannel, 23 getVideoChannel,
25 getVideoChannelsList, 24 getVideoChannelsList,
26 killallServers,
27 ServerInfo, 25 ServerInfo,
28 setAccessTokensToServers, 26 setAccessTokensToServers,
29 updateVideoChannel 27 updateVideoChannel
@@ -35,13 +33,12 @@ const expect = chai.expect
35describe('Test video channels', function () { 33describe('Test video channels', function () {
36 let servers: ServerInfo[] 34 let servers: ServerInfo[]
37 let userInfo: User 35 let userInfo: User
38 let accountUUID: string
39 let firstVideoChannelId: number 36 let firstVideoChannelId: number
40 let secondVideoChannelId: number 37 let secondVideoChannelId: number
41 let videoUUID: string 38 let videoUUID: string
42 39
43 before(async function () { 40 before(async function () {
44 this.timeout(30000) 41 this.timeout(60000)
45 42
46 servers = await flushAndRunMultipleServers(2) 43 servers = await flushAndRunMultipleServers(2)
47 44
@@ -51,7 +48,6 @@ describe('Test video channels', function () {
51 { 48 {
52 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) 49 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
53 const user: User = res.body 50 const user: User = res.body
54 accountUUID = user.account.uuid
55 51
56 firstVideoChannelId = user.videoChannels[0].id 52 firstVideoChannelId = user.videoChannels[0].id
57 } 53 }
@@ -83,7 +79,8 @@ describe('Test video channels', function () {
83 79
84 // The channel is 1 is propagated to servers 2 80 // The channel is 1 is propagated to servers 2
85 { 81 {
86 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'my video name', channelId: secondVideoChannelId }) 82 const videoAttributesArg = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' }
83 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAttributesArg)
87 videoUUID = res.body.video.uuid 84 videoUUID = res.body.video.uuid
88 } 85 }
89 86
@@ -108,7 +105,11 @@ describe('Test video channels', function () {
108 }) 105 })
109 106
110 it('Should have two video channels when getting account channels on server 1', async function () { 107 it('Should have two video channels when getting account channels on server 1', async function () {
111 const res = await getAccountVideoChannelsList(servers[0].url, userInfo.account.name + '@' + userInfo.account.host) 108 const res = await getAccountVideoChannelsList({
109 url: servers[ 0 ].url,
110 accountName: userInfo.account.name + '@' + userInfo.account.host
111 })
112
112 expect(res.body.total).to.equal(2) 113 expect(res.body.total).to.equal(2)
113 expect(res.body.data).to.be.an('array') 114 expect(res.body.data).to.be.an('array')
114 expect(res.body.data).to.have.lengthOf(2) 115 expect(res.body.data).to.have.lengthOf(2)
@@ -123,8 +124,62 @@ describe('Test video channels', function () {
123 expect(videoChannels[1].support).to.equal('super video channel support text') 124 expect(videoChannels[1].support).to.equal('super video channel support text')
124 }) 125 })
125 126
127 it('Should paginate and sort account channels', async function () {
128 {
129 const res = await getAccountVideoChannelsList({
130 url: servers[ 0 ].url,
131 accountName: userInfo.account.name + '@' + userInfo.account.host,
132 start: 0,
133 count: 1,
134 sort: 'createdAt'
135 })
136
137 expect(res.body.total).to.equal(2)
138 expect(res.body.data).to.have.lengthOf(1)
139
140 const videoChannel: VideoChannel = res.body.data[ 0 ]
141 expect(videoChannel.name).to.equal('root_channel')
142 }
143
144 {
145 const res = await getAccountVideoChannelsList({
146 url: servers[ 0 ].url,
147 accountName: userInfo.account.name + '@' + userInfo.account.host,
148 start: 0,
149 count: 1,
150 sort: '-createdAt'
151 })
152
153 expect(res.body.total).to.equal(2)
154 expect(res.body.data).to.have.lengthOf(1)
155
156 const videoChannel: VideoChannel = res.body.data[ 0 ]
157 expect(videoChannel.name).to.equal('second_video_channel')
158 }
159
160 {
161 const res = await getAccountVideoChannelsList({
162 url: servers[ 0 ].url,
163 accountName: userInfo.account.name + '@' + userInfo.account.host,
164 start: 1,
165 count: 1,
166 sort: '-createdAt'
167 })
168
169 expect(res.body.total).to.equal(2)
170 expect(res.body.data).to.have.lengthOf(1)
171
172 const videoChannel: VideoChannel = res.body.data[ 0 ]
173 expect(videoChannel.name).to.equal('root_channel')
174 }
175 })
176
126 it('Should have one video channel when getting account channels on server 2', async function () { 177 it('Should have one video channel when getting account channels on server 2', async function () {
127 const res = await getAccountVideoChannelsList(servers[1].url, userInfo.account.name + '@' + userInfo.account.host) 178 const res = await getAccountVideoChannelsList({
179 url: servers[ 1 ].url,
180 accountName: userInfo.account.name + '@' + userInfo.account.host
181 })
182
128 expect(res.body.total).to.equal(1) 183 expect(res.body.total).to.equal(1)
129 expect(res.body.data).to.be.an('array') 184 expect(res.body.data).to.be.an('array')
130 expect(res.body.data).to.have.lengthOf(1) 185 expect(res.body.data).to.have.lengthOf(1)
@@ -147,12 +202,12 @@ describe('Test video channels', function () {
147 }) 202 })
148 203
149 it('Should update video channel', async function () { 204 it('Should update video channel', async function () {
150 this.timeout(5000) 205 this.timeout(15000)
151 206
152 const videoChannelAttributes = { 207 const videoChannelAttributes = {
153 displayName: 'video channel updated', 208 displayName: 'video channel updated',
154 description: 'video channel description updated', 209 description: 'video channel description updated',
155 support: 'video channel support text updated' 210 support: 'support updated'
156 } 211 }
157 212
158 await updateVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel', videoChannelAttributes) 213 await updateVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel', videoChannelAttributes)
@@ -170,7 +225,36 @@ describe('Test video channels', function () {
170 expect(res.body.data[0].name).to.equal('second_video_channel') 225 expect(res.body.data[0].name).to.equal('second_video_channel')
171 expect(res.body.data[0].displayName).to.equal('video channel updated') 226 expect(res.body.data[0].displayName).to.equal('video channel updated')
172 expect(res.body.data[0].description).to.equal('video channel description updated') 227 expect(res.body.data[0].description).to.equal('video channel description updated')
173 expect(res.body.data[0].support).to.equal('video channel support text updated') 228 expect(res.body.data[0].support).to.equal('support updated')
229 }
230 })
231
232 it('Should not have updated the video support field', async function () {
233 for (const server of servers) {
234 const res = await getVideo(server.url, videoUUID)
235 const video: VideoDetails = res.body
236
237 expect(video.support).to.equal('video support field')
238 }
239 })
240
241 it('Should update the channel support field and update videos too', async function () {
242 this.timeout(35000)
243
244 const videoChannelAttributes = {
245 support: 'video channel support text updated',
246 bulkVideosSupportUpdate: true
247 }
248
249 await updateVideoChannel(servers[0].url, servers[0].accessToken, 'second_video_channel', videoChannelAttributes)
250
251 await waitJobs(servers)
252
253 for (const server of servers) {
254 const res = await getVideo(server.url, videoUUID)
255 const video: VideoDetails = res.body
256
257 expect(video.support).to.equal(videoChannelAttributes.support)
174 } 258 }
175 }) 259 })
176 260
@@ -213,7 +297,7 @@ describe('Test video channels', function () {
213 this.timeout(10000) 297 this.timeout(10000)
214 298
215 for (const server of servers) { 299 for (const server of servers) {
216 const channelURI = 'second_video_channel@localhost:9001' 300 const channelURI = 'second_video_channel@localhost:' + servers[0].port
217 const res1 = await getVideoChannelVideos(server.url, server.accessToken, channelURI, 0, 5) 301 const res1 = await getVideoChannelVideos(server.url, server.accessToken, channelURI, 0, 5)
218 expect(res1.body.total).to.equal(1) 302 expect(res1.body.total).to.equal(1)
219 expect(res1.body.data).to.be.an('array') 303 expect(res1.body.data).to.be.an('array')
@@ -234,11 +318,11 @@ describe('Test video channels', function () {
234 this.timeout(10000) 318 this.timeout(10000)
235 319
236 for (const server of servers) { 320 for (const server of servers) {
237 const secondChannelURI = 'second_video_channel@localhost:9001' 321 const secondChannelURI = 'second_video_channel@localhost:' + servers[0].port
238 const res1 = await getVideoChannelVideos(server.url, server.accessToken, secondChannelURI, 0, 5) 322 const res1 = await getVideoChannelVideos(server.url, server.accessToken, secondChannelURI, 0, 5)
239 expect(res1.body.total).to.equal(0) 323 expect(res1.body.total).to.equal(0)
240 324
241 const channelURI = 'root_channel@localhost:9001' 325 const channelURI = 'root_channel@localhost:' + servers[0].port
242 const res2 = await getVideoChannelVideos(server.url, server.accessToken, channelURI, 0, 5) 326 const res2 = await getVideoChannelVideos(server.url, server.accessToken, channelURI, 0, 5)
243 expect(res2.body.total).to.equal(1) 327 expect(res2.body.total).to.equal(1)
244 328
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts
index 22fd8c058..82182cc7c 100644
--- a/server/tests/api/videos/video-comments.ts
+++ b/server/tests/api/videos/video-comments.ts
@@ -66,8 +66,8 @@ describe('Test video comments', function () {
66 expect(comment.videoId).to.equal(videoId) 66 expect(comment.videoId).to.equal(videoId)
67 expect(comment.id).to.equal(comment.threadId) 67 expect(comment.id).to.equal(comment.threadId)
68 expect(comment.account.name).to.equal('root') 68 expect(comment.account.name).to.equal('root')
69 expect(comment.account.host).to.equal('localhost:9001') 69 expect(comment.account.host).to.equal('localhost:' + server.port)
70 expect(comment.account.url).to.equal('http://localhost:9001/accounts/root') 70 expect(comment.account.url).to.equal('http://localhost:' + server.port + '/accounts/root')
71 expect(comment.totalReplies).to.equal(0) 71 expect(comment.totalReplies).to.equal(0)
72 expect(dateIsValid(comment.createdAt as string)).to.be.true 72 expect(dateIsValid(comment.createdAt as string)).to.be.true
73 expect(dateIsValid(comment.updatedAt as string)).to.be.true 73 expect(dateIsValid(comment.updatedAt as string)).to.be.true
@@ -86,7 +86,7 @@ describe('Test video comments', function () {
86 expect(comment.videoId).to.equal(videoId) 86 expect(comment.videoId).to.equal(videoId)
87 expect(comment.id).to.equal(comment.threadId) 87 expect(comment.id).to.equal(comment.threadId)
88 expect(comment.account.name).to.equal('root') 88 expect(comment.account.name).to.equal('root')
89 expect(comment.account.host).to.equal('localhost:9001') 89 expect(comment.account.host).to.equal('localhost:' + server.port)
90 90
91 await testImage(server.url, 'avatar-resized', comment.account.avatar.path, '.png') 91 await testImage(server.url, 'avatar-resized', comment.account.avatar.path, '.png')
92 92
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts
index 22031c18b..39178bb1a 100644
--- a/server/tests/api/videos/video-hls.ts
+++ b/server/tests/api/videos/video-hls.ts
@@ -5,13 +5,12 @@ import 'mocha'
5import { 5import {
6 checkDirectoryIsEmpty, 6 checkDirectoryIsEmpty,
7 checkSegmentHash, 7 checkSegmentHash,
8 checkTmpIsEmpty, cleanupTests, 8 checkTmpIsEmpty,
9 cleanupTests,
9 doubleFollow, 10 doubleFollow,
10 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
11 flushTests,
12 getPlaylist, 12 getPlaylist,
13 getVideo, 13 getVideo,
14 killallServers,
15 removeVideo, 14 removeVideo,
16 ServerInfo, 15 ServerInfo,
17 setAccessTokensToServers, 16 setAccessTokensToServers,
@@ -22,12 +21,11 @@ import {
22import { VideoDetails } from '../../../../shared/models/videos' 21import { VideoDetails } from '../../../../shared/models/videos'
23import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' 22import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
24import { join } from 'path' 23import { join } from 'path'
24import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants'
25 25
26const expect = chai.expect 26const expect = chai.expect
27 27
28async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) { 28async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, resolutions = [ 240, 360, 480, 720 ]) {
29 const resolutions = [ 240, 360, 480, 720 ]
30
31 for (const server of servers) { 29 for (const server of servers) {
32 const res = await getVideo(server.url, videoUUID) 30 const res = await getVideo(server.url, videoUUID)
33 const videoDetails: VideoDetails = res.body 31 const videoDetails: VideoDetails = res.body
@@ -42,16 +40,15 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) {
42 40
43 const masterPlaylist = res2.text 41 const masterPlaylist = res2.text
44 42
45 expect(masterPlaylist).to.contain('#EXT-X-STREAM-INF:BANDWIDTH=55472,RESOLUTION=640x360,FRAME-RATE=25')
46
47 for (const resolution of resolutions) { 43 for (const resolution of resolutions) {
44 expect(masterPlaylist).to.match(new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+'))
48 expect(masterPlaylist).to.contain(`${resolution}.m3u8`) 45 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
49 } 46 }
50 } 47 }
51 48
52 { 49 {
53 for (const resolution of resolutions) { 50 for (const resolution of resolutions) {
54 const res2 = await getPlaylist(`http://localhost:9001/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`) 51 const res2 = await getPlaylist(`http://localhost:${servers[0].port}/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
55 52
56 const subPlaylist = res2.text 53 const subPlaylist = res2.text
57 expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) 54 expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
@@ -59,7 +56,7 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) {
59 } 56 }
60 57
61 { 58 {
62 const baseUrl = 'http://localhost:9001/static/streaming-playlists/hls' 59 const baseUrl = 'http://localhost:' + servers[0].port + '/static/streaming-playlists/hls'
63 60
64 for (const resolution of resolutions) { 61 for (const resolution of resolutions) {
65 await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist) 62 await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
@@ -71,11 +68,21 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) {
71describe('Test HLS videos', function () { 68describe('Test HLS videos', function () {
72 let servers: ServerInfo[] = [] 69 let servers: ServerInfo[] = []
73 let videoUUID = '' 70 let videoUUID = ''
71 let videoAudioUUID = ''
74 72
75 before(async function () { 73 before(async function () {
76 this.timeout(120000) 74 this.timeout(120000)
77 75
78 servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: true, hls: { enabled: true } } }) 76 const configOverride = {
77 transcoding: {
78 enabled: true,
79 allow_audio_files: true,
80 hls: {
81 enabled: true
82 }
83 }
84 }
85 servers = await flushAndRunMultipleServers(2, configOverride)
79 86
80 // Get the access tokens 87 // Get the access tokens
81 await setAccessTokensToServers(servers) 88 await setAccessTokensToServers(servers)
@@ -87,17 +94,28 @@ describe('Test HLS videos', function () {
87 it('Should upload a video and transcode it to HLS', async function () { 94 it('Should upload a video and transcode it to HLS', async function () {
88 this.timeout(120000) 95 this.timeout(120000)
89 96
90 { 97 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
91 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) 98 videoUUID = res.body.video.uuid
92 videoUUID = res.body.video.uuid
93 }
94 99
95 await waitJobs(servers) 100 await waitJobs(servers)
96 101
97 await checkHlsPlaylist(servers, videoUUID) 102 await checkHlsPlaylist(servers, videoUUID)
98 }) 103 })
99 104
105 it('Should upload an audio file and transcode it to HLS', async function () {
106 this.timeout(120000)
107
108 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' })
109 videoAudioUUID = res.body.video.uuid
110
111 await waitJobs(servers)
112
113 await checkHlsPlaylist(servers, videoAudioUUID, [ DEFAULT_AUDIO_RESOLUTION ])
114 })
115
100 it('Should update the video', async function () { 116 it('Should update the video', async function () {
117 this.timeout(10000)
118
101 await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' }) 119 await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' })
102 120
103 await waitJobs(servers) 121 await waitJobs(servers)
@@ -105,13 +123,17 @@ describe('Test HLS videos', function () {
105 await checkHlsPlaylist(servers, videoUUID) 123 await checkHlsPlaylist(servers, videoUUID)
106 }) 124 })
107 125
108 it('Should delete the video', async function () { 126 it('Should delete videos', async function () {
127 this.timeout(10000)
128
109 await removeVideo(servers[0].url, servers[0].accessToken, videoUUID) 129 await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
130 await removeVideo(servers[0].url, servers[0].accessToken, videoAudioUUID)
110 131
111 await waitJobs(servers) 132 await waitJobs(servers)
112 133
113 for (const server of servers) { 134 for (const server of servers) {
114 await getVideo(server.url, videoUUID, 404) 135 await getVideo(server.url, videoUUID, 404)
136 await getVideo(server.url, videoAudioUUID, 404)
115 } 137 }
116 }) 138 })
117 139
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts
index fd5e4c4be..f82c8cbce 100644
--- a/server/tests/api/videos/video-playlists.ts
+++ b/server/tests/api/videos/video-playlists.ts
@@ -371,7 +371,7 @@ describe('Test video playlists', function () {
371 371
372 for (const server of servers) { 372 for (const server of servers) {
373 const results = [ 373 const results = [
374 await getAccountPlaylistsList(server.url, 'root@localhost:9002', 0, 5, '-createdAt'), 374 await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[1].port, 0, 5, '-createdAt'),
375 await getVideoPlaylistsList(server.url, 0, 2, '-createdAt') 375 await getVideoPlaylistsList(server.url, 0, 2, '-createdAt')
376 ] 376 ]
377 377
@@ -754,6 +754,39 @@ describe('Test video playlists', function () {
754 } 754 }
755 }) 755 })
756 756
757 it('Should be able to create a public playlist, and set it to private', async function () {
758 this.timeout(30000)
759
760 const res = await createVideoPlaylist({
761 url: servers[0].url,
762 token: servers[0].accessToken,
763 playlistAttrs: {
764 displayName: 'my super public playlist',
765 privacy: VideoPlaylistPrivacy.PUBLIC,
766 videoChannelId: servers[0].videoChannel.id
767 }
768 })
769 const videoPlaylistIds = res.body.videoPlaylist
770
771 await waitJobs(servers)
772
773 for (const server of servers) {
774 await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 200)
775 }
776
777 const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE }
778 await updateVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs })
779
780 await waitJobs(servers)
781
782 for (const server of [ servers[1], servers[2] ]) {
783 await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 404)
784 }
785 await getVideoPlaylist(servers[0].url, videoPlaylistIds.uuid, 401)
786
787 await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistIds.uuid, 200)
788 })
789
757 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { 790 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
758 this.timeout(30000) 791 this.timeout(30000)
759 792
@@ -770,7 +803,7 @@ describe('Test video playlists', function () {
770 this.timeout(30000) 803 this.timeout(30000)
771 804
772 for (const server of servers) { 805 for (const server of servers) {
773 await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.serverNumber) 806 await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.internalServerNumber)
774 } 807 }
775 }) 808 })
776 809
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts
index ef1cf0f07..40b539106 100644
--- a/server/tests/api/videos/video-privacy.ts
+++ b/server/tests/api/videos/video-privacy.ts
@@ -6,8 +6,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enu
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
9 getVideosList, 9 getVideosList, getVideosListWithToken,
10 killallServers,
11 ServerInfo, 10 ServerInfo,
12 setAccessTokensToServers, 11 setAccessTokensToServers,
13 uploadVideo 12 uploadVideo
@@ -153,6 +152,29 @@ describe('Test video privacy', function () {
153 } 152 }
154 }) 153 })
155 154
155 it('Should set this new video as private', async function () {
156 this.timeout(10000)
157
158 await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.PRIVATE })
159
160 await waitJobs(servers)
161
162 for (const server of servers) {
163 const res = await getVideosList(server.url)
164
165 expect(res.body.total).to.equal(0)
166 expect(res.body.data).to.have.lengthOf(0)
167 }
168
169 {
170 const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
171
172 expect(res.body.total).to.equal(1)
173 expect(res.body.data).to.have.lengthOf(1)
174 expect(res.body.data[0].name).to.equal('super video public')
175 }
176 })
177
156 after(async function () { 178 after(async function () {
157 await cleanupTests(servers) 179 await cleanupTests(servers)
158 }) 180 })
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 3cd43e99b..90ade1652 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -4,24 +4,25 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' 6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
7import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 7import { audio, canDoQuickTranscode, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
8import { 8import {
9 buildAbsoluteFixturePath, cleanupTests, 9 buildAbsoluteFixturePath,
10 cleanupTests,
10 doubleFollow, 11 doubleFollow,
11 flushAndRunMultipleServers, 12 flushAndRunMultipleServers,
12 generateHighBitrateVideo, 13 generateHighBitrateVideo,
13 getMyVideos, 14 getMyVideos,
14 getVideo, 15 getVideo,
15 getVideosList, 16 getVideosList,
16 killallServers, 17 makeGetRequest,
17 root, 18 root,
18 ServerInfo, 19 ServerInfo,
19 setAccessTokensToServers, 20 setAccessTokensToServers,
20 uploadVideo, 21 uploadVideo,
22 waitJobs,
21 webtorrentAdd 23 webtorrentAdd
22} from '../../../../shared/extra-utils' 24} from '../../../../shared/extra-utils'
23import { extname, join } from 'path' 25import { join } from 'path'
24import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
25import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' 26import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
26 27
27const expect = chai.expect 28const expect = chai.expect
@@ -121,7 +122,7 @@ describe('Test video transcoding', function () {
121 122
122 expect(videoDetails.files).to.have.lengthOf(4) 123 expect(videoDetails.files).to.have.lengthOf(4)
123 124
124 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 125 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
125 const probe = await audio.get(path) 126 const probe = await audio.get(path)
126 127
127 if (probe.audioStream) { 128 if (probe.audioStream) {
@@ -152,7 +153,7 @@ describe('Test video transcoding', function () {
152 const videoDetails: VideoDetails = res2.body 153 const videoDetails: VideoDetails = res2.body
153 154
154 expect(videoDetails.files).to.have.lengthOf(4) 155 expect(videoDetails.files).to.have.lengthOf(4)
155 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 156 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
156 const probe = await audio.get(path) 157 const probe = await audio.get(path)
157 expect(probe).to.not.have.property('audioStream') 158 expect(probe).to.not.have.property('audioStream')
158 } 159 }
@@ -179,7 +180,7 @@ describe('Test video transcoding', function () {
179 expect(videoDetails.files).to.have.lengthOf(4) 180 expect(videoDetails.files).to.have.lengthOf(4)
180 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) 181 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
181 const fixtureVideoProbe = await audio.get(fixturePath) 182 const fixtureVideoProbe = await audio.get(fixturePath)
182 const path = join(root(), 'test2', 'videos', video.uuid + '-240.mp4') 183 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
183 const videoProbe = await audio.get(path) 184 const videoProbe = await audio.get(path)
184 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { 185 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
185 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] 186 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
@@ -216,13 +217,13 @@ describe('Test video transcoding', function () {
216 expect(videoDetails.files[ 3 ].fps).to.be.below(31) 217 expect(videoDetails.files[ 3 ].fps).to.be.below(31)
217 218
218 for (const resolution of [ '240', '360', '480' ]) { 219 for (const resolution of [ '240', '360', '480' ]) {
219 const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4') 220 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4')
220 const fps = await getVideoFileFPS(path) 221 const fps = await getVideoFileFPS(path)
221 222
222 expect(fps).to.be.below(31) 223 expect(fps).to.be.below(31)
223 } 224 }
224 225
225 const path = join(root(), 'test2', 'videos', video.uuid + '-720.mp4') 226 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-720.mp4')
226 const fps = await getVideoFileFPS(path) 227 const fps = await getVideoFileFPS(path)
227 228
228 expect(fps).to.be.above(58).and.below(62) 229 expect(fps).to.be.above(58).and.below(62)
@@ -310,7 +311,7 @@ describe('Test video transcoding', function () {
310 const video = res.body.data.find(v => v.name === videoAttributes.name) 311 const video = res.body.data.find(v => v.name === videoAttributes.name)
311 312
312 for (const resolution of ['240', '360', '480', '720', '1080']) { 313 for (const resolution of ['240', '360', '480', '720', '1080']) {
313 const path = join(root(), 'test2', 'videos', video.uuid + '-' + resolution + '.mp4') 314 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4')
314 const bitrate = await getVideoFileBitrate(path) 315 const bitrate = await getVideoFileBitrate(path)
315 const fps = await getVideoFileFPS(path) 316 const fps = await getVideoFileFPS(path)
316 const resolution2 = await getVideoFileResolution(path) 317 const resolution2 = await getVideoFileResolution(path)
@@ -324,6 +325,15 @@ describe('Test video transcoding', function () {
324 it('Should accept and transcode additional extensions', async function () { 325 it('Should accept and transcode additional extensions', async function () {
325 this.timeout(300000) 326 this.timeout(300000)
326 327
328 let tempFixturePath: string
329
330 {
331 tempFixturePath = await generateHighBitrateVideo()
332
333 const bitrate = await getVideoFileBitrate(tempFixturePath)
334 expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 60, VIDEO_TRANSCODING_FPS))
335 }
336
327 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { 337 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
328 const videoAttributes = { 338 const videoAttributes = {
329 name: fixture, 339 name: fixture,
@@ -349,6 +359,63 @@ describe('Test video transcoding', function () {
349 } 359 }
350 }) 360 })
351 361
362 it('Should correctly detect if quick transcode is possible', async function () {
363 this.timeout(10000)
364
365 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
366 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
367 })
368
369 it('Should merge an audio file with the preview file', async function () {
370 this.timeout(60000)
371
372 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
373 await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributesArg)
374
375 await waitJobs(servers)
376
377 for (const server of servers) {
378 const res = await getVideosList(server.url)
379
380 const video = res.body.data.find(v => v.name === 'audio_with_preview')
381 const res2 = await getVideo(server.url, video.id)
382 const videoDetails: VideoDetails = res2.body
383
384 expect(videoDetails.files).to.have.lengthOf(1)
385
386 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
387 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
388
389 const magnetUri = videoDetails.files[ 0 ].magnetUri
390 expect(magnetUri).to.contain('.mp4')
391 }
392 })
393
394 it('Should upload an audio file and choose a default background image', async function () {
395 this.timeout(60000)
396
397 const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
398 await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributesArg)
399
400 await waitJobs(servers)
401
402 for (const server of servers) {
403 const res = await getVideosList(server.url)
404
405 const video = res.body.data.find(v => v.name === 'audio_without_preview')
406 const res2 = await getVideo(server.url, video.id)
407 const videoDetails = res2.body
408
409 expect(videoDetails.files).to.have.lengthOf(1)
410
411 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
412 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
413
414 const magnetUri = videoDetails.files[ 0 ].magnetUri
415 expect(magnetUri).to.contain('.mp4')
416 }
417 })
418
352 after(async function () { 419 after(async function () {
353 await cleanupTests(servers) 420 await cleanupTests(servers)
354 }) 421 })
diff --git a/server/tests/api/videos/videos-views-cleaner.ts b/server/tests/api/videos/videos-views-cleaner.ts
index c21d46d56..fbddd40f4 100644
--- a/server/tests/api/videos/videos-views-cleaner.ts
+++ b/server/tests/api/videos/videos-views-cleaner.ts
@@ -10,7 +10,7 @@ import {
10 flushAndRunServer, 10 flushAndRunServer,
11 ServerInfo, 11 ServerInfo,
12 setAccessTokensToServers, 12 setAccessTokensToServers,
13 uploadVideo, uploadVideoAndGetId, viewVideo, wait, countVideoViewsOf, doubleFollow, waitJobs, cleanupTests 13 uploadVideo, uploadVideoAndGetId, viewVideo, wait, countVideoViewsOf, doubleFollow, waitJobs, cleanupTests, closeAllSequelize
14} from '../../../../shared/extra-utils' 14} from '../../../../shared/extra-utils'
15import { getVideosOverview } from '../../../../shared/extra-utils/overviews/overviews' 15import { getVideosOverview } from '../../../../shared/extra-utils/overviews/overviews'
16import { VideosOverview } from '../../../../shared/models/overviews' 16import { VideosOverview } from '../../../../shared/models/overviews'
@@ -58,14 +58,14 @@ describe('Test video views cleaner', function () {
58 58
59 { 59 {
60 for (const server of servers) { 60 for (const server of servers) {
61 const total = await countVideoViewsOf(server.serverNumber, videoIdServer1) 61 const total = await countVideoViewsOf(server.internalServerNumber, videoIdServer1)
62 expect(total).to.equal(2) 62 expect(total).to.equal(2)
63 } 63 }
64 } 64 }
65 65
66 { 66 {
67 for (const server of servers) { 67 for (const server of servers) {
68 const total = await countVideoViewsOf(server.serverNumber, videoIdServer2) 68 const total = await countVideoViewsOf(server.internalServerNumber, videoIdServer2)
69 expect(total).to.equal(2) 69 expect(total).to.equal(2)
70 } 70 }
71 } 71 }
@@ -74,8 +74,6 @@ describe('Test video views cleaner', function () {
74 it('Should clean old video views', async function () { 74 it('Should clean old video views', async function () {
75 this.timeout(50000) 75 this.timeout(50000)
76 76
77 this.timeout(50000)
78
79 killallServers([ servers[0] ]) 77 killallServers([ servers[0] ])
80 78
81 await reRunServer(servers[0], { views: { videos: { remote: { max_age: '5 seconds' } } } }) 79 await reRunServer(servers[0], { views: { videos: { remote: { max_age: '5 seconds' } } } })
@@ -86,21 +84,23 @@ describe('Test video views cleaner', function () {
86 84
87 { 85 {
88 for (const server of servers) { 86 for (const server of servers) {
89 const total = await countVideoViewsOf(server.serverNumber, videoIdServer1) 87 const total = await countVideoViewsOf(server.internalServerNumber, videoIdServer1)
90 expect(total).to.equal(2) 88 expect(total).to.equal(2)
91 } 89 }
92 } 90 }
93 91
94 { 92 {
95 const totalServer1 = await countVideoViewsOf(servers[0].serverNumber, videoIdServer2) 93 const totalServer1 = await countVideoViewsOf(servers[0].internalServerNumber, videoIdServer2)
96 expect(totalServer1).to.equal(0) 94 expect(totalServer1).to.equal(0)
97 95
98 const totalServer2 = await countVideoViewsOf(servers[1].serverNumber, videoIdServer2) 96 const totalServer2 = await countVideoViewsOf(servers[1].internalServerNumber, videoIdServer2)
99 expect(totalServer2).to.equal(2) 97 expect(totalServer2).to.equal(2)
100 } 98 }
101 }) 99 })
102 100
103 after(async function () { 101 after(async function () {
102 await closeAllSequelize(servers)
103
104 await cleanupTests(servers) 104 await cleanupTests(servers)
105 }) 105 })
106}) 106})
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts
index 5e12c0089..3822fca42 100644
--- a/server/tests/cli/optimize-old-videos.ts
+++ b/server/tests/cli/optimize-old-videos.ts
@@ -8,14 +8,16 @@ import {
8 doubleFollow, 8 doubleFollow,
9 execCLI, 9 execCLI,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 flushTests, generateHighBitrateVideo, 11 generateHighBitrateVideo,
12 getEnvCli, 12 getEnvCli,
13 getVideo, 13 getVideo,
14 getVideosList, 14 getVideosList,
15 killallServers, root, 15 root,
16 ServerInfo, 16 ServerInfo,
17 setAccessTokensToServers, 17 setAccessTokensToServers,
18 uploadVideo, viewVideo, wait 18 uploadVideo,
19 viewVideo,
20 wait
19} from '../../../shared/extra-utils' 21} from '../../../shared/extra-utils'
20import { waitJobs } from '../../../shared/extra-utils/server/jobs' 22import { waitJobs } from '../../../shared/extra-utils/server/jobs'
21import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' 23import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils'
@@ -102,7 +104,7 @@ describe('Test optimize old videos', function () {
102 104
103 expect(file.size).to.be.below(5000000) 105 expect(file.size).to.be.below(5000000)
104 106
105 const path = join(root(), 'test1', 'videos', video.uuid + '-' + file.resolution.id + '.mp4') 107 const path = join(root(), 'test' + servers[0].internalServerNumber, 'videos', video.uuid + '-' + file.resolution.id + '.mp4')
106 const bitrate = await getVideoFileBitrate(path) 108 const bitrate = await getVideoFileBitrate(path)
107 const fps = await getVideoFileFPS(path) 109 const fps = await getVideoFileFPS(path)
108 const resolution = await getVideoFileResolution(path) 110 const resolution = await getVideoFileResolution(path)
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
index 80bbc98d5..d73e27564 100644
--- a/server/tests/cli/peertube.ts
+++ b/server/tests/cli/peertube.ts
@@ -1,28 +1,46 @@
1/* tslint:disable:no-unused-expression */
2
1import 'mocha' 3import 'mocha'
4import { expect } from 'chai'
2import { 5import {
3 expect 6 addVideoChannel,
4} from 'chai' 7 buildAbsoluteFixturePath,
5import { 8 cleanupTests,
6 createUser, 9 createUser,
7 execCLI, 10 execCLI,
8 flushTests,
9 getEnvCli,
10 killallServers,
11 flushAndRunServer, 11 flushAndRunServer,
12 getEnvCli,
13 getVideo,
14 getVideosList,
15 getVideosListWithToken, removeVideo,
12 ServerInfo, 16 ServerInfo,
13 setAccessTokensToServers, cleanupTests 17 setAccessTokensToServers,
18 userLogin,
19 waitJobs
14} from '../../../shared/extra-utils' 20} from '../../../shared/extra-utils'
21import { Video, VideoDetails } from '../../../shared'
22import { getYoutubeVideoUrl } from '../../../shared/extra-utils/videos/video-imports'
15 23
16describe('Test CLI wrapper', function () { 24describe('Test CLI wrapper', function () {
17 let server: ServerInfo 25 let server: ServerInfo
26 let userAccessToken: string
27
18 const cmd = 'node ./dist/server/tools/peertube.js' 28 const cmd = 'node ./dist/server/tools/peertube.js'
19 29
20 before(async function () { 30 before(async function () {
21 this.timeout(30000) 31 this.timeout(30000)
32
22 server = await flushAndRunServer(1) 33 server = await flushAndRunServer(1)
23 await setAccessTokensToServers([ server ]) 34 await setAccessTokensToServers([ server ])
24 35
25 await createUser({ url: server.url, accessToken: server.accessToken, username: 'user_1', password: 'super password' }) 36 await createUser({ url: server.url, accessToken: server.accessToken, username: 'user_1', password: 'super_password' })
37
38 userAccessToken = await userLogin(server, { username: 'user_1', password: 'super_password' })
39
40 {
41 const args = { name: 'user_channel', displayName: 'User channel', support: 'super support text' }
42 await addVideoChannel(server.url, userAccessToken, args)
43 }
26 }) 44 })
27 45
28 it('Should display no selected instance', async function () { 46 it('Should display no selected instance', async function () {
@@ -31,21 +49,132 @@ describe('Test CLI wrapper', function () {
31 const env = getEnvCli(server) 49 const env = getEnvCli(server)
32 const stdout = await execCLI(`${env} ${cmd} --help`) 50 const stdout = await execCLI(`${env} ${cmd} --help`)
33 51
34 expect(stdout).to.contain('selected') 52 expect(stdout).to.contain('no instance selected')
53 })
54
55 it('Should add a user', async function () {
56 this.timeout(60000)
57
58 const env = getEnvCli(server)
59 await execCLI(`${env} ${cmd} auth add -u ${server.url} -U user_1 -p super_password`)
60 })
61
62 it('Should default to this user', async function () {
63 this.timeout(60000)
64
65 const env = getEnvCli(server)
66 const stdout = await execCLI(`${env} ${cmd} --help`)
67
68 expect(stdout).to.contain(`instance ${server.url} selected`)
69 })
70
71 it('Should remember the user', async function () {
72 this.timeout(60000)
73
74 const env = getEnvCli(server)
75 const stdout = await execCLI(`${env} ${cmd} auth list`)
76
77 expect(stdout).to.contain(server.url)
78 })
79
80 it('Should upload a video', async function () {
81 this.timeout(60000)
82
83 const env = getEnvCli(server)
84
85 const fixture = buildAbsoluteFixturePath('60fps_720p_small.mp4')
86
87 const params = `-f ${fixture} --video-name 'test upload' --channel-name user_channel --support 'support_text'`
88
89 await execCLI(`${env} ${cmd} upload ${params}`)
90 })
91
92 it('Should have the video uploaded', async function () {
93 const res = await getVideosList(server.url)
94
95 expect(res.body.total).to.equal(1)
96
97 const videos: Video[] = res.body.data
98
99 const video: VideoDetails = (await getVideo(server.url, videos[0].uuid)).body
100
101 expect(video.name).to.equal('test upload')
102 expect(video.support).to.equal('support_text')
103 expect(video.channel.name).to.equal('user_channel')
35 }) 104 })
36 105
37 it('Should remember the authentifying material of the user', async function () { 106 it('Should import a video', async function () {
38 this.timeout(60000) 107 this.timeout(60000)
39 108
40 const env = getEnvCli(server) 109 const env = getEnvCli(server)
41 await execCLI(`${env} ` + cmd + ` auth add --url ${server.url} -U user_1 -p "super password"`) 110
111 const params = `--target-url ${getYoutubeVideoUrl()} --channel-name user_channel`
112
113 await execCLI(`${env} ${cmd} import ${params}`)
114 })
115
116 it('Should have imported the video', async function () {
117 this.timeout(60000)
118
119 await waitJobs([ server ])
120
121 const res = await getVideosList(server.url)
122
123 expect(res.body.total).to.equal(2)
124
125 const videos: Video[] = res.body.data
126 const video = videos.find(v => v.name === 'small video - youtube')
127 expect(video).to.not.be.undefined
128
129 const videoDetails: VideoDetails = (await getVideo(server.url, video.id)).body
130 expect(videoDetails.channel.name).to.equal('user_channel')
131 expect(videoDetails.support).to.equal('super support text')
132 expect(videoDetails.nsfw).to.be.false
133
134 // So we can reimport it
135 await removeVideo(server.url, userAccessToken, video.id)
136 })
137
138 it('Should import and override some imported attributes', async function () {
139 this.timeout(60000)
140
141 const env = getEnvCli(server)
142
143 const params = `--target-url ${getYoutubeVideoUrl()} --channel-name user_channel --video-name toto --nsfw --support support`
144
145 await execCLI(`${env} ${cmd} import ${params}`)
146
147 await waitJobs([ server ])
148
149 {
150 const res = await getVideosList(server.url)
151 expect(res.body.total).to.equal(2)
152
153 const videos: Video[] = res.body.data
154 const video = videos.find(v => v.name === 'toto')
155 expect(video).to.not.be.undefined
156
157 const videoDetails: VideoDetails = (await getVideo(server.url, video.id)).body
158 expect(videoDetails.channel.name).to.equal('user_channel')
159 expect(videoDetails.support).to.equal('support')
160 expect(videoDetails.nsfw).to.be.true
161 expect(videoDetails.commentsEnabled).to.be.true
162 }
163 })
164
165 it('Should remove the auth user', async function () {
166 const env = getEnvCli(server)
167
168 await execCLI(`${env} ${cmd} auth del ${server.url}`)
169
170 const stdout = await execCLI(`${env} ${cmd} --help`)
171
172 expect(stdout).to.contain('no instance selected')
42 }) 173 })
43 174
44 after(async function () { 175 after(async function () {
45 this.timeout(10000) 176 this.timeout(10000)
46 177
47 await execCLI(cmd + ` auth del ${server.url}`)
48
49 await cleanupTests([ server ]) 178 await cleanupTests([ server ])
50 }) 179 })
51}) 180})
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts
index 0dcdf09cf..437470327 100644
--- a/server/tests/feeds/feeds.ts
+++ b/server/tests/feeds/feeds.ts
@@ -7,13 +7,13 @@ import {
7 createUser, 7 createUser,
8 doubleFollow, 8 doubleFollow,
9 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
10 flushTests, 10 getJSONfeed,
11 getJSONfeed, getMyUserInformation, 11 getMyUserInformation,
12 getXMLfeed, 12 getXMLfeed,
13 killallServers,
14 ServerInfo, 13 ServerInfo,
15 setAccessTokensToServers, 14 setAccessTokensToServers,
16 uploadVideo, userLogin 15 uploadVideo,
16 userLogin
17} from '../../../shared/extra-utils' 17} from '../../../shared/extra-utils'
18import * as libxmljs from 'libxmljs' 18import * as libxmljs from 'libxmljs'
19import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments' 19import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
@@ -28,10 +28,10 @@ const expect = chai.expect
28describe('Test syndication feeds', () => { 28describe('Test syndication feeds', () => {
29 let servers: ServerInfo[] = [] 29 let servers: ServerInfo[] = []
30 let userAccessToken: string 30 let userAccessToken: string
31 let rootAccountUUID: string 31 let rootAccountId: number
32 let rootChannelUUID: string 32 let rootChannelId: number
33 let userAccountUUID: string 33 let userAccountId: number
34 let userChannelUUID: string 34 let userChannelId: number
35 35
36 before(async function () { 36 before(async function () {
37 this.timeout(120000) 37 this.timeout(120000)
@@ -45,8 +45,8 @@ describe('Test syndication feeds', () => {
45 { 45 {
46 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) 46 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
47 const user: User = res.body 47 const user: User = res.body
48 rootAccountUUID = user.account.uuid 48 rootAccountId = user.account.id
49 rootChannelUUID = user.videoChannels[0].uuid 49 rootChannelId = user.videoChannels[0].id
50 } 50 }
51 51
52 { 52 {
@@ -56,8 +56,8 @@ describe('Test syndication feeds', () => {
56 56
57 const res = await getMyUserInformation(servers[0].url, userAccessToken) 57 const res = await getMyUserInformation(servers[0].url, userAccessToken)
58 const user: User = res.body 58 const user: User = res.body
59 userAccountUUID = user.account.uuid 59 userAccountId = user.account.id
60 userChannelUUID = user.videoChannels[0].uuid 60 userChannelId = user.videoChannels[0].id
61 } 61 }
62 62
63 { 63 {
@@ -127,71 +127,71 @@ describe('Test syndication feeds', () => {
127 }) 127 })
128 128
129 it('Should filter by account', async function () { 129 it('Should filter by account', async function () {
130 {
131 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: rootAccountId })
132 const jsonObj = JSON.parse(json.text)
133 expect(jsonObj.items.length).to.be.equal(1)
134 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1')
135 expect(jsonObj.items[ 0 ].author.name).to.equal('root')
136 }
137
138 {
139 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId })
140 const jsonObj = JSON.parse(json.text)
141 expect(jsonObj.items.length).to.be.equal(1)
142 expect(jsonObj.items[ 0 ].title).to.equal('user video')
143 expect(jsonObj.items[ 0 ].author.name).to.equal('john')
144 }
145
130 for (const server of servers) { 146 for (const server of servers) {
131 { 147 {
132 const json = await getJSONfeed(server.url, 'videos', { accountId: rootAccountUUID }) 148 const json = await getJSONfeed(server.url, 'videos', { accountName: 'root@localhost:' + servers[0].port })
133 const jsonObj = JSON.parse(json.text) 149 const jsonObj = JSON.parse(json.text)
134 expect(jsonObj.items.length).to.be.equal(1) 150 expect(jsonObj.items.length).to.be.equal(1)
135 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1') 151 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1')
136 expect(jsonObj.items[ 0 ].author.name).to.equal('root')
137 } 152 }
138 153
139 { 154 {
140 const json = await getJSONfeed(server.url, 'videos', { accountId: userAccountUUID }) 155 const json = await getJSONfeed(server.url, 'videos', { accountName: 'john@localhost:' + servers[0].port })
141 const jsonObj = JSON.parse(json.text) 156 const jsonObj = JSON.parse(json.text)
142 expect(jsonObj.items.length).to.be.equal(1) 157 expect(jsonObj.items.length).to.be.equal(1)
143 expect(jsonObj.items[ 0 ].title).to.equal('user video') 158 expect(jsonObj.items[ 0 ].title).to.equal('user video')
144 expect(jsonObj.items[ 0 ].author.name).to.equal('john')
145 } 159 }
146 } 160 }
161 })
147 162
163 it('Should filter by video channel', async function () {
148 { 164 {
149 const json = await getJSONfeed(servers[0].url, 'videos', { accountName: 'root' }) 165 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: rootChannelId })
150 const jsonObj = JSON.parse(json.text) 166 const jsonObj = JSON.parse(json.text)
151 expect(jsonObj.items.length).to.be.equal(1) 167 expect(jsonObj.items.length).to.be.equal(1)
152 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1') 168 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1')
169 expect(jsonObj.items[ 0 ].author.name).to.equal('root')
153 } 170 }
154 171
155 { 172 {
156 const json = await getJSONfeed(servers[0].url, 'videos', { accountName: 'john' }) 173 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: userChannelId })
157 const jsonObj = JSON.parse(json.text) 174 const jsonObj = JSON.parse(json.text)
158 expect(jsonObj.items.length).to.be.equal(1) 175 expect(jsonObj.items.length).to.be.equal(1)
159 expect(jsonObj.items[ 0 ].title).to.equal('user video') 176 expect(jsonObj.items[ 0 ].title).to.equal('user video')
177 expect(jsonObj.items[ 0 ].author.name).to.equal('john')
160 } 178 }
161 })
162 179
163 it('Should filter by video channel', async function () {
164 for (const server of servers) { 180 for (const server of servers) {
165 { 181 {
166 const json = await getJSONfeed(server.url, 'videos', { videoChannelId: rootChannelUUID }) 182 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'root_channel@localhost:' + servers[0].port })
167 const jsonObj = JSON.parse(json.text) 183 const jsonObj = JSON.parse(json.text)
168 expect(jsonObj.items.length).to.be.equal(1) 184 expect(jsonObj.items.length).to.be.equal(1)
169 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1') 185 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1')
170 expect(jsonObj.items[ 0 ].author.name).to.equal('root')
171 } 186 }
172 187
173 { 188 {
174 const json = await getJSONfeed(server.url, 'videos', { videoChannelId: userChannelUUID }) 189 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'john_channel@localhost:' + servers[0].port })
175 const jsonObj = JSON.parse(json.text) 190 const jsonObj = JSON.parse(json.text)
176 expect(jsonObj.items.length).to.be.equal(1) 191 expect(jsonObj.items.length).to.be.equal(1)
177 expect(jsonObj.items[ 0 ].title).to.equal('user video') 192 expect(jsonObj.items[ 0 ].title).to.equal('user video')
178 expect(jsonObj.items[ 0 ].author.name).to.equal('john')
179 } 193 }
180 } 194 }
181
182 {
183 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelName: 'root_channel' })
184 const jsonObj = JSON.parse(json.text)
185 expect(jsonObj.items.length).to.be.equal(1)
186 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1')
187 }
188
189 {
190 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelName: 'john_channel' })
191 const jsonObj = JSON.parse(json.text)
192 expect(jsonObj.items.length).to.be.equal(1)
193 expect(jsonObj.items[ 0 ].title).to.equal('user video')
194 }
195 }) 195 })
196 }) 196 })
197 197
diff --git a/server/tests/fixtures/preview.jpg b/server/tests/fixtures/preview.jpg
index c40ece838..cb5692281 100644
--- a/server/tests/fixtures/preview.jpg
+++ b/server/tests/fixtures/preview.jpg
Binary files differ
diff --git a/server/tests/fixtures/sample.ogg b/server/tests/fixtures/sample.ogg
new file mode 100644
index 000000000..0d7f43eb7
--- /dev/null
+++ b/server/tests/fixtures/sample.ogg
Binary files differ
diff --git a/server/tests/fixtures/video_short1-preview.webm.jpg b/server/tests/fixtures/video_short1-preview.webm.jpg
index d2a068b78..157d3ca9a 100644
--- a/server/tests/fixtures/video_short1-preview.webm.jpg
+++ b/server/tests/fixtures/video_short1-preview.webm.jpg
Binary files differ
diff --git a/server/tools/cli.ts b/server/tools/cli.ts
index 59e9fcfc4..2eec51aa4 100644
--- a/server/tools/cli.ts
+++ b/server/tools/cli.ts
@@ -1,5 +1,14 @@
1const config = require('application-config')('PeerTube/CLI') 1import { Netrc } from 'netrc-parser'
2const netrc = require('netrc-parser').default 2import { getAppNumber, isTestInstance } from '../helpers/core-utils'
3import { join } from 'path'
4import { getVideoChannel, root } from '../../shared/extra-utils'
5import { Command } from 'commander'
6import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
7
8let configName = 'PeerTube/CLI'
9if (isTestInstance()) configName += `-${getAppNumber()}`
10
11const config = require('application-config')(configName)
3 12
4const version = require('../../../package.json').version 13const version = require('../../../package.json').version
5 14
@@ -12,7 +21,7 @@ function getSettings () {
12 return new Promise<Settings>((res, rej) => { 21 return new Promise<Settings>((res, rej) => {
13 const defaultSettings = { 22 const defaultSettings = {
14 remotes: [], 23 remotes: [],
15 default: 0 24 default: -1
16 } 25 }
17 26
18 config.read((err, data) => { 27 config.read((err, data) => {
@@ -24,6 +33,12 @@ function getSettings () {
24} 33}
25 34
26async function getNetrc () { 35async function getNetrc () {
36 const Netrc = require('netrc-parser').Netrc
37
38 const netrc = isTestInstance()
39 ? new Netrc(join(root(), 'test' + getAppNumber(), 'netrc'))
40 : new Netrc()
41
27 await netrc.load() 42 await netrc.load()
28 43
29 return netrc 44 return netrc
@@ -31,7 +46,17 @@ async function getNetrc () {
31 46
32function writeSettings (settings) { 47function writeSettings (settings) {
33 return new Promise((res, rej) => { 48 return new Promise((res, rej) => {
34 config.write(settings, function (err) { 49 config.write(settings, err => {
50 if (err) return rej(err)
51
52 return res()
53 })
54 })
55}
56
57function deleteSettings () {
58 return new Promise((res, rej) => {
59 config.trash((err) => {
35 if (err) return rej(err) 60 if (err) return rej(err)
36 61
37 return res() 62 return res()
@@ -39,10 +64,10 @@ function writeSettings (settings) {
39 }) 64 })
40} 65}
41 66
42function getRemoteObjectOrDie (program: any, settings: Settings) { 67function getRemoteObjectOrDie (program: any, settings: Settings, netrc: Netrc) {
43 if (!program['url'] || !program['username'] || !program['password']) { 68 if (!program['url'] || !program['username'] || !program['password']) {
44 // No remote and we don't have program parameters: throw 69 // No remote and we don't have program parameters: quit
45 if (settings.remotes.length === 0) { 70 if (settings.remotes.length === 0 || Object.keys(netrc.machines).length === 0) {
46 if (!program[ 'url' ]) console.error('--url field is required.') 71 if (!program[ 'url' ]) console.error('--url field is required.')
47 if (!program[ 'username' ]) console.error('--username field is required.') 72 if (!program[ 'username' ]) console.error('--username field is required.')
48 if (!program[ 'password' ]) console.error('--password field is required.') 73 if (!program[ 'password' ]) console.error('--password field is required.')
@@ -54,14 +79,12 @@ function getRemoteObjectOrDie (program: any, settings: Settings) {
54 let username: string = program['username'] 79 let username: string = program['username']
55 let password: string = program['password'] 80 let password: string = program['password']
56 81
57 if (!url) { 82 if (!url && settings.default !== -1) url = settings.remotes[settings.default]
58 url = settings.default !== -1 83
59 ? settings.remotes[settings.default] 84 const machine = netrc.machines[url]
60 : settings.remotes[0]
61 }
62 85
63 if (!username) username = netrc.machines[url].login 86 if (!username && machine) username = machine.login
64 if (!password) password = netrc.machines[url].password 87 if (!password && machine) password = machine.password
65 88
66 return { url, username, password } 89 return { url, username, password }
67 } 90 }
@@ -73,6 +96,71 @@ function getRemoteObjectOrDie (program: any, settings: Settings) {
73 } 96 }
74} 97}
75 98
99function buildCommonVideoOptions (command: Command) {
100 function list (val) {
101 return val.split(',')
102 }
103
104 return command
105 .option('-n, --video-name <name>', 'Video name')
106 .option('-c, --category <category_number>', 'Category number')
107 .option('-l, --licence <licence_number>', 'Licence number')
108 .option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)')
109 .option('-t, --tags <tags>', 'Video tags', list)
110 .option('-N, --nsfw', 'Video is Not Safe For Work')
111 .option('-d, --video-description <description>', 'Video description')
112 .option('-P, --privacy <privacy_number>', 'Privacy')
113 .option('-C, --channel-name <channel_name>', 'Channel name')
114 .option('-m, --comments-enabled', 'Enable comments')
115 .option('-s, --support <support>', 'Video support text')
116 .option('-w, --wait-transcoding', 'Wait transcoding before publishing the video')
117}
118
119async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any) {
120 const defaultBooleanAttributes = {
121 nsfw: false,
122 commentsEnabled: true,
123 downloadEnabled: true,
124 waitTranscoding: true
125 }
126
127 const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {}
128
129 for (const key of Object.keys(defaultBooleanAttributes)) {
130 if (command[ key ] !== undefined) {
131 booleanAttributes[key] = command[ key ]
132 } else if (defaultAttributes[key] !== undefined) {
133 booleanAttributes[key] = defaultAttributes[key]
134 } else {
135 booleanAttributes[key] = defaultBooleanAttributes[key]
136 }
137 }
138
139 const videoAttributes = {
140 name: command[ 'videoName' ] || defaultAttributes.name,
141 category: command[ 'category' ] || defaultAttributes.category || undefined,
142 licence: command[ 'licence' ] || defaultAttributes.licence || undefined,
143 language: command[ 'language' ] || defaultAttributes.language || undefined,
144 privacy: command[ 'privacy' ] || defaultAttributes.privacy || VideoPrivacy.PUBLIC,
145 support: command[ 'support' ] || defaultAttributes.support || undefined
146 }
147
148 Object.assign(videoAttributes, booleanAttributes)
149
150 if (command[ 'channelName' ]) {
151 const res = await getVideoChannel(url, command['channelName'])
152 const videoChannel: VideoChannel = res.body
153
154 Object.assign(videoAttributes, { channelId: videoChannel.id })
155
156 if (!videoAttributes.support && videoChannel.support) {
157 Object.assign(videoAttributes, { support: videoChannel.support })
158 }
159 }
160
161 return videoAttributes
162}
163
76// --------------------------------------------------------------------------- 164// ---------------------------------------------------------------------------
77 165
78export { 166export {
@@ -81,5 +169,9 @@ export {
81 getSettings, 169 getSettings,
82 getNetrc, 170 getNetrc,
83 getRemoteObjectOrDie, 171 getRemoteObjectOrDie,
84 writeSettings 172 writeSettings,
173 deleteSettings,
174
175 buildCommonVideoOptions,
176 buildVideoAttributesFromCommander
85} 177}
diff --git a/server/tools/package.json b/server/tools/package.json
new file mode 100644
index 000000000..22fb8d24c
--- /dev/null
+++ b/server/tools/package.json
@@ -0,0 +1,14 @@
1{
2 "name": "@peertube/cli",
3 "version": "1.0.0",
4 "private": true,
5 "dependencies": {
6 "application-config": "^1.0.1",
7 "cli-table": "^0.3.1",
8 "netrc-parser": "^3.1.6",
9 "webtorrent-hybrid": "^2.1.0"
10 },
11 "summon": {
12 "silent": true
13 }
14}
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts
index 8bc3d332c..ff5ffb60e 100644
--- a/server/tools/peertube-auth.ts
+++ b/server/tools/peertube-auth.ts
@@ -9,7 +9,11 @@ const Table = require('cli-table')
9async function delInstance (url: string) { 9async function delInstance (url: string) {
10 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) 10 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
11 11
12 settings.remotes.splice(settings.remotes.indexOf(url)) 12 const index = settings.remotes.indexOf(url)
13 settings.remotes.splice(index)
14
15 if (settings.default === index) settings.default = -1
16
13 await writeSettings(settings) 17 await writeSettings(settings)
14 18
15 delete netrc.machines[url] 19 delete netrc.machines[url]
@@ -17,12 +21,17 @@ async function delInstance (url: string) {
17 await netrc.save() 21 await netrc.save()
18} 22}
19 23
20async function setInstance (url: string, username: string, password: string) { 24async function setInstance (url: string, username: string, password: string, isDefault: boolean) {
21 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) 25 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
22 26
23 if (settings.remotes.indexOf(url) === -1) { 27 if (settings.remotes.indexOf(url) === -1) {
24 settings.remotes.push(url) 28 settings.remotes.push(url)
25 } 29 }
30
31 if (isDefault || settings.remotes.length === 1) {
32 settings.default = settings.remotes.length - 1
33 }
34
26 await writeSettings(settings) 35 await writeSettings(settings)
27 36
28 netrc.machines[url] = { login: username, password } 37 netrc.machines[url] = { login: username, password }
@@ -66,7 +75,7 @@ program
66 } 75 }
67 } 76 }
68 }, async (_, result) => { 77 }, async (_, result) => {
69 await setInstance(result.url, result.username, result.password) 78 await setInstance(result.url, result.username, result.password, program['default'])
70 79
71 process.exit(0) 80 process.exit(0)
72 }) 81 })
@@ -93,6 +102,8 @@ program
93 }) 102 })
94 103
95 settings.remotes.forEach(element => { 104 settings.remotes.forEach(element => {
105 if (!netrc.machines[element]) return
106
96 table.push([ 107 table.push([
97 element, 108 element,
98 netrc.machines[element].login 109 netrc.machines[element].login
diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts
index 85660de2c..103495347 100644
--- a/server/tools/peertube-get-access-token.ts
+++ b/server/tools/peertube-get-access-token.ts
@@ -1,12 +1,5 @@
1import * as program from 'commander' 1import * as program from 'commander'
2 2import { getClient, Server, serverLogin } from '../../shared/extra-utils'
3import {
4 getClient,
5 serverLogin,
6 Server,
7 Client,
8 User
9} from '../../shared/extra-utils'
10 3
11program 4program
12 .option('-u, --url <url>', 'Server url') 5 .option('-u, --url <url>', 'Server url')
@@ -22,6 +15,7 @@ if (
22 if (!program['url']) console.error('--url field is required.') 15 if (!program['url']) console.error('--url field is required.')
23 if (!program['username']) console.error('--username field is required.') 16 if (!program['username']) console.error('--username field is required.')
24 if (!program['password']) console.error('--password field is required.') 17 if (!program['password']) console.error('--password field is required.')
18
25 process.exit(-1) 19 process.exit(-1)
26} 20}
27 21
@@ -32,11 +26,11 @@ getClient(program.url)
32 user: { 26 user: {
33 username: program['username'], 27 username: program['username'],
34 password: program['password'] 28 password: program['password']
35 } as User, 29 },
36 client: { 30 client: {
37 id: res.body.client_id as string, 31 id: res.body.client_id,
38 secret: res.body.client_secret as string 32 secret: res.body.client_secret
39 } as Client 33 }
40 } as Server 34 } as Server
41 35
42 return serverLogin(server) 36 return serverLogin(server)
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index 9a366dbbd..d7bb00e02 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -3,7 +3,6 @@ require('tls').DEFAULT_ECDH_CURVE = 'auto'
3 3
4import * as program from 'commander' 4import * as program from 'commander'
5import { join } from 'path' 5import { join } from 'path'
6import { VideoPrivacy } from '../../shared/models/videos'
7import { doRequestAndSaveToFile } from '../helpers/requests' 6import { doRequestAndSaveToFile } from '../helpers/requests'
8import { CONSTRAINTS_FIELDS } from '../initializers/constants' 7import { CONSTRAINTS_FIELDS } from '../initializers/constants'
9import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index' 8import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index'
@@ -12,29 +11,34 @@ import * as prompt from 'prompt'
12import { remove } from 'fs-extra' 11import { remove } from 'fs-extra'
13import { sha256 } from '../helpers/core-utils' 12import { sha256 } from '../helpers/core-utils'
14import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' 13import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
15import { getNetrc, getRemoteObjectOrDie, getSettings } from './cli' 14import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getNetrc, getRemoteObjectOrDie, getSettings } from './cli'
16 15
17let accessToken: string 16type UserInfo = {
18let client: { id: string, secret: string } 17 username: string
18 password: string
19}
19 20
20const processOptions = { 21const processOptions = {
21 cwd: __dirname, 22 cwd: __dirname,
22 maxBuffer: Infinity 23 maxBuffer: Infinity
23} 24}
24 25
25program 26let command = program
26 .name('import-videos') 27 .name('import-videos')
28
29command = buildCommonVideoOptions(command)
30
31command
27 .option('-u, --url <url>', 'Server url') 32 .option('-u, --url <url>', 'Server url')
28 .option('-U, --username <username>', 'Username') 33 .option('-U, --username <username>', 'Username')
29 .option('-p, --password <token>', 'Password') 34 .option('-p, --password <token>', 'Password')
30 .option('-t, --target-url <targetUrl>', 'Video target URL') 35 .option('-t, --target-url <targetUrl>', 'Video target URL')
31 .option('-l, --language <languageCode>', 'Language ISO 639 code (fr or en...)')
32 .option('-v, --verbose', 'Verbose mode') 36 .option('-v, --verbose', 'Verbose mode')
33 .parse(process.argv) 37 .parse(process.argv)
34 38
35Promise.all([ getSettings(), getNetrc() ]) 39Promise.all([ getSettings(), getNetrc() ])
36 .then(([ settings, netrc ]) => { 40 .then(([ settings, netrc ]) => {
37 const { url, username, password } = getRemoteObjectOrDie(program, settings) 41 const { url, username, password } = getRemoteObjectOrDie(program, settings, netrc)
38 42
39 if (!program[ 'targetUrl' ]) { 43 if (!program[ 'targetUrl' ]) {
40 console.error('--targetUrl field is required.') 44 console.error('--targetUrl field is required.')
@@ -45,56 +49,20 @@ Promise.all([ getSettings(), getNetrc() ])
45 removeEndSlashes(url) 49 removeEndSlashes(url)
46 removeEndSlashes(program[ 'targetUrl' ]) 50 removeEndSlashes(program[ 'targetUrl' ])
47 51
48 const user = { 52 const user = { username, password }
49 username: username,
50 password: password
51 }
52 53
53 run(user, url) 54 run(url, user)
54 .catch(err => { 55 .catch(err => {
55 console.error(err) 56 console.error(err)
56 process.exit(-1) 57 process.exit(-1)
57 }) 58 })
58 }) 59 })
59 60
60async function promptPassword () { 61async function run (url: string, user: UserInfo) {
61 return new Promise((res, rej) => {
62 prompt.start()
63 const schema = {
64 properties: {
65 password: {
66 hidden: true,
67 required: true
68 }
69 }
70 }
71 prompt.get(schema, function (err, result) {
72 if (err) {
73 return rej(err)
74 }
75 return res(result.password)
76 })
77 })
78}
79
80async function run (user, url: string) {
81 if (!user.password) { 62 if (!user.password) {
82 user.password = await promptPassword() 63 user.password = await promptPassword()
83 } 64 }
84 65
85 const res = await getClient(url)
86 client = {
87 id: res.body.client_id,
88 secret: res.body.client_secret
89 }
90
91 try {
92 const res = await login(program[ 'url' ], client, user)
93 accessToken = res.body.access_token
94 } catch (err) {
95 throw new Error('Cannot authenticate. Please check your username/password.')
96 }
97
98 const youtubeDL = await safeGetYoutubeDL() 66 const youtubeDL = await safeGetYoutubeDL()
99 67
100 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] 68 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
@@ -115,7 +83,12 @@ async function run (user, url: string) {
115 console.log('Will download and upload %d videos.\n', infoArray.length) 83 console.log('Will download and upload %d videos.\n', infoArray.length)
116 84
117 for (const info of infoArray) { 85 for (const info of infoArray) {
118 await processVideo(info, program[ 'language' ], processOptions.cwd, url, user) 86 await processVideo({
87 cwd: processOptions.cwd,
88 url,
89 user,
90 youtubeInfo: info
91 })
119 } 92 }
120 93
121 console.log('Video/s for user %s imported: %s', program[ 'username' ], program[ 'targetUrl' ]) 94 console.log('Video/s for user %s imported: %s', program[ 'username' ], program[ 'targetUrl' ])
@@ -123,11 +96,18 @@ async function run (user, url: string) {
123 }) 96 })
124} 97}
125 98
126function processVideo (info: any, languageCode: string, cwd: string, url: string, user) { 99function processVideo (parameters: {
100 cwd: string,
101 url: string,
102 user: { username: string, password: string },
103 youtubeInfo: any
104}) {
105 const { youtubeInfo, cwd, url, user } = parameters
106
127 return new Promise(async res => { 107 return new Promise(async res => {
128 if (program[ 'verbose' ]) console.log('Fetching object.', info) 108 if (program[ 'verbose' ]) console.log('Fetching object.', youtubeInfo)
129 109
130 const videoInfo = await fetchObject(info) 110 const videoInfo = await fetchObject(youtubeInfo)
131 if (program[ 'verbose' ]) console.log('Fetched object.', videoInfo) 111 if (program[ 'verbose' ]) console.log('Fetched object.', videoInfo)
132 112
133 const result = await searchVideoWithSort(url, videoInfo.title, '-match') 113 const result = await searchVideoWithSort(url, videoInfo.title, '-match')
@@ -153,7 +133,13 @@ function processVideo (info: any, languageCode: string, cwd: string, url: string
153 } 133 }
154 134
155 console.log(output.join('\n')) 135 console.log(output.join('\n'))
156 await uploadVideoOnPeerTube(normalizeObject(videoInfo), path, cwd, url, user, languageCode) 136 await uploadVideoOnPeerTube({
137 cwd,
138 url,
139 user,
140 videoInfo: normalizeObject(videoInfo),
141 videoPath: path
142 })
157 return res() 143 return res()
158 }) 144 })
159 } catch (err) { 145 } catch (err) {
@@ -163,7 +149,15 @@ function processVideo (info: any, languageCode: string, cwd: string, url: string
163 }) 149 })
164} 150}
165 151
166async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: string, url: string, user, language?: string) { 152async function uploadVideoOnPeerTube (parameters: {
153 videoInfo: any,
154 videoPath: string,
155 cwd: string,
156 url: string,
157 user: { username: string; password: string }
158}) {
159 const { videoInfo, videoPath, cwd, url, user } = parameters
160
167 const category = await getCategory(videoInfo.categories, url) 161 const category = await getCategory(videoInfo.categories, url)
168 const licence = getLicence(videoInfo.license) 162 const licence = getLicence(videoInfo.license)
169 let tags = [] 163 let tags = []
@@ -186,7 +180,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
186 180
187 const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo) 181 const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
188 182
189 const videoAttributes = { 183 const defaultAttributes = {
190 name: truncate(videoInfo.title, { 184 name: truncate(videoInfo.title, {
191 'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max, 185 'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
192 'separator': /,? +/, 186 'separator': /,? +/,
@@ -194,30 +188,31 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
194 }), 188 }),
195 category, 189 category,
196 licence, 190 licence,
197 language,
198 nsfw: isNSFW(videoInfo), 191 nsfw: isNSFW(videoInfo),
199 waitTranscoding: true, 192 description: videoInfo.description,
200 commentsEnabled: true, 193 tags
201 downloadEnabled: true, 194 }
202 description: videoInfo.description || undefined, 195
203 support: undefined, 196 const videoAttributes = await buildVideoAttributesFromCommander(url, program, defaultAttributes)
204 tags, 197
205 privacy: VideoPrivacy.PUBLIC, 198 Object.assign(videoAttributes, {
206 fixture: videoPath, 199 originallyPublishedAt: originallyPublishedAt ? originallyPublishedAt.toISOString() : null,
207 thumbnailfile, 200 thumbnailfile,
208 previewfile: thumbnailfile, 201 previewfile: thumbnailfile,
209 originallyPublishedAt: originallyPublishedAt ? originallyPublishedAt.toISOString() : null 202 fixture: videoPath
210 } 203 })
211 204
212 console.log('\nUploading on PeerTube video "%s".', videoAttributes.name) 205 console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
206
207 let accessToken = await getAccessTokenOrDie(url, user)
208
213 try { 209 try {
214 await uploadVideo(url, accessToken, videoAttributes) 210 await uploadVideo(url, accessToken, videoAttributes)
215 } catch (err) { 211 } catch (err) {
216 if (err.message.indexOf('401') !== -1) { 212 if (err.message.indexOf('401') !== -1) {
217 console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.') 213 console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.')
218 214
219 const res = await login(url, client, user) 215 accessToken = await getAccessTokenOrDie(url, user)
220 accessToken = res.body.access_token
221 216
222 await uploadVideo(url, accessToken, videoAttributes) 217 await uploadVideo(url, accessToken, videoAttributes)
223 } else { 218 } else {
@@ -232,6 +227,8 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
232 console.log('Uploaded video "%s"!\n', videoAttributes.name) 227 console.log('Uploaded video "%s"!\n', videoAttributes.name)
233} 228}
234 229
230/* ---------------------------------------------------------- */
231
235async function getCategory (categories: string[], url: string) { 232async function getCategory (categories: string[], url: string) {
236 if (!categories) return undefined 233 if (!categories) return undefined
237 234
@@ -250,8 +247,6 @@ async function getCategory (categories: string[], url: string) {
250 return undefined 247 return undefined
251} 248}
252 249
253/* ---------------------------------------------------------- */
254
255function getLicence (licence: string) { 250function getLicence (licence: string) {
256 if (!licence) return undefined 251 if (!licence) return undefined
257 252
@@ -305,9 +300,7 @@ function buildUrl (info: any) {
305} 300}
306 301
307function isNSFW (info: any) { 302function isNSFW (info: any) {
308 if (info.age_limit && info.age_limit >= 16) return true 303 return info.age_limit && info.age_limit >= 16
309
310 return false
311} 304}
312 305
313function removeEndSlashes (url: string) { 306function removeEndSlashes (url: string) {
@@ -315,3 +308,39 @@ function removeEndSlashes (url: string) {
315 url.slice(0, -1) 308 url.slice(0, -1)
316 } 309 }
317} 310}
311
312async function promptPassword () {
313 return new Promise<string>((res, rej) => {
314 prompt.start()
315 const schema = {
316 properties: {
317 password: {
318 hidden: true,
319 required: true
320 }
321 }
322 }
323 prompt.get(schema, function (err, result) {
324 if (err) {
325 return rej(err)
326 }
327 return res(result.password)
328 })
329 })
330}
331
332async function getAccessTokenOrDie (url: string, user: UserInfo) {
333 const resClient = await getClient(url)
334 const client = {
335 id: resClient.body.client_id,
336 secret: resClient.body.client_secret
337 }
338
339 try {
340 const res = await login(url, client, user)
341 return res.body.access_token
342 } catch (err) {
343 console.error('Cannot authenticate. Please check your username/password.')
344 process.exit(-1)
345 }
346}
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts
index 04d8b95a3..fbdec1613 100644
--- a/server/tools/peertube-repl.ts
+++ b/server/tools/peertube-repl.ts
@@ -43,7 +43,7 @@ const start = async () => {
43 Object.defineProperty(context, prop, { 43 Object.defineProperty(context, prop, {
44 configurable: false, 44 configurable: false,
45 enumerable: true, 45 enumerable: true,
46 value: properties[prop] 46 value: properties[ prop ]
47 }) 47 })
48 } 48 }
49 } 49 }
@@ -69,8 +69,7 @@ const start = async () => {
69 69
70} 70}
71 71
72start().then((data) => { 72start()
73 // do nothing 73 .catch((err) => {
74}).catch((err) => { 74 console.error(err)
75 console.error(err) 75 })
76})
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts
index 687f2e60b..c00205e8f 100644
--- a/server/tools/peertube-upload.ts
+++ b/server/tools/peertube-upload.ts
@@ -3,54 +3,47 @@ import { access, constants } from 'fs-extra'
3import { isAbsolute } from 'path' 3import { isAbsolute } from 'path'
4import { getClient, login } from '../../shared/extra-utils' 4import { getClient, login } from '../../shared/extra-utils'
5import { uploadVideo } from '../../shared/extra-utils/' 5import { uploadVideo } from '../../shared/extra-utils/'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getNetrc, getRemoteObjectOrDie, getSettings } from './cli'
7import { getRemoteObjectOrDie, getSettings } from './cli'
8 7
9program 8let command = program
10 .name('upload') 9 .name('upload')
10
11command = buildCommonVideoOptions(command)
12
13command
14
11 .option('-u, --url <url>', 'Server url') 15 .option('-u, --url <url>', 'Server url')
12 .option('-U, --username <username>', 'Username') 16 .option('-U, --username <username>', 'Username')
13 .option('-p, --password <token>', 'Password') 17 .option('-p, --password <token>', 'Password')
14 .option('-n, --video-name <name>', 'Video name')
15 .option('-P, --privacy <privacy_number>', 'Privacy')
16 .option('-N, --nsfw', 'Video is Not Safe For Work')
17 .option('-c, --category <category_number>', 'Category number')
18 .option('-C, --channel-id <channel_id>', 'Channel ID')
19 .option('-m, --comments-enabled', 'Enable comments')
20 .option('-l, --licence <licence_number>', 'Licence number')
21 .option('-L, --language <language_code>', 'Language ISO 639 code (fr or en...)')
22 .option('-d, --video-description <description>', 'Video description')
23 .option('-t, --tags <tags>', 'Video tags', list)
24 .option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path') 18 .option('-b, --thumbnail <thumbnailPath>', 'Thumbnail path')
25 .option('-v, --preview <previewPath>', 'Preview path') 19 .option('-v, --preview <previewPath>', 'Preview path')
26 .option('-f, --file <file>', 'Video absolute file path') 20 .option('-f, --file <file>', 'Video absolute file path')
27 .parse(process.argv) 21 .parse(process.argv)
28 22
29getSettings() 23Promise.all([ getSettings(), getNetrc() ])
30 .then(settings => { 24 .then(([ settings, netrc ]) => {
31 const { url, username, password } = getRemoteObjectOrDie(program, settings) 25 const { url, username, password } = getRemoteObjectOrDie(program, settings, netrc)
32 26
33 if (!program['videoName'] || !program['file'] || !program['channelId']) { 27 if (!program[ 'videoName' ] || !program[ 'file' ]) {
34 if (!program['videoName']) console.error('--video-name is required.') 28 if (!program[ 'videoName' ]) console.error('--video-name is required.')
35 if (!program['file']) console.error('--file is required.') 29 if (!program[ 'file' ]) console.error('--file is required.')
36 if (!program['channelId']) console.error('--channel-id is required.')
37 30
38 process.exit(-1) 31 process.exit(-1)
39 } 32 }
40 33
41 if (isAbsolute(program['file']) === false) { 34 if (isAbsolute(program[ 'file' ]) === false) {
42 console.error('File path should be absolute.') 35 console.error('File path should be absolute.')
43 process.exit(-1) 36 process.exit(-1)
44 } 37 }
45 38
46 run(url, username, password).catch(err => { 39 run(url, username, password).catch(err => {
47 console.error(err) 40 console.error(err)
48 process.exit(-1) 41 process.exit(-1)
49 }) 42 })
50 }) 43 })
51 44
52async function run (url: string, username: string, password: string) { 45async function run (url: string, username: string, password: string) {
53 const resClient = await getClient(program[ 'url' ]) 46 const resClient = await getClient(url)
54 const client = { 47 const client = {
55 id: resClient.body.client_id, 48 id: resClient.body.client_id,
56 secret: resClient.body.client_secret 49 secret: resClient.body.client_secret
@@ -70,38 +63,26 @@ async function run (url: string, username: string, password: string) {
70 63
71 console.log('Uploading %s video...', program[ 'videoName' ]) 64 console.log('Uploading %s video...', program[ 'videoName' ])
72 65
73 const videoAttributes = { 66 const defaultAttributes = {
74 name: program['videoName'], 67 tags: command[ 'tags' ],
75 category: program['category'] || undefined, 68 description: command[ 'videoDescription' ]
76 channelId: program['channelId'],
77 licence: program['licence'] || undefined,
78 language: program['language'] || undefined,
79 nsfw: program['nsfw'] !== undefined ? program['nsfw'] : false,
80 description: program['videoDescription'] || '',
81 tags: program['tags'] || [],
82 commentsEnabled: program['commentsEnabled'] !== undefined ? program['commentsEnabled'] : true,
83 downloadEnabled: program['downloadEnabled'] !== undefined ? program['downloadEnabled'] : true,
84 fixture: program['file'],
85 thumbnailfile: program['thumbnail'],
86 previewfile: program['preview'],
87 waitTranscoding: true,
88 privacy: program['privacy'] || VideoPrivacy.PUBLIC,
89 support: undefined
90 } 69 }
70 const videoAttributes = await buildVideoAttributesFromCommander(url, program, defaultAttributes)
71
72 Object.assign(videoAttributes, {
73 fixture: program[ 'file' ],
74 thumbnailfile: program[ 'thumbnail' ],
75 previewfile: program[ 'preview' ]
76 })
91 77
92 try { 78 try {
93 await uploadVideo(url, accessToken, videoAttributes) 79 await uploadVideo(url, accessToken, videoAttributes)
94 console.log(`Video ${program['videoName']} uploaded.`) 80 console.log(`Video ${program[ 'videoName' ]} uploaded.`)
95 process.exit(0) 81 process.exit(0)
96 } catch (err) { 82 } catch (err) {
97 console.log('coucou')
98 console.error(require('util').inspect(err)) 83 console.error(require('util').inspect(err))
99 process.exit(-1) 84 process.exit(-1)
100 } 85 }
101} 86}
102 87
103// ---------------------------------------------------------------------------- 88// ----------------------------------------------------------------------------
104
105function list (val) {
106 return val.split(',')
107}
diff --git a/server/tools/peertube-watch.ts b/server/tools/peertube-watch.ts
index bf7274aab..7c27c1364 100644
--- a/server/tools/peertube-watch.ts
+++ b/server/tools/peertube-watch.ts
@@ -1,21 +1,15 @@
1import * as program from 'commander' 1import * as program from 'commander'
2import * as summon from 'summon-install'
3import { join } from 'path' 2import { join } from 'path'
4import { execSync } from 'child_process' 3import { execSync } from 'child_process'
5import { root } from '../helpers/core-utils'
6
7let videoURL
8 4
9program 5program
10 .name('watch') 6 .name('watch')
11 .arguments('<url>') 7 .arguments('<url>')
12 .option('-g, --gui <player>', 'player type', /^(airplay|stdout|chromecast|mpv|vlc|mplayer|ascii|xbmc)$/i, 'ascii') 8 .option('-g, --gui <player>', 'player type', /^(airplay|stdout|chromecast|mpv|vlc|mplayer|xbmc)$/i, 'vlc')
13 .option('-i, --invert', 'invert colors (ascii player only)', true) 9 .option('-r, --resolution <res>', 'video resolution', '480')
14 .option('-r, --resolution <res>', 'video resolution', /^(240|360|720|1080)$/i, '720')
15 .on('--help', function () { 10 .on('--help', function () {
16 console.log(' Available Players:') 11 console.log(' Available Players:')
17 console.log() 12 console.log()
18 console.log(' - ascii')
19 console.log(' - mpv') 13 console.log(' - mpv')
20 console.log(' - mplayer') 14 console.log(' - mplayer')
21 console.log(' - vlc') 15 console.log(' - vlc')
@@ -24,7 +18,6 @@ program
24 console.log(' - airplay') 18 console.log(' - airplay')
25 console.log(' - chromecast') 19 console.log(' - chromecast')
26 console.log() 20 console.log()
27 console.log(' Note: \'ascii\' is the only option not using WebTorrent and not seeding back the video.')
28 console.log() 21 console.log()
29 console.log(' Examples:') 22 console.log(' Examples:')
30 console.log() 23 console.log()
@@ -33,29 +26,25 @@ program
33 console.log(' $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10') 26 console.log(' $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10')
34 console.log() 27 console.log()
35 }) 28 })
36 .action((url) => { 29 .action((url, cmd) => {
37 videoURL = url 30 run(url, cmd)
31 .catch(err => {
32 console.error(err)
33 process.exit(-1)
34 })
38 }) 35 })
39 .parse(process.argv) 36 .parse(process.argv)
40 37
41if (!videoURL) { 38async function run (url: string, program: any) {
42 console.error('<url> positional argument is required.') 39 if (!url) {
43 process.exit(-1) 40 console.error('<url> positional argument is required.')
44} else { program['url'] = videoURL } 41 process.exit(-1)
42 }
45 43
46handler(program) 44 const cmd = 'node ' + join(__dirname, 'node_modules', 'webtorrent-hybrid', 'bin', 'cmd.js')
45 const args = ` --${program.gui} ` +
46 url.replace('videos/watch', 'download/torrents') +
47 `-${program.resolution}.torrent`
47 48
48function handler (argv) { 49 execSync(cmd + args)
49 if (argv['gui'] === 'ascii') {
50 summon('peerterminal')
51 const peerterminal = summon('peerterminal')
52 peerterminal([ '--link', videoURL, '--invert', argv['invert'] ])
53 } else {
54 summon('webtorrent-hybrid')
55 const CMD = 'node ' + join(root(), 'node_modules', 'webtorrent-hybrid', 'bin', 'cmd.js')
56 const CMDargs = ` --${argv.gui} ` +
57 argv['url'].replace('videos/watch', 'download/torrents') +
58 `-${argv.resolution}.torrent`
59 execSync(CMD + CMDargs)
60 }
61} 50}
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts
index 5d3ab2815..daa5586c3 100755..100644
--- a/server/tools/peertube.ts
+++ b/server/tools/peertube.ts
@@ -63,9 +63,10 @@ if (!process.argv.slice(2).length) {
63 63
64getSettings() 64getSettings()
65 .then(settings => { 65 .then(settings => {
66 const state = (settings.default === undefined || settings.default === -1) ? 66 const state = (settings.default === undefined || settings.default === -1)
67 'no instance selected, commands will require explicit arguments' : 67 ? 'no instance selected, commands will require explicit arguments'
68 ('instance ' + settings.remotes[settings.default] + ' selected') 68 : 'instance ' + settings.remotes[settings.default] + ' selected'
69
69 program 70 program
70 .on('--help', function () { 71 .on('--help', function () {
71 console.log() 72 console.log()
diff --git a/server/tools/tsconfig.json b/server/tools/tsconfig.json
new file mode 100644
index 000000000..f8a1c705c
--- /dev/null
+++ b/server/tools/tsconfig.json
@@ -0,0 +1,4 @@
1{
2 "extends": "../../tsconfig.json",
3 "exclude": [ ] // Overwrite exclude property
4}
diff --git a/server/tools/yarn.lock b/server/tools/yarn.lock
new file mode 100644
index 000000000..7ef68fc71
--- /dev/null
+++ b/server/tools/yarn.lock
@@ -0,0 +1,2063 @@
1# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2# yarn lockfile v1
3
4
5abbrev@1:
6 version "1.1.1"
7 resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
8 integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
9
10addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.4.2:
11 version "1.5.1"
12 resolved "https://registry.yarnpkg.com/addr-to-ip-port/-/addr-to-ip-port-1.5.1.tgz#bfada13fd6aeeeac19f1e9f7d84b4bbab45e5208"
13 integrity sha512-bA+dyydTNuQtrEDJ0g9eR7XabNhvrM5yZY0hvTbNK3yvoeC73ZqMES6E1cEqH9WPxs4uMtMsOjfwS4FmluhsAA==
14
15airplay-js@^0.3.0:
16 version "0.3.0"
17 resolved "https://registry.yarnpkg.com/airplay-js/-/airplay-js-0.3.0.tgz#16bac2ef91b31249382924bfdeeabaddc9db7398"
18 integrity sha1-FrrC75GzEkk4KSS/3uq63cnbc5g=
19 dependencies:
20 mdns-js "0.5.0"
21 plist-with-patches "0.5.1"
22
23ansi-regex@^2.0.0:
24 version "2.1.1"
25 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
26 integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
27
28ansi-regex@^3.0.0:
29 version "3.0.0"
30 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
31 integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
32
33application-config-path@^0.1.0:
34 version "0.1.0"
35 resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.0.tgz#193c5f0a86541a4c66fba1e2dc38583362ea5e8f"
36 integrity sha1-GTxfCoZUGkxm+6Hi3DhYM2LqXo8=
37
38application-config@^1.0.1:
39 version "1.0.1"
40 resolved "https://registry.yarnpkg.com/application-config/-/application-config-1.0.1.tgz#5aa2e2a5ed6abd2e5d1d473d3596f574044fe9e7"
41 integrity sha1-WqLipe1qvS5dHUc9NZb1dARP6ec=
42 dependencies:
43 application-config-path "^0.1.0"
44 mkdirp "^0.5.1"
45
46aproba@^1.0.3:
47 version "1.2.0"
48 resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
49 integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
50
51are-we-there-yet@~1.1.2:
52 version "1.1.5"
53 resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
54 integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
55 dependencies:
56 delegates "^1.0.0"
57 readable-stream "^2.0.6"
58
59ascli@~0.3:
60 version "0.3.0"
61 resolved "https://registry.yarnpkg.com/ascli/-/ascli-0.3.0.tgz#5e66230e5219fe3e8952a4efb4f20fae596a813a"
62 integrity sha1-XmYjDlIZ/j6JUqTvtPIPrllqgTo=
63 dependencies:
64 colour latest
65 optjs latest
66
67async-limiter@~1.0.0:
68 version "1.0.0"
69 resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
70 integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
71
72balanced-match@^1.0.0:
73 version "1.0.0"
74 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
75 integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
76
77bencode@^2.0.0:
78 version "2.0.1"
79 resolved "https://registry.yarnpkg.com/bencode/-/bencode-2.0.1.tgz#667a6a31c5e038d558608333da6b7c94e836c85b"
80 integrity sha512-2uhEl8FdjSBUyb69qDTgOEeeqDTa+n3yMQzLW0cOzNf1Ow5bwcg3idf+qsWisIKRH8Bk8oC7UXL8irRcPA8ZEQ==
81 dependencies:
82 safe-buffer "^5.1.1"
83
84binary-search@^1.3.4:
85 version "1.3.5"
86 resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.5.tgz#479ad009589e0273cf54e5d74ab1546c489078ce"
87 integrity sha512-RHFP0AdU6KAB0CCZsRMU2CJTk2EpL8GLURT+4gilpjr1f/7M91FgUMnXuQLmf3OKLet34gjuNFwO7e4agdX5pw==
88
89bitfield@^2.0.0:
90 version "2.0.0"
91 resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-2.0.0.tgz#fbe6767592fe5b4c87ecf1d04126294cc1bfa837"
92 integrity sha512-4xM4DYejOHQ/qWBfeqBXNA4mJ12PwcOibFYnH1kYh5U9BHciCqEJBqGNVnMJXUhm8mflujNRLSv7IiVQxovgjw==
93
94bittorrent-dht@^9.0.0:
95 version "9.0.0"
96 resolved "https://registry.yarnpkg.com/bittorrent-dht/-/bittorrent-dht-9.0.0.tgz#08d5ebb51ed91d7e3eea5c275554f4323fb523e5"
97 integrity sha512-X5ax4G/PLtEPfqOUjqDZ2nmPENndWRMK4sT2jcQ4sXor904zhR40r4KqTyTvWYAljh5/hPPqM9DCUUtqWzRXoQ==
98 dependencies:
99 bencode "^2.0.0"
100 buffer-equals "^1.0.3"
101 debug "^3.1.0"
102 inherits "^2.0.1"
103 k-bucket "^5.0.0"
104 k-rpc "^5.0.0"
105 last-one-wins "^1.0.4"
106 lru "^3.1.0"
107 randombytes "^2.0.5"
108 record-cache "^1.0.2"
109 safe-buffer "^5.0.1"
110 simple-sha1 "^2.1.0"
111
112bittorrent-peerid@^1.0.2:
113 version "1.3.0"
114 resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.0.tgz#a435d3b267c887c586c528b53359845905d7c158"
115 integrity sha512-SYd5H3RbN1ex+TrWAKXkEkASFWxAR7Tk6iLt9tfAT9ehBvZb/Y3AQDVRVJynlrixcWpnmsLYKI7tkRWgp7ORoQ==
116
117bittorrent-protocol@^3.0.0:
118 version "3.0.1"
119 resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.0.1.tgz#d3948f4d2b09d538095f7e5f93f64ba5df6b5c2a"
120 integrity sha512-hnvOzAu9u+2H0OLLL5byoFdz6oz5f3bx5f7R+ItUohTHMq9TgUhEJfcjo7xWtQHSKOVciYWwYTJ4EjczF5RX2A==
121 dependencies:
122 bencode "^2.0.0"
123 bitfield "^2.0.0"
124 debug "^3.1.0"
125 randombytes "^2.0.5"
126 readable-stream "^2.3.2"
127 speedometer "^1.0.0"
128 unordered-array-remove "^1.0.2"
129 xtend "^4.0.0"
130
131bittorrent-tracker@^9.0.0:
132 version "9.11.0"
133 resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.11.0.tgz#9911f9c14e5a29f84990a0c31b3d83dd16eb2876"
134 integrity sha512-T1zvW/kSeEnWT4I3JE+6c7aZbO5jtleZyQe911SyzIxFF9DvtUNWXud3p5ZUkXaoI2xXwfpvlks5VFj5SKEB+A==
135 dependencies:
136 bencode "^2.0.0"
137 bittorrent-peerid "^1.0.2"
138 bn.js "^4.4.0"
139 compact2string "^1.2.0"
140 debug "^4.0.1"
141 ip "^1.0.1"
142 lru "^3.0.0"
143 minimist "^1.1.1"
144 once "^1.3.0"
145 random-iterate "^1.0.1"
146 randombytes "^2.0.3"
147 run-parallel "^1.1.2"
148 run-series "^1.0.2"
149 safe-buffer "^5.0.0"
150 simple-get "^3.0.0"
151 simple-peer "^9.0.0"
152 simple-websocket "^7.0.1"
153 string2compact "^1.1.1"
154 uniq "^1.0.1"
155 unordered-array-remove "^1.0.2"
156 ws "^6.0.0"
157 optionalDependencies:
158 bufferutil "^4.0.0"
159 utf-8-validate "^5.0.1"
160
161blob-to-buffer@^1.2.6:
162 version "1.2.8"
163 resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.8.tgz#78eeeb332f1280ed0ca6fb2b60693a8c6d36903a"
164 integrity sha512-re0AIxakF504MgeMtIyJkVcZ8T5aUxtp/QmTMlmjyb3P44E1BEv5x3LATBGApWAJATyXHtkXRD+gWTmeyYLiQA==
165
166block-stream2@^1.0.0:
167 version "1.1.0"
168 resolved "https://registry.yarnpkg.com/block-stream2/-/block-stream2-1.1.0.tgz#c738e3a91ba977ebb5e1fef431e13ca11d8639e2"
169 integrity sha1-xzjjqRupd+u14f70MeE8oR2GOeI=
170 dependencies:
171 defined "^1.0.0"
172 inherits "^2.0.1"
173 readable-stream "^2.0.4"
174
175bn.js@^4.4.0:
176 version "4.11.8"
177 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
178 integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
179
180brace-expansion@^1.1.7:
181 version "1.1.11"
182 resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
183 integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
184 dependencies:
185 balanced-match "^1.0.0"
186 concat-map "0.0.1"
187
188browserify-package-json@^1.0.0:
189 version "1.0.1"
190 resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea"
191 integrity sha1-mN3oqlxWH9bT/km7qhArdLOW/eo=
192
193buffer-alloc-unsafe@^1.1.0:
194 version "1.1.0"
195 resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
196 integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
197
198buffer-alloc@^1.1.0, buffer-alloc@^1.2.0:
199 version "1.2.0"
200 resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
201 integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
202 dependencies:
203 buffer-alloc-unsafe "^1.1.0"
204 buffer-fill "^1.0.0"
205
206buffer-equals@^1.0.3, buffer-equals@^1.0.4:
207 version "1.0.4"
208 resolved "https://registry.yarnpkg.com/buffer-equals/-/buffer-equals-1.0.4.tgz#0353b54fd07fd9564170671ae6f66b9cf10d27f5"
209 integrity sha1-A1O1T9B/2VZBcGca5vZrnPENJ/U=
210
211buffer-fill@^1.0.0:
212 version "1.0.0"
213 resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
214 integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
215
216buffer-from@^1.0.0, buffer-from@^1.1.0:
217 version "1.1.1"
218 resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
219 integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
220
221buffer-indexof@^1.0.0:
222 version "1.1.1"
223 resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
224 integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==
225
226bufferutil@^4.0.0:
227 version "4.0.1"
228 resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
229 integrity sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==
230 dependencies:
231 node-gyp-build "~3.7.0"
232
233bufferview@~1:
234 version "1.0.1"
235 resolved "https://registry.yarnpkg.com/bufferview/-/bufferview-1.0.1.tgz#7afd74a45f937fa422a1d338c08bbfdc76cd725d"
236 integrity sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0=
237
238"bytebuffer@~3 >=3.5":
239 version "3.5.5"
240 resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-3.5.5.tgz#7a6faf1a13514b083f1fcf9541c4c9bfbe7e7fd3"
241 integrity sha1-em+vGhNRSwg/H8+VQcTJv75+f9M=
242 dependencies:
243 bufferview "~1"
244 long "~2 >=2.2.3"
245
246camelcase@^3.0.0:
247 version "3.0.0"
248 resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
249 integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo=
250
251castv2-client@^1.1.0:
252 version "1.2.0"
253 resolved "https://registry.yarnpkg.com/castv2-client/-/castv2-client-1.2.0.tgz#a9193b1a5448b8cb9a0415bd021c8811ed7b0544"
254 integrity sha1-qRk7GlRIuMuaBBW9AhyIEe17BUQ=
255 dependencies:
256 castv2 "~0.1.4"
257 debug "^2.2.0"
258
259castv2@~0.1.4:
260 version "0.1.9"
261 resolved "https://registry.yarnpkg.com/castv2/-/castv2-0.1.9.tgz#d0b0fab1fd06b0d9cca636886716ec1293a5905a"
262 integrity sha1-0LD6sf0GsNnMpjaIZxbsEpOlkFo=
263 dependencies:
264 debug "^2.2.0"
265 protobufjs "^3.2.2"
266
267chownr@^1.1.1:
268 version "1.1.1"
269 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
270 integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
271
272chromecasts@^1.5.3:
273 version "1.9.1"
274 resolved "https://registry.yarnpkg.com/chromecasts/-/chromecasts-1.9.1.tgz#67b162e8414d57d6106c49fe4a0e9b08f20bbd12"
275 integrity sha512-nsXv7ufgrpC8s5DUm6FJEa2XJ2VvE9FmbTVi6r4zGreTFTTSRSJjvqVEqLUFX/fGo/zbSre3zdoV+Pu9DGLz0A==
276 dependencies:
277 castv2-client "^1.1.0"
278 debug "^2.1.3"
279 dns-txt "^2.0.2"
280 mime "^1.3.4"
281 multicast-dns "^6.0.1"
282 simple-get "^2.0.0"
283 thunky "^0.1.0"
284 xml2js "^0.4.8"
285 optionalDependencies:
286 node-ssdp "^2.2.0"
287
288chunk-store-stream@^3.0.1:
289 version "3.0.1"
290 resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-3.0.1.tgz#8e0d739226dcb386f44447b82a005b597a1d41d9"
291 integrity sha512-GA1NIFDZKElhkjiO6QOyzfK1QbUt6M3gFhUU/aR05JYaDqXbU5d7U92cLvGKdItJEDfojky6NQefy5VL5PpDBA==
292 dependencies:
293 block-stream2 "^1.0.0"
294 readable-stream "^2.0.5"
295
296cli-table@^0.3.1:
297 version "0.3.1"
298 resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
299 integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM=
300 dependencies:
301 colors "1.0.3"
302
303cliui@^3.2.0:
304 version "3.2.0"
305 resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
306 integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
307 dependencies:
308 string-width "^1.0.1"
309 strip-ansi "^3.0.1"
310 wrap-ansi "^2.0.0"
311
312clivas@^0.2.0:
313 version "0.2.0"
314 resolved "https://registry.yarnpkg.com/clivas/-/clivas-0.2.0.tgz#b8d19188b3243e390f302410bd0cb1622db82649"
315 integrity sha1-uNGRiLMkPjkPMCQQvQyxYi24Jkk=
316
317closest-to@~2.0.0:
318 version "2.0.0"
319 resolved "https://registry.yarnpkg.com/closest-to/-/closest-to-2.0.0.tgz#bb2a860edb7769b62d04821748ae50da24dbefaa"
320 integrity sha1-uyqGDtt3abYtBIIXSK5Q2iTb76o=
321
322code-point-at@^1.0.0:
323 version "1.1.0"
324 resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
325 integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
326
327colors@1.0.3:
328 version "1.0.3"
329 resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
330 integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
331
332colour@latest:
333 version "0.7.1"
334 resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778"
335 integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=
336
337compact2string@^1.2.0:
338 version "1.4.1"
339 resolved "https://registry.yarnpkg.com/compact2string/-/compact2string-1.4.1.tgz#8d34929055f8300a13cfc030ad1832e2e53c2e25"
340 integrity sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og==
341 dependencies:
342 ipaddr.js ">= 0.1.5"
343
344concat-map@0.0.1:
345 version "0.0.1"
346 resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
347 integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
348
349concat-stream@^1.4.8:
350 version "1.6.2"
351 resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
352 integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
353 dependencies:
354 buffer-from "^1.0.0"
355 inherits "^2.0.3"
356 readable-stream "^2.2.2"
357 typedarray "^0.0.6"
358
359console-control-strings@^1.0.0, console-control-strings@~1.1.0:
360 version "1.1.0"
361 resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
362 integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
363
364core-util-is@~1.0.0:
365 version "1.0.2"
366 resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
367 integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
368
369create-torrent@^3.23.1, create-torrent@^3.33.0:
370 version "3.33.0"
371 resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-3.33.0.tgz#8a7a2aa2213a799c266c40e4c12f1468ede25105"
372 integrity sha512-KMd0KuvwVUg1grlRd5skG9ZkSbBYDDkAjDUMLnvxdRn0rL7ph3IwoOk7I8u1yLX4HYjGiLVlWYO55YWNNPjJFA==
373 dependencies:
374 bencode "^2.0.0"
375 block-stream2 "^1.0.0"
376 filestream "^4.0.0"
377 flatten "^1.0.2"
378 is-file "^1.0.0"
379 junk "^2.1.0"
380 minimist "^1.1.0"
381 multistream "^2.0.2"
382 once "^1.3.0"
383 piece-length "^1.0.0"
384 readable-stream "^3.0.2"
385 run-parallel "^1.0.0"
386 simple-sha1 "^2.0.0"
387
388cross-spawn@^6.0.0:
389 version "6.0.5"
390 resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
391 integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
392 dependencies:
393 nice-try "^1.0.4"
394 path-key "^2.0.1"
395 semver "^5.5.0"
396 shebang-command "^1.2.0"
397 which "^1.2.9"
398
399debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0:
400 version "2.6.9"
401 resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
402 integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
403 dependencies:
404 ms "2.0.0"
405
406debug@^3.1.0, debug@^3.2.6:
407 version "3.2.6"
408 resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
409 integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
410 dependencies:
411 ms "^2.1.1"
412
413debug@^4.0.1, debug@^4.1.0:
414 version "4.1.1"
415 resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
416 integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
417 dependencies:
418 ms "^2.1.1"
419
420decamelize@^1.1.1:
421 version "1.2.0"
422 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
423 integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
424
425decompress-response@^3.3.0:
426 version "3.3.0"
427 resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
428 integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
429 dependencies:
430 mimic-response "^1.0.0"
431
432deep-extend@^0.6.0:
433 version "0.6.0"
434 resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
435 integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
436
437defined@^1.0.0:
438 version "1.0.0"
439 resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
440 integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
441
442delegates@^1.0.0:
443 version "1.0.0"
444 resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
445 integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
446
447detect-libc@^1.0.2:
448 version "1.0.3"
449 resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
450 integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
451
452dlnacasts@^0.1.0:
453 version "0.1.0"
454 resolved "https://registry.yarnpkg.com/dlnacasts/-/dlnacasts-0.1.0.tgz#f805211dcac74f6bb3a4d5d5541ad783b1b67d22"
455 integrity sha1-+AUhHcrHT2uzpNXVVBrXg7G2fSI=
456 dependencies:
457 debug "^2.1.3"
458 mime "^1.3.4"
459 node-ssdp "^2.7.1"
460 run-parallel "^1.1.6"
461 simple-get "^2.1.0"
462 thunky "^0.1.0"
463 upnp-mediarenderer-client "^1.2.2"
464 xml2js "^0.4.8"
465
466dns-packet@^1.3.1:
467 version "1.3.1"
468 resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
469 integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
470 dependencies:
471 ip "^1.1.0"
472 safe-buffer "^5.0.1"
473
474dns-txt@^2.0.2:
475 version "2.0.2"
476 resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
477 integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=
478 dependencies:
479 buffer-indexof "^1.0.0"
480
481domexception@^1.0.1:
482 version "1.0.1"
483 resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
484 integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
485 dependencies:
486 webidl-conversions "^4.0.2"
487
488ecstatic@^3.0.0:
489 version "3.3.2"
490 resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48"
491 integrity sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==
492 dependencies:
493 he "^1.1.1"
494 mime "^1.6.0"
495 minimist "^1.1.0"
496 url-join "^2.0.5"
497
498elementtree@^0.1.6, elementtree@~0.1.6:
499 version "0.1.7"
500 resolved "https://registry.yarnpkg.com/elementtree/-/elementtree-0.1.7.tgz#9ac91be6e52fb6e6244c4e54a4ac3ed8ae8e29c0"
501 integrity sha1-mskb5uUvtuYkTE5UpKw+2K6OKcA=
502 dependencies:
503 sax "1.1.4"
504
505end-of-stream@^1.1.0:
506 version "1.4.1"
507 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
508 integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
509 dependencies:
510 once "^1.4.0"
511
512error-ex@^1.2.0:
513 version "1.3.2"
514 resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
515 integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
516 dependencies:
517 is-arrayish "^0.2.1"
518
519execa@^0.10.0:
520 version "0.10.0"
521 resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
522 integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
523 dependencies:
524 cross-spawn "^6.0.0"
525 get-stream "^3.0.0"
526 is-stream "^1.1.0"
527 npm-run-path "^2.0.0"
528 p-finally "^1.0.0"
529 signal-exit "^3.0.0"
530 strip-eof "^1.0.0"
531
532executable@^4.0.0:
533 version "4.1.1"
534 resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
535 integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==
536 dependencies:
537 pify "^2.2.0"
538
539filestream@^4.0.0:
540 version "4.1.3"
541 resolved "https://registry.yarnpkg.com/filestream/-/filestream-4.1.3.tgz#948fcaade8221f715f5ecaddc54862faaacc9325"
542 integrity sha1-lI/KregiH3FfXsrdxUhi+qrMkyU=
543 dependencies:
544 inherits "^2.0.1"
545 readable-stream "^2.0.5"
546 typedarray-to-buffer "^3.0.0"
547 xtend "^4.0.1"
548
549find-up@^1.0.0:
550 version "1.1.2"
551 resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
552 integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
553 dependencies:
554 path-exists "^2.0.0"
555 pinkie-promise "^2.0.0"
556
557flatten@^1.0.2:
558 version "1.0.2"
559 resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
560 integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
561
562fs-chunk-store@^1.6.2:
563 version "1.7.0"
564 resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-1.7.0.tgz#1c4bcbe93c99af10aa04b65348f2bb27377a4010"
565 integrity sha512-KhjJmZAs2eqfhCb6PdPx4RcZtheGTz86tpTC5JTvqBn/xda+Nb+0C7dCyjOSN7T76H6a56LvH0SVXQMchLXDRw==
566 dependencies:
567 mkdirp "^0.5.1"
568 random-access-file "^2.0.1"
569 randombytes "^2.0.3"
570 rimraf "^2.4.2"
571 run-parallel "^1.1.2"
572 thunky "^1.0.1"
573
574fs-minipass@^1.2.5:
575 version "1.2.6"
576 resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
577 integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==
578 dependencies:
579 minipass "^2.2.1"
580
581fs.realpath@^1.0.0:
582 version "1.0.0"
583 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
584 integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
585
586gauge@~2.7.3:
587 version "2.7.4"
588 resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
589 integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
590 dependencies:
591 aproba "^1.0.3"
592 console-control-strings "^1.0.0"
593 has-unicode "^2.0.0"
594 object-assign "^4.1.0"
595 signal-exit "^3.0.0"
596 string-width "^1.0.1"
597 strip-ansi "^3.0.1"
598 wide-align "^1.1.0"
599
600get-browser-rtc@^1.0.0:
601 version "1.0.2"
602 resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9"
603 integrity sha1-u81AyEUaftTvXDc7gWmkCd0dEdk=
604
605get-caller-file@^1.0.1:
606 version "1.0.3"
607 resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
608 integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
609
610get-stdin@^6.0.0:
611 version "6.0.0"
612 resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
613 integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
614
615get-stream@^3.0.0:
616 version "3.0.0"
617 resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
618 integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
619
620glob@^7.1.3:
621 version "7.1.4"
622 resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
623 integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
624 dependencies:
625 fs.realpath "^1.0.0"
626 inflight "^1.0.4"
627 inherits "2"
628 minimatch "^3.0.4"
629 once "^1.3.0"
630 path-is-absolute "^1.0.0"
631
632graceful-fs@^4.1.2:
633 version "4.1.15"
634 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
635 integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
636
637has-unicode@^2.0.0:
638 version "2.0.1"
639 resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
640 integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
641
642he@^1.1.1:
643 version "1.2.0"
644 resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
645 integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
646
647hosted-git-info@^2.1.4:
648 version "2.7.1"
649 resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
650 integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==
651
652iconv-lite@^0.4.4:
653 version "0.4.24"
654 resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
655 integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
656 dependencies:
657 safer-buffer ">= 2.1.2 < 3"
658
659ignore-walk@^3.0.1:
660 version "3.0.1"
661 resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
662 integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==
663 dependencies:
664 minimatch "^3.0.4"
665
666immediate-chunk-store@^2.0.0:
667 version "2.0.0"
668 resolved "https://registry.yarnpkg.com/immediate-chunk-store/-/immediate-chunk-store-2.0.0.tgz#f313fd0cc71396d8911ad031179e1cccfda3da18"
669 integrity sha512-5s6NiCGbtWc+OQA60jrre54w12U7tynIyUNjO5LJjNA5lWwvCv6640roq8Wk/wIuaqnd4Pgtp453OyJ7hbONkQ==
670
671inflight@^1.0.4:
672 version "1.0.6"
673 resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
674 integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
675 dependencies:
676 once "^1.3.0"
677 wrappy "1"
678
679inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
680 version "2.0.3"
681 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
682 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
683
684ini@~1.3.0:
685 version "1.3.5"
686 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
687 integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
688
689invert-kv@^1.0.0:
690 version "1.0.0"
691 resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
692 integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
693
694ip-set@^1.0.0:
695 version "1.0.2"
696 resolved "https://registry.yarnpkg.com/ip-set/-/ip-set-1.0.2.tgz#be4f119f82c124836455993dfcd554639c7007de"
697 integrity sha512-Mb6kv78bTi4RNAIIWL8Bbre7hXOR2pNUi3j8FaQkLaitf/ZWxkq3/iIwXNYk2ACO3IMfdVdQrOkUtwZblO7uBA==
698 dependencies:
699 ip "^1.1.3"
700
701ip@^1.0.1, ip@^1.1.0, ip@^1.1.3:
702 version "1.1.5"
703 resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
704 integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
705
706"ipaddr.js@>= 0.1.5", ipaddr.js@^1.0.1:
707 version "1.9.0"
708 resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
709 integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
710
711is-arrayish@^0.2.1:
712 version "0.2.1"
713 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
714 integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
715
716is-ascii@^1.0.0:
717 version "1.0.0"
718 resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929"
719 integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk=
720
721is-file@^1.0.0:
722 version "1.0.0"
723 resolved "https://registry.yarnpkg.com/is-file/-/is-file-1.0.0.tgz#28a44cfbd9d3db193045f22b65fce8edf9620596"
724 integrity sha1-KKRM+9nT2xkwRfIrZfzo7fliBZY=
725
726is-fullwidth-code-point@^1.0.0:
727 version "1.0.0"
728 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
729 integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
730 dependencies:
731 number-is-nan "^1.0.0"
732
733is-fullwidth-code-point@^2.0.0:
734 version "2.0.0"
735 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
736 integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
737
738is-stream@^1.1.0:
739 version "1.1.0"
740 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
741 integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
742
743is-typedarray@^1.0.0:
744 version "1.0.0"
745 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
746 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
747
748is-utf8@^0.2.0:
749 version "0.2.1"
750 resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
751 integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
752
753isarray@~1.0.0:
754 version "1.0.0"
755 resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
756 integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
757
758isexe@^2.0.0:
759 version "2.0.0"
760 resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
761 integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
762
763junk@^2.1.0:
764 version "2.1.0"
765 resolved "https://registry.yarnpkg.com/junk/-/junk-2.1.0.tgz#f431b4b7f072dc500a5f10ce7f4ec71930e70134"
766 integrity sha1-9DG0t/By3FAKXxDOf07HGTDnATQ=
767
768k-bucket@^4.0.0:
769 version "4.0.1"
770 resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-4.0.1.tgz#3fc2e5693f0b7bff90d7b6b476edd6087955d542"
771 integrity sha512-YvDpmY3waI999h1zZoW1rJ04fZrgZ+5PAlVmvwDHT6YO/Q1AOhdel07xsKy9eAvJjQ9xZV1wz3rXKqEfaWvlcQ==
772 dependencies:
773 inherits "^2.0.1"
774 randombytes "^2.0.3"
775
776k-bucket@^5.0.0:
777 version "5.0.0"
778 resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-5.0.0.tgz#ef7a401fcd4c37cd31dceaa6ae4440ca91055e01"
779 integrity sha512-r/q+wV/Kde62/tk+rqyttEJn6h0jR7x+incdMVSYTqK73zVxVrzJa70kJL49cIKen8XjIgUZKSvk8ktnrQbK4w==
780 dependencies:
781 randombytes "^2.0.3"
782
783k-rpc-socket@^1.7.2:
784 version "1.8.0"
785 resolved "https://registry.yarnpkg.com/k-rpc-socket/-/k-rpc-socket-1.8.0.tgz#9a4dd6a4f3795ed847ffa156579cc389990bd1f2"
786 integrity sha512-f/9TynsO8YYjZ6JjNNtSSH7CJcIHcio1buy3zqByGxb/GX8AWLdL6FZEWTrN8V3/J7W4/E0ZTQQ+Jt2rVq7ELg==
787 dependencies:
788 bencode "^2.0.0"
789 buffer-equals "^1.0.4"
790 safe-buffer "^5.1.1"
791
792k-rpc@^5.0.0:
793 version "5.0.0"
794 resolved "https://registry.yarnpkg.com/k-rpc/-/k-rpc-5.0.0.tgz#a72651860c96db440579e4c9f38dce8a42b481a8"
795 integrity sha512-vCH2rQdfMOS+MlUuTSuar1pS2EMrltURf9LmAR9xR6Jik0XPlMX3vEixgqMn17wKmFVCublJqSJ4hJIP7oKZ3Q==
796 dependencies:
797 buffer-equals "^1.0.3"
798 k-bucket "^4.0.0"
799 k-rpc-socket "^1.7.2"
800 randombytes "^2.0.5"
801 safe-buffer "^5.1.1"
802
803last-one-wins@^1.0.4:
804 version "1.0.4"
805 resolved "https://registry.yarnpkg.com/last-one-wins/-/last-one-wins-1.0.4.tgz#c1bfd0cbcb46790ec9156b8d1aee8fcb86cda22a"
806 integrity sha1-wb/Qy8tGeQ7JFWuNGu6Py4bNoio=
807
808lcid@^1.0.0:
809 version "1.0.0"
810 resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
811 integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=
812 dependencies:
813 invert-kv "^1.0.0"
814
815load-ip-set@^2.1.0:
816 version "2.1.0"
817 resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.1.0.tgz#2d50b737cae41de4e413d213991d4083a3e1784b"
818 integrity sha512-taz7U6B+F7Zq90dfIKwqsB1CrFKelSEmMGC68OUqem8Cgd1QZygQBYb2Fk9i6muBSfH4xwF/Pjt4KKlAdOyWZw==
819 dependencies:
820 ip-set "^1.0.0"
821 netmask "^1.0.6"
822 once "^1.3.0"
823 simple-get "^3.0.0"
824 split "^1.0.0"
825
826load-json-file@^1.0.0:
827 version "1.1.0"
828 resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
829 integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
830 dependencies:
831 graceful-fs "^4.1.2"
832 parse-json "^2.2.0"
833 pify "^2.0.0"
834 pinkie-promise "^2.0.0"
835 strip-bom "^2.0.0"
836
837"long@~2 >=2.2.3":
838 version "2.4.0"
839 resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f"
840 integrity sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=
841
842lru@^3.0.0, lru@^3.1.0:
843 version "3.1.0"
844 resolved "https://registry.yarnpkg.com/lru/-/lru-3.1.0.tgz#ea7fb8546d83733396a13091d76cfeb4c06837d5"
845 integrity sha1-6n+4VG2DczOWoTCR12z+tMBoN9U=
846 dependencies:
847 inherits "^2.0.1"
848
849magnet-uri@^5.1.3:
850 version "5.2.4"
851 resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-5.2.4.tgz#7afe5b736af04445aff744c93a890a3710077688"
852 integrity sha512-VYaJMxhr8B9BrCiNINUsuhaEe40YnG+AQBwcqUKO66lSVaI9I3A1iH/6EmEwRI8OYUg5Gt+4lLE7achg676lrg==
853 dependencies:
854 thirty-two "^1.0.1"
855 uniq "^1.0.1"
856
857mdns-js-packet@~0.2.0:
858 version "0.2.0"
859 resolved "https://registry.yarnpkg.com/mdns-js-packet/-/mdns-js-packet-0.2.0.tgz#642409e8183c7561cc60615bbd1420ec2fad7616"
860 integrity sha1-ZCQJ6Bg8dWHMYGFbvRQg7C+tdhY=
861 dependencies:
862 debug "^2.1.0"
863 qap "^3.1.2"
864
865mdns-js@0.5.0:
866 version "0.5.0"
867 resolved "https://registry.yarnpkg.com/mdns-js/-/mdns-js-0.5.0.tgz#4c8abb6ba7cabdc892d39228c3faa2556e09cf87"
868 integrity sha1-TIq7a6fKvciS05Iow/qiVW4Jz4c=
869 dependencies:
870 debug "^2.1.1"
871 mdns-js-packet "~0.2.0"
872 semver "~5.1.0"
873
874mediasource@^2.1.0, mediasource@^2.2.2:
875 version "2.3.0"
876 resolved "https://registry.yarnpkg.com/mediasource/-/mediasource-2.3.0.tgz#4c7b49e7ea4fb88f1cc181d8fcf0d94649271dc6"
877 integrity sha512-fqm86UwHvAnneIv40Uy1sDQaFtAByq/k0SQ3uCtbnEeSQNT1s5TDHCZOD1VmYCHwfY1jL2NjoZVwzZKYqy3L7A==
878 dependencies:
879 inherits "^2.0.1"
880 readable-stream "^3.0.0"
881 to-arraybuffer "^1.0.1"
882
883memory-chunk-store@^1.2.0:
884 version "1.3.0"
885 resolved "https://registry.yarnpkg.com/memory-chunk-store/-/memory-chunk-store-1.3.0.tgz#ae99e7e3b58b52db43d49d94722930d39459d0c4"
886 integrity sha512-6LsOpHKKhxYrLhHmOJdBCUtSO7op5rUs1pag0fhjHo0QiXRyna0bwYf4EmQuL7InUeF2J7dUMPr6VMogRyf9NA==
887
888mime@^1.3.4, mime@^1.6.0:
889 version "1.6.0"
890 resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
891 integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
892
893mime@^2.1.0, mime@^2.4.0:
894 version "2.4.3"
895 resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.3.tgz#229687331e86f68924e6cb59e1cdd937f18275fe"
896 integrity sha512-QgrPRJfE+riq5TPZMcHZOtm8c6K/yYrMbKIoRfapfiGLxS8OTeIfRhUGW5LU7MlRa52KOAGCfUNruqLrIBvWZw==
897
898mimic-response@^1.0.0:
899 version "1.0.1"
900 resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
901 integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
902
903minimatch@^3.0.4:
904 version "3.0.4"
905 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
906 integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
907 dependencies:
908 brace-expansion "^1.1.7"
909
910minimist@0.0.8:
911 version "0.0.8"
912 resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
913 integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
914
915minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0:
916 version "1.2.0"
917 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
918 integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
919
920minipass@^2.2.1, minipass@^2.3.4:
921 version "2.3.5"
922 resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
923 integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
924 dependencies:
925 safe-buffer "^5.1.2"
926 yallist "^3.0.0"
927
928minizlib@^1.1.1:
929 version "1.2.1"
930 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614"
931 integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==
932 dependencies:
933 minipass "^2.2.1"
934
935mkdirp@^0.5.0, mkdirp@^0.5.1:
936 version "0.5.1"
937 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
938 integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
939 dependencies:
940 minimist "0.0.8"
941
942moment@^2.12.0:
943 version "2.24.0"
944 resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
945 integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
946
947mp4-box-encoding@^1.1.0, mp4-box-encoding@^1.3.0:
948 version "1.3.0"
949 resolved "https://registry.yarnpkg.com/mp4-box-encoding/-/mp4-box-encoding-1.3.0.tgz#2a6f750947ff68c3a498fd76cd6424c53d995d48"
950 integrity sha512-U4pMLpjT/UzB8d36dxj6Mf1bG9xypEvgbuRIa1fztRXNKKTCAtRxsnFZhNOd7YDFOKtjBgssYGvo4H/Q3ZY1MA==
951 dependencies:
952 buffer-alloc "^1.2.0"
953 buffer-from "^1.1.0"
954 uint64be "^2.0.2"
955
956mp4-stream@^2.0.0:
957 version "2.0.3"
958 resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-2.0.3.tgz#30acee07709d323f8dcd87a07b3ce9c3c4bfb364"
959 integrity sha512-5NzgI0+bGakoZEwnIYINXqB3mnewkt3Y7jcvkXsTubnCNUSdM8cpP0Vemxf6FLg0qUN8fydTgNMVAc3QU8B92g==
960 dependencies:
961 buffer-alloc "^1.1.0"
962 inherits "^2.0.1"
963 mp4-box-encoding "^1.1.0"
964 next-event "^1.0.0"
965 readable-stream "^2.0.3"
966
967ms@2.0.0:
968 version "2.0.0"
969 resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
970 integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
971
972ms@^2.1.1:
973 version "2.1.1"
974 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
975 integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
976
977multicast-dns@^6.0.1:
978 version "6.2.3"
979 resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229"
980 integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==
981 dependencies:
982 dns-packet "^1.3.1"
983 thunky "^1.0.2"
984
985multistream@^2.0.2, multistream@^2.0.5:
986 version "2.1.1"
987 resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c"
988 integrity sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==
989 dependencies:
990 inherits "^2.0.1"
991 readable-stream "^2.0.5"
992
993nan@*, nan@^2.3.2:
994 version "2.14.0"
995 resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
996 integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
997
998needle@^2.2.1:
999 version "2.4.0"
1000 resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c"
1001 integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==
1002 dependencies:
1003 debug "^3.2.6"
1004 iconv-lite "^0.4.4"
1005 sax "^1.2.4"
1006
1007netmask@^1.0.6:
1008 version "1.0.6"
1009 resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
1010 integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=
1011
1012netrc-parser@^3.1.6:
1013 version "3.1.6"
1014 resolved "https://registry.yarnpkg.com/netrc-parser/-/netrc-parser-3.1.6.tgz#7243c9ec850b8e805b9bdc7eae7b1450d4a96e72"
1015 integrity sha512-lY+fmkqSwntAAjfP63jB4z5p5WbuZwyMCD3pInT7dpHU/Gc6Vv90SAC6A0aNiqaRGHiuZFBtiwu+pu8W/Eyotw==
1016 dependencies:
1017 debug "^3.1.0"
1018 execa "^0.10.0"
1019
1020network-address@^1.0.0, network-address@^1.1.0:
1021 version "1.1.2"
1022 resolved "https://registry.yarnpkg.com/network-address/-/network-address-1.1.2.tgz#4aa7bfd43f03f0b81c9702b13d6a858ddb326f3e"
1023 integrity sha1-Sqe/1D8D8LgclwKxPWqFjdsybz4=
1024
1025next-event@^1.0.0:
1026 version "1.0.0"
1027 resolved "https://registry.yarnpkg.com/next-event/-/next-event-1.0.0.tgz#e7778acde2e55802e0ad1879c39cf6f75eda61d8"
1028 integrity sha1-53eKzeLlWALgrRh5w5z2917aYdg=
1029
1030nice-try@^1.0.4:
1031 version "1.0.5"
1032 resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
1033 integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
1034
1035node-cmake@2.3.2:
1036 version "2.3.2"
1037 resolved "https://registry.yarnpkg.com/node-cmake/-/node-cmake-2.3.2.tgz#e0fbc54b11405b07705e4d6d41865ae95ad289d0"
1038 integrity sha1-4PvFSxFAWwdwXk1tQYZa6VrSidA=
1039 dependencies:
1040 nan "*"
1041 which "^1.2.14"
1042 yargs "^7.0.2"
1043
1044node-gyp-build@~3.7.0:
1045 version "3.7.0"
1046 resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
1047 integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
1048
1049node-pre-gyp@0.11.x:
1050 version "0.11.0"
1051 resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
1052 integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==
1053 dependencies:
1054 detect-libc "^1.0.2"
1055 mkdirp "^0.5.1"
1056 needle "^2.2.1"
1057 nopt "^4.0.1"
1058 npm-packlist "^1.1.6"
1059 npmlog "^4.0.2"
1060 rc "^1.2.7"
1061 rimraf "^2.6.1"
1062 semver "^5.3.0"
1063 tar "^4"
1064
1065node-ssdp@^2.2.0, node-ssdp@^2.7.1:
1066 version "2.9.1"
1067 resolved "https://registry.yarnpkg.com/node-ssdp/-/node-ssdp-2.9.1.tgz#2d6ba8e7eff9bf5b338564f91f7ac5d5cdddc55b"
1068 integrity sha1-LWuo5+/5v1szhWT5H3rF1c3dxVs=
1069 dependencies:
1070 debug "^2.2.0"
1071 ip "^1.0.1"
1072
1073nodebmc@0.0.7:
1074 version "0.0.7"
1075 resolved "https://registry.yarnpkg.com/nodebmc/-/nodebmc-0.0.7.tgz#fae179165265509302cefbebeabd29bd4035184d"
1076 integrity sha1-+uF5FlJlUJMCzvvr6r0pvUA1GE0=
1077 dependencies:
1078 mdns-js "0.5.0"
1079
1080nopt@^4.0.1:
1081 version "4.0.1"
1082 resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
1083 integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
1084 dependencies:
1085 abbrev "1"
1086 osenv "^0.1.4"
1087
1088normalize-package-data@^2.3.2:
1089 version "2.5.0"
1090 resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
1091 integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
1092 dependencies:
1093 hosted-git-info "^2.1.4"
1094 resolve "^1.10.0"
1095 semver "2 || 3 || 4 || 5"
1096 validate-npm-package-license "^3.0.1"
1097
1098npm-bundled@^1.0.1:
1099 version "1.0.6"
1100 resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
1101 integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
1102
1103npm-packlist@^1.1.6:
1104 version "1.4.1"
1105 resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
1106 integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==
1107 dependencies:
1108 ignore-walk "^3.0.1"
1109 npm-bundled "^1.0.1"
1110
1111npm-run-path@^2.0.0:
1112 version "2.0.2"
1113 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
1114 integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
1115 dependencies:
1116 path-key "^2.0.0"
1117
1118npmlog@^4.0.2:
1119 version "4.1.2"
1120 resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
1121 integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
1122 dependencies:
1123 are-we-there-yet "~1.1.2"
1124 console-control-strings "~1.1.0"
1125 gauge "~2.7.3"
1126 set-blocking "~2.0.0"
1127
1128number-is-nan@^1.0.0:
1129 version "1.0.1"
1130 resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
1131 integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
1132
1133object-assign@^4.1.0:
1134 version "4.1.1"
1135 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
1136 integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
1137
1138once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
1139 version "1.4.0"
1140 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
1141 integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
1142 dependencies:
1143 wrappy "1"
1144
1145open@0.0.5:
1146 version "0.0.5"
1147 resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc"
1148 integrity sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=
1149
1150optjs@latest:
1151 version "3.2.2"
1152 resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee"
1153 integrity sha1-aabOicRCpEQDFBrS+bNwvVu29O4=
1154
1155os-homedir@^1.0.0:
1156 version "1.0.2"
1157 resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
1158 integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
1159
1160os-locale@^1.4.0:
1161 version "1.4.0"
1162 resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
1163 integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=
1164 dependencies:
1165 lcid "^1.0.0"
1166
1167os-tmpdir@^1.0.0:
1168 version "1.0.2"
1169 resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
1170 integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
1171
1172osenv@^0.1.4:
1173 version "0.1.5"
1174 resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
1175 integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
1176 dependencies:
1177 os-homedir "^1.0.0"
1178 os-tmpdir "^1.0.0"
1179
1180p-finally@^1.0.0:
1181 version "1.0.0"
1182 resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
1183 integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
1184
1185package-json-versionify@^1.0.2:
1186 version "1.0.4"
1187 resolved "https://registry.yarnpkg.com/package-json-versionify/-/package-json-versionify-1.0.4.tgz#5860587a944873a6b7e6d26e8e51ffb22315bf17"
1188 integrity sha1-WGBYepRIc6a35tJujlH/siMVvxc=
1189 dependencies:
1190 browserify-package-json "^1.0.0"
1191
1192parse-json@^2.2.0:
1193 version "2.2.0"
1194 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
1195 integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
1196 dependencies:
1197 error-ex "^1.2.0"
1198
1199parse-numeric-range@^0.0.2:
1200 version "0.0.2"
1201 resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-0.0.2.tgz#b4f09d413c7adbcd987f6e9233c7b4b210c938e4"
1202 integrity sha1-tPCdQTx6282Yf26SM8e0shDJOOQ=
1203
1204parse-torrent@^6.0.0, parse-torrent@^6.1.2:
1205 version "6.1.2"
1206 resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-6.1.2.tgz#99da5bdd23435a1cb7e8e7a63847c4efb21b1956"
1207 integrity sha512-Z/vig84sHwtrTEbOzisT4xnYTFlOgAaLQccPruMPgRahZUppVE/BUXzAos3jZM7c64o0lfukQdQ4ozWa5lN39w==
1208 dependencies:
1209 bencode "^2.0.0"
1210 blob-to-buffer "^1.2.6"
1211 get-stdin "^6.0.0"
1212 magnet-uri "^5.1.3"
1213 simple-get "^3.0.1"
1214 simple-sha1 "^2.0.0"
1215 uniq "^1.0.1"
1216
1217path-exists@^2.0.0:
1218 version "2.1.0"
1219 resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
1220 integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
1221 dependencies:
1222 pinkie-promise "^2.0.0"
1223
1224path-is-absolute@^1.0.0:
1225 version "1.0.1"
1226 resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
1227 integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
1228
1229path-key@^2.0.0, path-key@^2.0.1:
1230 version "2.0.1"
1231 resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
1232 integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
1233
1234path-parse@^1.0.6:
1235 version "1.0.6"
1236 resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
1237 integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
1238
1239path-type@^1.0.0:
1240 version "1.1.0"
1241 resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
1242 integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
1243 dependencies:
1244 graceful-fs "^4.1.2"
1245 pify "^2.0.0"
1246 pinkie-promise "^2.0.0"
1247
1248piece-length@^1.0.0:
1249 version "1.0.0"
1250 resolved "https://registry.yarnpkg.com/piece-length/-/piece-length-1.0.0.tgz#4db7167157fd69fef14caf7262cd39f189b24508"
1251 integrity sha1-TbcWcVf9af7xTK9yYs058YmyRQg=
1252 dependencies:
1253 closest-to "~2.0.0"
1254
1255pify@^2.0.0, pify@^2.2.0:
1256 version "2.3.0"
1257 resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
1258 integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
1259
1260pinkie-promise@^2.0.0:
1261 version "2.0.1"
1262 resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
1263 integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
1264 dependencies:
1265 pinkie "^2.0.0"
1266
1267pinkie@^2.0.0:
1268 version "2.0.4"
1269 resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
1270 integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
1271
1272plist-with-patches@0.5.1:
1273 version "0.5.1"
1274 resolved "https://registry.yarnpkg.com/plist-with-patches/-/plist-with-patches-0.5.1.tgz#868aae2e0df8989b026562b35cbc19cfd8bb780d"
1275 integrity sha1-hoquLg34mJsCZWKzXLwZz9i7eA0=
1276 dependencies:
1277 xmlbuilder "0.4.x"
1278 xmldom "0.1.x"
1279
1280prettier-bytes@^1.0.3:
1281 version "1.0.4"
1282 resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6"
1283 integrity sha1-mUsCqkb2mcULYle1+qp/4lV+YtY=
1284
1285process-nextick-args@~2.0.0:
1286 version "2.0.0"
1287 resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
1288 integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
1289
1290protobufjs@^3.2.2:
1291 version "3.8.2"
1292 resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-3.8.2.tgz#bc826e34c3af4697e8d0af7a669e4d612aedcd17"
1293 integrity sha1-vIJuNMOvRpfo0K96Zp5NYSrtzRc=
1294 dependencies:
1295 ascli "~0.3"
1296 bytebuffer "~3 >=3.5"
1297
1298pump@^3.0.0:
1299 version "3.0.0"
1300 resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
1301 integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
1302 dependencies:
1303 end-of-stream "^1.1.0"
1304 once "^1.3.1"
1305
1306qap@^3.1.2:
1307 version "3.3.1"
1308 resolved "https://registry.yarnpkg.com/qap/-/qap-3.3.1.tgz#11f9e8fa8890fe7cb99210c0f44d0613b7372cac"
1309 integrity sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw=
1310
1311random-access-file@^2.0.1:
1312 version "2.1.2"
1313 resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.1.2.tgz#eeb32e50b9831f32060516862381152ae4e05aa6"
1314 integrity sha512-dZo7HEcEPbZ/6XLXC4GXypiWvFbXVkdeMrJTi0B94pBJwddt/AvJh8GaQhso6KGYROGYCI/VWdHbmRDtkwT9pQ==
1315 dependencies:
1316 mkdirp "^0.5.1"
1317 random-access-storage "^1.1.1"
1318
1319random-access-storage@^1.1.1:
1320 version "1.3.0"
1321 resolved "https://registry.yarnpkg.com/random-access-storage/-/random-access-storage-1.3.0.tgz#d27e4d897b79dc4358afc2bbe553044e5c8cfe35"
1322 integrity sha512-pdS9Mcb9TB7oICypPRALlheaSuszuAKmLVEPKJMuYor7R/zDuHh5ALuQoS+ox31XRwQUL+tDwWH2GPdyspwelA==
1323 dependencies:
1324 inherits "^2.0.3"
1325
1326random-iterate@^1.0.1:
1327 version "1.0.1"
1328 resolved "https://registry.yarnpkg.com/random-iterate/-/random-iterate-1.0.1.tgz#f7d97d92dee6665ec5f6da08c7f963cad4b2ac99"
1329 integrity sha1-99l9kt7mZl7F9toIx/ljytSyrJk=
1330
1331randombytes@^2.0.3, randombytes@^2.0.5:
1332 version "2.1.0"
1333 resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
1334 integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
1335 dependencies:
1336 safe-buffer "^5.1.0"
1337
1338range-parser@^1.2.0:
1339 version "1.2.1"
1340 resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
1341 integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
1342
1343range-slice-stream@^2.0.0:
1344 version "2.0.0"
1345 resolved "https://registry.yarnpkg.com/range-slice-stream/-/range-slice-stream-2.0.0.tgz#1f25fc7a2cacf9ccd140c46f9cf670a1a7fe3ce6"
1346 integrity sha512-PPYLwZ63lXi6Tv2EZ8w3M4FzC0rVqvxivaOVS8pXSp5FMIHFnvi4MWHL3UdFLhwSy50aNtJsgjY0mBC6oFL26Q==
1347 dependencies:
1348 readable-stream "^3.0.2"
1349
1350rc@^1.2.7:
1351 version "1.2.8"
1352 resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
1353 integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
1354 dependencies:
1355 deep-extend "^0.6.0"
1356 ini "~1.3.0"
1357 minimist "^1.2.0"
1358 strip-json-comments "~2.0.1"
1359
1360read-pkg-up@^1.0.1:
1361 version "1.0.1"
1362 resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
1363 integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
1364 dependencies:
1365 find-up "^1.0.0"
1366 read-pkg "^1.0.0"
1367
1368read-pkg@^1.0.0:
1369 version "1.1.0"
1370 resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
1371 integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
1372 dependencies:
1373 load-json-file "^1.0.0"
1374 normalize-package-data "^2.3.2"
1375 path-type "^1.0.0"
1376
1377readable-stream@^2.0.3, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.2, readable-stream@^2.3.4:
1378 version "2.3.6"
1379 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
1380 integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
1381 dependencies:
1382 core-util-is "~1.0.0"
1383 inherits "~2.0.3"
1384 isarray "~1.0.0"
1385 process-nextick-args "~2.0.0"
1386 safe-buffer "~5.1.1"
1387 string_decoder "~1.1.1"
1388 util-deprecate "~1.0.1"
1389
1390readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6:
1391 version "3.3.0"
1392 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9"
1393 integrity sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==
1394 dependencies:
1395 inherits "^2.0.3"
1396 string_decoder "^1.1.1"
1397 util-deprecate "^1.0.1"
1398
1399record-cache@^1.0.2:
1400 version "1.1.0"
1401 resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.1.0.tgz#f8a467a691a469584b26e88d36b18afdb3932037"
1402 integrity sha512-u8rbtLEJV7HRacl/ZYwSBFD8NFyB3PfTTfGLP37IW3hftQCwu6z4Q2RLyxo1YJUNRTEzJfpLpGwVuEYdaIkG9Q==
1403
1404render-media@^3.0.0:
1405 version "3.1.3"
1406 resolved "https://registry.yarnpkg.com/render-media/-/render-media-3.1.3.tgz#aa8c8cd3f720049370067180709b551d3c566254"
1407 integrity sha512-K7ziKKlIcgYpAovRsABDiSaNn7TzDDyyuFGpRwM52cloNcajInB6sCxFPUEzOuTJUeyvKCqT/k5INOjpKLCjhQ==
1408 dependencies:
1409 debug "^3.1.0"
1410 is-ascii "^1.0.0"
1411 mediasource "^2.1.0"
1412 stream-to-blob-url "^2.0.0"
1413 videostream "^2.5.1"
1414
1415require-directory@^2.1.1:
1416 version "2.1.1"
1417 resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
1418 integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
1419
1420require-main-filename@^1.0.1:
1421 version "1.0.1"
1422 resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
1423 integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
1424
1425resolve@^1.10.0:
1426 version "1.11.0"
1427 resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232"
1428 integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==
1429 dependencies:
1430 path-parse "^1.0.6"
1431
1432rimraf@^2.4.2, rimraf@^2.6.1:
1433 version "2.6.3"
1434 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
1435 integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
1436 dependencies:
1437 glob "^7.1.3"
1438
1439run-parallel-limit@^1.0.3:
1440 version "1.0.5"
1441 resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.0.5.tgz#c29a4fd17b4df358cb52a8a697811a63c984f1b7"
1442 integrity sha512-NsY+oDngvrvMxKB3G8ijBzIema6aYbQMD2bHOamvN52BysbIGTnEY2xsNyfrcr9GhY995/t/0nQN3R3oZvaDlg==
1443
1444run-parallel@^1.0.0, run-parallel@^1.1.2, run-parallel@^1.1.6:
1445 version "1.1.9"
1446 resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
1447 integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
1448
1449run-series@^1.0.2:
1450 version "1.1.8"
1451 resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.8.tgz#2c4558f49221e01cd6371ff4e0a1e203e460fc36"
1452 integrity sha512-+GztYEPRpIsQoCSraWHDBs9WVy4eVME16zhOtDB4H9J4xN0XRhknnmLOl+4gRgZtu8dpp9N/utSPjKH/xmDzXg==
1453
1454rusha@^0.8.1:
1455 version "0.8.13"
1456 resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a"
1457 integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo=
1458
1459safe-buffer@^5.0.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
1460 version "5.1.2"
1461 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
1462 integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
1463
1464"safer-buffer@>= 2.1.2 < 3":
1465 version "2.1.2"
1466 resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
1467 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
1468
1469sax@1.1.4:
1470 version "1.1.4"
1471 resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.4.tgz#74b6d33c9ae1e001510f179a91168588f1aedaa9"
1472 integrity sha1-dLbTPJrh4AFRDxeakRaFiPGu2qk=
1473
1474sax@>=0.6.0, sax@^1.2.4:
1475 version "1.2.4"
1476 resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
1477 integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
1478
1479"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0:
1480 version "5.7.0"
1481 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
1482 integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
1483
1484semver@~5.1.0:
1485 version "5.1.1"
1486 resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19"
1487 integrity sha1-oykqNz5vPgeY2gsgZBuanFvEfhk=
1488
1489set-blocking@^2.0.0, set-blocking@~2.0.0:
1490 version "2.0.0"
1491 resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
1492 integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
1493
1494shebang-command@^1.2.0:
1495 version "1.2.0"
1496 resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
1497 integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
1498 dependencies:
1499 shebang-regex "^1.0.0"
1500
1501shebang-regex@^1.0.0:
1502 version "1.0.0"
1503 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
1504 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
1505
1506signal-exit@^3.0.0:
1507 version "3.0.2"
1508 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
1509 integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
1510
1511simple-concat@^1.0.0:
1512 version "1.0.0"
1513 resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
1514 integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
1515
1516simple-get@^2.0.0, simple-get@^2.1.0:
1517 version "2.8.1"
1518 resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d"
1519 integrity sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==
1520 dependencies:
1521 decompress-response "^3.3.0"
1522 once "^1.3.1"
1523 simple-concat "^1.0.0"
1524
1525simple-get@^3.0.0, simple-get@^3.0.1:
1526 version "3.0.3"
1527 resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.0.3.tgz#924528ac3f9d7718ce5e9ec1b1a69c0be4d62efa"
1528 integrity sha512-Wvre/Jq5vgoz31Z9stYWPLn0PqRqmBDpFSdypAnHu5AvRVCYPRYGnvryNLiXu8GOBNDH82J2FRHUGMjjHUpXFw==
1529 dependencies:
1530 decompress-response "^3.3.0"
1531 once "^1.3.1"
1532 simple-concat "^1.0.0"
1533
1534simple-peer@^9.0.0:
1535 version "9.3.0"
1536 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.3.0.tgz#85ecb126b23d8730f3904f199db65e84141e0f4e"
1537 integrity sha512-5dLDfrRomrS2LuZUuH2aO7yTGtHFEl5Eb+8ZzqM0KC0lHcYUyJudUomP9ZY/lPUKBx2broL/Eee9bQ53yycEgQ==
1538 dependencies:
1539 debug "^4.0.1"
1540 get-browser-rtc "^1.0.0"
1541 inherits "^2.0.1"
1542 randombytes "^2.0.3"
1543 readable-stream "^2.3.4"
1544
1545simple-sha1@^2.0.0, simple-sha1@^2.0.8, simple-sha1@^2.1.0:
1546 version "2.1.2"
1547 resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-2.1.2.tgz#de40cbd5aae278fde8e3bb3250a35d74c67326b1"
1548 integrity sha512-TQl9rm4rdKAVmhO++sXAb8TNN0D6JAD5iyI1mqEPNpxUzTRrtm4aOG1pDf/5W/qCFihiaoK6uuL9rvQz1x1VKw==
1549 dependencies:
1550 rusha "^0.8.1"
1551
1552simple-websocket@^7.0.1:
1553 version "7.2.0"
1554 resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-7.2.0.tgz#c3190555d74399372b96b51435f2d8c4b04611df"
1555 integrity sha512-wdxFg1fHw1yqFKWDcw+yNb4VIYqtl+vknZMlpLhvZSlR6l7/iVuwozqo+Qtl73mB1IH5QnXzonD1S+hAaLNTvQ==
1556 dependencies:
1557 debug "^3.1.0"
1558 inherits "^2.0.1"
1559 randombytes "^2.0.3"
1560 readable-stream "^2.0.5"
1561 ws "^6.0.0"
1562
1563spdx-correct@^3.0.0:
1564 version "3.1.0"
1565 resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
1566 integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
1567 dependencies:
1568 spdx-expression-parse "^3.0.0"
1569 spdx-license-ids "^3.0.0"
1570
1571spdx-exceptions@^2.1.0:
1572 version "2.2.0"
1573 resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
1574 integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
1575
1576spdx-expression-parse@^3.0.0:
1577 version "3.0.0"
1578 resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
1579 integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
1580 dependencies:
1581 spdx-exceptions "^2.1.0"
1582 spdx-license-ids "^3.0.0"
1583
1584spdx-license-ids@^3.0.0:
1585 version "3.0.4"
1586 resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1"
1587 integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==
1588
1589speedometer@^1.0.0:
1590 version "1.1.0"
1591 resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.1.0.tgz#a30b13abda45687a1a76977012c060f2ac8a7934"
1592 integrity sha512-z/wAiTESw2XVPssY2XRcme4niTc4S5FkkJ4gknudtVoc33Zil8TdTxHy5torRcgqMqksJV2Yz8HQcvtbsnw0mQ==
1593
1594split@^1.0.0:
1595 version "1.0.1"
1596 resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
1597 integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==
1598 dependencies:
1599 through "2"
1600
1601stream-to-blob-url@^2.0.0, stream-to-blob-url@^2.1.0:
1602 version "2.1.1"
1603 resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2"
1604 integrity sha512-DKJPEmCmIZoBfGVle9IhSfERiWaN5cuOtmfPxP2dZbLDRZxkBWZ4QbYxEJOSALk1Kf+WjBgedAMO6qkkf7Lmrg==
1605 dependencies:
1606 stream-to-blob "^1.0.0"
1607
1608stream-to-blob@^1.0.0:
1609 version "1.0.1"
1610 resolved "https://registry.yarnpkg.com/stream-to-blob/-/stream-to-blob-1.0.1.tgz#2dc1e09b71677a234d00445f8eb7ff70c4fe9948"
1611 integrity sha512-aRy4neA4rf+qMtLT9fCRLPGWdrsIKtCx4kUdNTIPgPQ2hkHkdxbViVAvABMx9oRM6yCWfngHx6pwXfbYkVuPuw==
1612 dependencies:
1613 once "^1.3.3"
1614
1615stream-with-known-length-to-buffer@^1.0.0:
1616 version "1.0.2"
1617 resolved "https://registry.yarnpkg.com/stream-with-known-length-to-buffer/-/stream-with-known-length-to-buffer-1.0.2.tgz#b8ea5a92086a1ed5d27fc4c529636682118c945b"
1618 integrity sha512-UxSISjxmguvfYzZdq6d4XAjc3gAocqTIOS1CjgwkDkkGT/LMTsIYiV8agIw42IHFFHf8k4lPOoroCCf4W9oqzg==
1619 dependencies:
1620 once "^1.3.3"
1621
1622string-width@^1.0.1, string-width@^1.0.2:
1623 version "1.0.2"
1624 resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
1625 integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
1626 dependencies:
1627 code-point-at "^1.0.0"
1628 is-fullwidth-code-point "^1.0.0"
1629 strip-ansi "^3.0.0"
1630
1631"string-width@^1.0.2 || 2":
1632 version "2.1.1"
1633 resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
1634 integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
1635 dependencies:
1636 is-fullwidth-code-point "^2.0.0"
1637 strip-ansi "^4.0.0"
1638
1639string2compact@^1.1.1, string2compact@^1.2.5:
1640 version "1.3.0"
1641 resolved "https://registry.yarnpkg.com/string2compact/-/string2compact-1.3.0.tgz#22d946127b082d1203c51316af60117a337423c3"
1642 integrity sha512-004ulKKANDuQilQsNxy2lisrpMG0qUJxBU+2YCEF7KziRyNR0Nredm2qk0f1V82nva59H3y9GWeHXE63HzGRFw==
1643 dependencies:
1644 addr-to-ip-port "^1.0.1"
1645 ipaddr.js "^1.0.1"
1646
1647string_decoder@^1.1.1:
1648 version "1.2.0"
1649 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
1650 integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
1651 dependencies:
1652 safe-buffer "~5.1.0"
1653
1654string_decoder@~1.1.1:
1655 version "1.1.1"
1656 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
1657 integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
1658 dependencies:
1659 safe-buffer "~5.1.0"
1660
1661strip-ansi@^3.0.0, strip-ansi@^3.0.1:
1662 version "3.0.1"
1663 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
1664 integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
1665 dependencies:
1666 ansi-regex "^2.0.0"
1667
1668strip-ansi@^4.0.0:
1669 version "4.0.0"
1670 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
1671 integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
1672 dependencies:
1673 ansi-regex "^3.0.0"
1674
1675strip-bom@^2.0.0:
1676 version "2.0.0"
1677 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
1678 integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
1679 dependencies:
1680 is-utf8 "^0.2.0"
1681
1682strip-eof@^1.0.0:
1683 version "1.0.0"
1684 resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
1685 integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
1686
1687strip-json-comments@~2.0.1:
1688 version "2.0.1"
1689 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
1690 integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
1691
1692tar@^4:
1693 version "4.4.8"
1694 resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
1695 integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
1696 dependencies:
1697 chownr "^1.1.1"
1698 fs-minipass "^1.2.5"
1699 minipass "^2.3.4"
1700 minizlib "^1.1.1"
1701 mkdirp "^0.5.0"
1702 safe-buffer "^5.1.2"
1703 yallist "^3.0.2"
1704
1705thirty-two@^1.0.1:
1706 version "1.0.2"
1707 resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
1708 integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno=
1709
1710through@2:
1711 version "2.3.8"
1712 resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
1713 integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
1714
1715thunky@^0.1.0:
1716 version "0.1.0"
1717 resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
1718 integrity sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=
1719
1720thunky@^1.0.1, thunky@^1.0.2:
1721 version "1.0.3"
1722 resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826"
1723 integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==
1724
1725to-arraybuffer@^1.0.1:
1726 version "1.0.1"
1727 resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
1728 integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
1729
1730torrent-discovery@^9.1.1:
1731 version "9.1.1"
1732 resolved "https://registry.yarnpkg.com/torrent-discovery/-/torrent-discovery-9.1.1.tgz#56704e6747b24fe00dbb75b442d202051f78d37d"
1733 integrity sha512-3mHf+bxVCVLrlkPJdAoMbPMY1hpTZVeWw5hNc2pPFm+HCc2DS0HgVFTBTSWtB8vQPWA1hSEZpqJ+3QfdXxDE1g==
1734 dependencies:
1735 bittorrent-dht "^9.0.0"
1736 bittorrent-tracker "^9.0.0"
1737 debug "^3.1.0"
1738 run-parallel "^1.1.2"
1739
1740torrent-piece@^2.0.0:
1741 version "2.0.0"
1742 resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.0.tgz#6598ae67d93699e887f178db267ba16d89d7ec9b"
1743 integrity sha512-H/Z/yCuvZJj1vl1IQHI8dvF2QrUuXRJoptT5DW5967/dsLpXlCg+uyhFR5lfNj5mNaYePUbKtnL+qKWZGXv4Nw==
1744
1745typedarray-to-buffer@^3.0.0:
1746 version "3.1.5"
1747 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
1748 integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
1749 dependencies:
1750 is-typedarray "^1.0.0"
1751
1752typedarray@^0.0.6:
1753 version "0.0.6"
1754 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
1755 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
1756
1757uint64be@^2.0.2:
1758 version "2.0.2"
1759 resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-2.0.2.tgz#ef4a179752fe8f9ddaa29544ecfc13490031e8e5"
1760 integrity sha512-9QqdvpGQTXgxthP+lY4e/gIBy+RuqcBaC6JVwT5I3bDLgT/btL6twZMR0pI3/Fgah9G/pdwzIprE5gL6v9UvyQ==
1761 dependencies:
1762 buffer-alloc "^1.1.0"
1763
1764uniq@^1.0.1:
1765 version "1.0.1"
1766 resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
1767 integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
1768
1769unordered-array-remove@^1.0.2:
1770 version "1.0.2"
1771 resolved "https://registry.yarnpkg.com/unordered-array-remove/-/unordered-array-remove-1.0.2.tgz#c546e8f88e317a0cf2644c97ecb57dba66d250ef"
1772 integrity sha1-xUbo+I4xegzyZEyX7LV9umbSUO8=
1773
1774upnp-device-client@^1.0.0:
1775 version "1.0.2"
1776 resolved "https://registry.yarnpkg.com/upnp-device-client/-/upnp-device-client-1.0.2.tgz#91f84705f2349bf89082855fff4e3006ac435337"
1777 integrity sha1-kfhHBfI0m/iQgoVf/04wBqxDUzc=
1778 dependencies:
1779 concat-stream "^1.4.8"
1780 debug "^2.1.3"
1781 elementtree "~0.1.6"
1782 network-address "^1.0.0"
1783
1784upnp-mediarenderer-client@^1.2.2:
1785 version "1.2.4"
1786 resolved "https://registry.yarnpkg.com/upnp-mediarenderer-client/-/upnp-mediarenderer-client-1.2.4.tgz#0c63a51802082b6b03b596c475cc64fc1e0877c8"
1787 integrity sha1-DGOlGAIIK2sDtZbEdcxk/B4Id8g=
1788 dependencies:
1789 debug "^2.1.3"
1790 elementtree "^0.1.6"
1791 upnp-device-client "^1.0.0"
1792
1793url-join@^2.0.5:
1794 version "2.0.5"
1795 resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
1796 integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=
1797
1798ut_metadata@^3.3.0:
1799 version "3.3.0"
1800 resolved "https://registry.yarnpkg.com/ut_metadata/-/ut_metadata-3.3.0.tgz#a0e0e861ebc39ed96e506601d1463ade3b548a7e"
1801 integrity sha512-IK+ke9yL6a4oPLz/3oSW9TW7m9Wr4RG+5kW5aS2YulzEU1QDGAtago/NnOlno91fo3fSO7mnsqzn3NXNXdv8nA==
1802 dependencies:
1803 bencode "^2.0.0"
1804 bitfield "^2.0.0"
1805 debug "^3.1.0"
1806 simple-sha1 "^2.0.0"
1807
1808ut_pex@^1.1.1:
1809 version "1.2.1"
1810 resolved "https://registry.yarnpkg.com/ut_pex/-/ut_pex-1.2.1.tgz#472ed0ea5e9bbc9148b833339d56d7b17cf3dad0"
1811 integrity sha512-ZrxMCbffYtxQDqvREN9kBXK2CB9tPnd5PylHoqQX9ai+3HV9/S39FnA5JnhLOC82dxIQQg0nTN2wmhtAdGNtOA==
1812 dependencies:
1813 bencode "^2.0.0"
1814 compact2string "^1.2.0"
1815 inherits "^2.0.1"
1816 string2compact "^1.2.5"
1817
1818utf-8-validate@^5.0.1:
1819 version "5.0.2"
1820 resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.2.tgz#63cfbccd85dc1f2b66cf7a1d0eebc08ed056bfb3"
1821 integrity sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==
1822 dependencies:
1823 node-gyp-build "~3.7.0"
1824
1825util-deprecate@^1.0.1, util-deprecate@~1.0.1:
1826 version "1.0.2"
1827 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
1828 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
1829
1830validate-npm-package-license@^3.0.1:
1831 version "3.0.4"
1832 resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
1833 integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
1834 dependencies:
1835 spdx-correct "^3.0.0"
1836 spdx-expression-parse "^3.0.0"
1837
1838videostream@^2.5.1:
1839 version "2.6.0"
1840 resolved "https://registry.yarnpkg.com/videostream/-/videostream-2.6.0.tgz#7f0b2b84bc457c12cfe599aa2345f5cc06241ab6"
1841 integrity sha512-nSsullx1BYClJxVSt4Fa+Ulsv0Cf7UwaHq+4LQdLkAUdmqNhY1DlGxXDWVY2gui5XV4FvDiSbXmSbGryMrrUCQ==
1842 dependencies:
1843 binary-search "^1.3.4"
1844 inherits "^2.0.1"
1845 mediasource "^2.2.2"
1846 mp4-box-encoding "^1.3.0"
1847 mp4-stream "^2.0.0"
1848 multistream "^2.0.2"
1849 pump "^3.0.0"
1850 range-slice-stream "^2.0.0"
1851
1852vlc-command@^1.0.0:
1853 version "1.1.2"
1854 resolved "https://registry.yarnpkg.com/vlc-command/-/vlc-command-1.1.2.tgz#61a9b4249a0001c0bcac8cdaf36d3a8e674cffce"
1855 integrity sha512-KZ15RTHz96OEiQDA8oNFn1edYDWyKJIWI4gF74Am9woZo5XmVYryk5RYXSwOMvsaAgL5ejICEGCl0suQyDBu+Q==
1856 dependencies:
1857 run-parallel "^1.1.6"
1858 winreg "^1.2.1"
1859
1860webidl-conversions@^4.0.2:
1861 version "4.0.2"
1862 resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
1863 integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
1864
1865webtorrent-cli@^1.12.3:
1866 version "1.12.3"
1867 resolved "https://registry.yarnpkg.com/webtorrent-cli/-/webtorrent-cli-1.12.3.tgz#e6a1060cd3f104346da91e67763276ca897f238c"
1868 integrity sha512-NnBAGkD64CRsl9edM9q0QU+ku6nCX32nM0U+YC8Gs/36c8y+5m9Tya3mWIux3oZKZ54yGiVtnok4tUpqDE5tMA==
1869 dependencies:
1870 clivas "^0.2.0"
1871 create-torrent "^3.23.1"
1872 dlnacasts "^0.1.0"
1873 ecstatic "^3.0.0"
1874 executable "^4.0.0"
1875 mime "^2.1.0"
1876 minimist "^1.2.0"
1877 moment "^2.12.0"
1878 network-address "^1.1.0"
1879 open "0.0.5"
1880 parse-torrent "^6.0.0"
1881 prettier-bytes "^1.0.3"
1882 vlc-command "^1.0.0"
1883 webtorrent "0.x"
1884 winreg "^1.0.1"
1885 optionalDependencies:
1886 airplay-js "^0.3.0"
1887 chromecasts "^1.5.3"
1888 nodebmc "0.0.7"
1889
1890webtorrent-hybrid@^2.1.0:
1891 version "2.1.0"
1892 resolved "https://registry.yarnpkg.com/webtorrent-hybrid/-/webtorrent-hybrid-2.1.0.tgz#c14d33d6769667d8ae524ca2d9dfdcd18d4cfbf2"
1893 integrity sha512-S8tUgUbPLwGazPrBMTqjsuxlmhaCZaiC+KlgS7ECRGaHVVZTJjKG2kUw8uf558DdZbsEA3jNxOOsMvXiA62sFw==
1894 dependencies:
1895 create-torrent "^3.33.0"
1896 webtorrent "^0.x"
1897 webtorrent-cli "^1.12.3"
1898 wrtc "^0.3.3"
1899
1900webtorrent@0.x, webtorrent@^0.x:
1901 version "0.103.1"
1902 resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.103.1.tgz#18ead369bbcaa60dc8ea138784c33451edd34479"
1903 integrity sha512-rqMD8sAaPzrUzEpA6gDEOgcN9Xyz4WGrQiHoVY6HlymLoNSkScMnmnEX1bsdMcIU9iOrnUlghDEW9sJ9jYCmwQ==
1904 dependencies:
1905 addr-to-ip-port "^1.4.2"
1906 bitfield "^2.0.0"
1907 bittorrent-dht "^9.0.0"
1908 bittorrent-protocol "^3.0.0"
1909 chunk-store-stream "^3.0.1"
1910 create-torrent "^3.33.0"
1911 debug "^4.1.0"
1912 end-of-stream "^1.1.0"
1913 fs-chunk-store "^1.6.2"
1914 immediate-chunk-store "^2.0.0"
1915 load-ip-set "^2.1.0"
1916 memory-chunk-store "^1.2.0"
1917 mime "^2.4.0"
1918 multistream "^2.0.5"
1919 package-json-versionify "^1.0.2"
1920 parse-numeric-range "^0.0.2"
1921 parse-torrent "^6.1.2"
1922 pump "^3.0.0"
1923 random-iterate "^1.0.1"
1924 randombytes "^2.0.3"
1925 range-parser "^1.2.0"
1926 readable-stream "^3.0.6"
1927 render-media "^3.0.0"
1928 run-parallel "^1.1.6"
1929 run-parallel-limit "^1.0.3"
1930 safe-buffer "^5.0.1"
1931 simple-concat "^1.0.0"
1932 simple-get "^3.0.1"
1933 simple-peer "^9.0.0"
1934 simple-sha1 "^2.0.8"
1935 speedometer "^1.0.0"
1936 stream-to-blob "^1.0.0"
1937 stream-to-blob-url "^2.1.0"
1938 stream-with-known-length-to-buffer "^1.0.0"
1939 torrent-discovery "^9.1.1"
1940 torrent-piece "^2.0.0"
1941 uniq "^1.0.1"
1942 unordered-array-remove "^1.0.2"
1943 ut_metadata "^3.3.0"
1944 ut_pex "^1.1.1"
1945
1946which-module@^1.0.0:
1947 version "1.0.0"
1948 resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
1949 integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=
1950
1951which@^1.2.14, which@^1.2.9:
1952 version "1.3.1"
1953 resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
1954 integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
1955 dependencies:
1956 isexe "^2.0.0"
1957
1958wide-align@^1.1.0:
1959 version "1.1.3"
1960 resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
1961 integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
1962 dependencies:
1963 string-width "^1.0.2 || 2"
1964
1965winreg@^1.0.1, winreg@^1.2.1:
1966 version "1.2.4"
1967 resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b"
1968 integrity sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=
1969
1970wrap-ansi@^2.0.0:
1971 version "2.1.0"
1972 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
1973 integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
1974 dependencies:
1975 string-width "^1.0.1"
1976 strip-ansi "^3.0.1"
1977
1978wrappy@1:
1979 version "1.0.2"
1980 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
1981 integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
1982
1983wrtc@^0.3.3:
1984 version "0.3.7"
1985 resolved "https://registry.yarnpkg.com/wrtc/-/wrtc-0.3.7.tgz#2279f1cb3a83573e77b3d9b7e720071fab2ae4af"
1986 integrity sha512-mDFNFqAB+3IYVKlP15vpGD0EhXjsQlj/GLNje4KLpClLSq8pyTG0xqJFoU+Oq43XvDIUMmIJ/r1aNfrjT7KUQw==
1987 dependencies:
1988 nan "^2.3.2"
1989 node-cmake "2.3.2"
1990 node-pre-gyp "0.11.x"
1991 optionalDependencies:
1992 domexception "^1.0.1"
1993
1994ws@^6.0.0:
1995 version "6.2.1"
1996 resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
1997 integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
1998 dependencies:
1999 async-limiter "~1.0.0"
2000
2001xml2js@^0.4.8:
2002 version "0.4.19"
2003 resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
2004 integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
2005 dependencies:
2006 sax ">=0.6.0"
2007 xmlbuilder "~9.0.1"
2008
2009xmlbuilder@0.4.x:
2010 version "0.4.3"
2011 resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58"
2012 integrity sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=
2013
2014xmlbuilder@~9.0.1:
2015 version "9.0.7"
2016 resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
2017 integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
2018
2019xmldom@0.1.x:
2020 version "0.1.27"
2021 resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
2022 integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
2023
2024xtend@^4.0.0, xtend@^4.0.1:
2025 version "4.0.1"
2026 resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
2027 integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
2028
2029y18n@^3.2.1:
2030 version "3.2.1"
2031 resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
2032 integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
2033
2034yallist@^3.0.0, yallist@^3.0.2:
2035 version "3.0.3"
2036 resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
2037 integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
2038
2039yargs-parser@^5.0.0:
2040 version "5.0.0"
2041 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
2042 integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=
2043 dependencies:
2044 camelcase "^3.0.0"
2045
2046yargs@^7.0.2:
2047 version "7.1.0"
2048 resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
2049 integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=
2050 dependencies:
2051 camelcase "^3.0.0"
2052 cliui "^3.2.0"
2053 decamelize "^1.1.1"
2054 get-caller-file "^1.0.1"
2055 os-locale "^1.4.0"
2056 read-pkg-up "^1.0.1"
2057 require-directory "^2.1.1"
2058 require-main-filename "^1.0.1"
2059 set-blocking "^2.0.0"
2060 string-width "^1.0.2"
2061 which-module "^1.0.0"
2062 y18n "^3.2.1"
2063 yargs-parser "^5.0.0"