diff options
Diffstat (limited to 'server/controllers/api')
22 files changed, 742 insertions, 468 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 2ddb73519..c9b5c8047 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { ServerConfigManager } from '@server/lib/server-config-manager' | ||
1 | import * as express from 'express' | 2 | import * as express from 'express' |
2 | import { remove, writeJSON } from 'fs-extra' | 3 | import { remove, writeJSON } from 'fs-extra' |
3 | import { snakeCase } from 'lodash' | 4 | import { snakeCase } from 'lodash' |
4 | import validator from 'validator' | 5 | import validator from 'validator' |
5 | import { getServerConfig } from '@server/lib/config' | ||
6 | import { UserRight } from '../../../shared' | 6 | import { UserRight } from '../../../shared' |
7 | import { About } from '../../../shared/models/server/about.model' | 7 | import { About } from '../../../shared/models/server/about.model' |
8 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 8 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
@@ -18,6 +18,7 @@ const configRouter = express.Router() | |||
18 | const auditLogger = auditLoggerFactory('config') | 18 | const auditLogger = auditLoggerFactory('config') |
19 | 19 | ||
20 | configRouter.get('/about', getAbout) | 20 | configRouter.get('/about', getAbout) |
21 | |||
21 | configRouter.get('/', | 22 | configRouter.get('/', |
22 | asyncMiddleware(getConfig) | 23 | asyncMiddleware(getConfig) |
23 | ) | 24 | ) |
@@ -27,12 +28,14 @@ configRouter.get('/custom', | |||
27 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), | 28 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), |
28 | getCustomConfig | 29 | getCustomConfig |
29 | ) | 30 | ) |
31 | |||
30 | configRouter.put('/custom', | 32 | configRouter.put('/custom', |
31 | authenticate, | 33 | authenticate, |
32 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), | 34 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), |
33 | customConfigUpdateValidator, | 35 | customConfigUpdateValidator, |
34 | asyncMiddleware(updateCustomConfig) | 36 | asyncMiddleware(updateCustomConfig) |
35 | ) | 37 | ) |
38 | |||
36 | configRouter.delete('/custom', | 39 | configRouter.delete('/custom', |
37 | authenticate, | 40 | authenticate, |
38 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), | 41 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), |
@@ -40,7 +43,7 @@ configRouter.delete('/custom', | |||
40 | ) | 43 | ) |
41 | 44 | ||
42 | async function getConfig (req: express.Request, res: express.Response) { | 45 | async function getConfig (req: express.Request, res: express.Response) { |
43 | const json = await getServerConfig(req.ip) | 46 | const json = await ServerConfigManager.Instance.getServerConfig(req.ip) |
44 | 47 | ||
45 | return res.json(json) | 48 | return res.json(json) |
46 | } | 49 | } |
@@ -67,13 +70,13 @@ function getAbout (req: express.Request, res: express.Response) { | |||
67 | } | 70 | } |
68 | } | 71 | } |
69 | 72 | ||
70 | return res.json(about).end() | 73 | return res.json(about) |
71 | } | 74 | } |
72 | 75 | ||
73 | function getCustomConfig (req: express.Request, res: express.Response) { | 76 | function getCustomConfig (req: express.Request, res: express.Response) { |
74 | const data = customConfig() | 77 | const data = customConfig() |
75 | 78 | ||
76 | return res.json(data).end() | 79 | return res.json(data) |
77 | } | 80 | } |
78 | 81 | ||
79 | async function deleteCustomConfig (req: express.Request, res: express.Response) { | 82 | async function deleteCustomConfig (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/custom-page.ts b/server/controllers/api/custom-page.ts new file mode 100644 index 000000000..3c47f7b9a --- /dev/null +++ b/server/controllers/api/custom-page.ts | |||
@@ -0,0 +1,42 @@ | |||
1 | import * as express from 'express' | ||
2 | import { ServerConfigManager } from '@server/lib/server-config-manager' | ||
3 | import { ActorCustomPageModel } from '@server/models/account/actor-custom-page' | ||
4 | import { HttpStatusCode } from '@shared/core-utils' | ||
5 | import { UserRight } from '@shared/models' | ||
6 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' | ||
7 | |||
8 | const customPageRouter = express.Router() | ||
9 | |||
10 | customPageRouter.get('/homepage/instance', | ||
11 | asyncMiddleware(getInstanceHomepage) | ||
12 | ) | ||
13 | |||
14 | customPageRouter.put('/homepage/instance', | ||
15 | authenticate, | ||
16 | ensureUserHasRight(UserRight.MANAGE_INSTANCE_CUSTOM_PAGE), | ||
17 | asyncMiddleware(updateInstanceHomepage) | ||
18 | ) | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | customPageRouter | ||
24 | } | ||
25 | |||
26 | // --------------------------------------------------------------------------- | ||
27 | |||
28 | async function getInstanceHomepage (req: express.Request, res: express.Response) { | ||
29 | const page = await ActorCustomPageModel.loadInstanceHomepage() | ||
30 | if (!page) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | ||
31 | |||
32 | return res.json(page.toFormattedJSON()) | ||
33 | } | ||
34 | |||
35 | async function updateInstanceHomepage (req: express.Request, res: express.Response) { | ||
36 | const content = req.body.content | ||
37 | |||
38 | await ActorCustomPageModel.updateInstanceHomepage(content) | ||
39 | ServerConfigManager.Instance.updateHomepageState(content) | ||
40 | |||
41 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
42 | } | ||
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 4f4561ffd..9ffcf1337 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts | |||
@@ -8,6 +8,7 @@ import { abuseRouter } from './abuse' | |||
8 | import { accountsRouter } from './accounts' | 8 | import { accountsRouter } from './accounts' |
9 | import { bulkRouter } from './bulk' | 9 | import { bulkRouter } from './bulk' |
10 | import { configRouter } from './config' | 10 | import { configRouter } from './config' |
11 | import { customPageRouter } from './custom-page' | ||
11 | import { jobsRouter } from './jobs' | 12 | import { jobsRouter } from './jobs' |
12 | import { oauthClientsRouter } from './oauth-clients' | 13 | import { oauthClientsRouter } from './oauth-clients' |
13 | import { overviewsRouter } from './overviews' | 14 | import { overviewsRouter } from './overviews' |
@@ -49,6 +50,7 @@ apiRouter.use('/jobs', jobsRouter) | |||
49 | apiRouter.use('/search', searchRouter) | 50 | apiRouter.use('/search', searchRouter) |
50 | apiRouter.use('/overviews', overviewsRouter) | 51 | apiRouter.use('/overviews', overviewsRouter) |
51 | apiRouter.use('/plugins', pluginRouter) | 52 | apiRouter.use('/plugins', pluginRouter) |
53 | apiRouter.use('/custom-pages', customPageRouter) | ||
52 | apiRouter.use('/ping', pong) | 54 | apiRouter.use('/ping', pong) |
53 | apiRouter.use('/*', badRequest) | 55 | apiRouter.use('/*', badRequest) |
54 | 56 | ||
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index a186de010..e18eed332 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts | |||
@@ -1,16 +1,18 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { getFormattedObjects } from '../../helpers/utils' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { getFormattedObjects } from '@server/helpers/utils' | ||
4 | import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index' | ||
5 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
3 | import { | 6 | import { |
4 | asyncMiddleware, | 7 | asyncMiddleware, |
5 | authenticate, | 8 | authenticate, |
9 | availablePluginsSortValidator, | ||
6 | ensureUserHasRight, | 10 | ensureUserHasRight, |
7 | paginationValidator, | 11 | paginationValidator, |
12 | pluginsSortValidator, | ||
8 | setDefaultPagination, | 13 | setDefaultPagination, |
9 | setDefaultSort | 14 | setDefaultSort |
10 | } from '../../middlewares' | 15 | } from '@server/middlewares' |
11 | import { availablePluginsSortValidator, pluginsSortValidator } from '../../middlewares/validators' | ||
12 | import { PluginModel } from '../../models/server/plugin' | ||
13 | import { UserRight } from '../../../shared/models/users' | ||
14 | import { | 16 | import { |
15 | existingPluginValidator, | 17 | existingPluginValidator, |
16 | installOrUpdatePluginValidator, | 18 | installOrUpdatePluginValidator, |
@@ -18,16 +20,17 @@ import { | |||
18 | listPluginsValidator, | 20 | listPluginsValidator, |
19 | uninstallPluginValidator, | 21 | uninstallPluginValidator, |
20 | updatePluginSettingsValidator | 22 | updatePluginSettingsValidator |
21 | } from '../../middlewares/validators/plugins' | 23 | } from '@server/middlewares/validators/plugins' |
22 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 24 | import { PluginModel } from '@server/models/server/plugin' |
23 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' | 25 | import { HttpStatusCode } from '@shared/core-utils' |
24 | import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' | 26 | import { |
25 | import { logger } from '../../helpers/logger' | 27 | InstallOrUpdatePlugin, |
26 | import { listAvailablePluginsFromIndex } from '../../lib/plugins/plugin-index' | 28 | ManagePlugin, |
27 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | 29 | PeertubePluginIndexList, |
28 | import { RegisteredServerSettings } from '../../../shared/models/plugins/register-server-setting.model' | 30 | PublicServerSetting, |
29 | import { PublicServerSetting } from '../../../shared/models/plugins/public-server.setting' | 31 | RegisteredServerSettings, |
30 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 32 | UserRight |
33 | } from '@shared/models' | ||
31 | 34 | ||
32 | const pluginRouter = express.Router() | 35 | const pluginRouter = express.Router() |
33 | 36 | ||
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts index 7787186be..ff0d9ca3c 100644 --- a/server/controllers/api/server/debug.ts +++ b/server/controllers/api/server/debug.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' | 1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' |
2 | import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' | ||
3 | import { SendDebugCommand } from '@shared/models' | ||
2 | import * as express from 'express' | 4 | import * as express from 'express' |
3 | import { UserRight } from '../../../../shared/models/users' | 5 | import { UserRight } from '../../../../shared/models/users' |
4 | import { authenticate, ensureUserHasRight } from '../../../middlewares' | 6 | import { authenticate, ensureUserHasRight } from '../../../middlewares' |
@@ -11,6 +13,12 @@ debugRouter.get('/debug', | |||
11 | getDebug | 13 | getDebug |
12 | ) | 14 | ) |
13 | 15 | ||
16 | debugRouter.post('/debug/run-command', | ||
17 | authenticate, | ||
18 | ensureUserHasRight(UserRight.MANAGE_DEBUG), | ||
19 | runCommand | ||
20 | ) | ||
21 | |||
14 | // --------------------------------------------------------------------------- | 22 | // --------------------------------------------------------------------------- |
15 | 23 | ||
16 | export { | 24 | export { |
@@ -25,3 +33,13 @@ function getDebug (req: express.Request, res: express.Response) { | |||
25 | activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting() | 33 | activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting() |
26 | }) | 34 | }) |
27 | } | 35 | } |
36 | |||
37 | async function runCommand (req: express.Request, res: express.Response) { | ||
38 | const body: SendDebugCommand = req.body | ||
39 | |||
40 | if (body.command === 'remove-dandling-resumable-uploads') { | ||
41 | await RemoveDanglingResumableUploadsScheduler.Instance.execute() | ||
42 | } | ||
43 | |||
44 | return res.sendStatus(204) | ||
45 | } | ||
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 80025bc5b..daeef22de 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -1,9 +1,15 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
2 | import { UserRight } from '../../../../shared/models/users' | 4 | import { UserRight } from '../../../../shared/models/users' |
3 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
4 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { getFormattedObjects } from '../../../helpers/utils' |
5 | import { SERVER_ACTOR_NAME } from '../../../initializers/constants' | 7 | import { SERVER_ACTOR_NAME } from '../../../initializers/constants' |
8 | import { sequelizeTypescript } from '../../../initializers/database' | ||
9 | import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' | ||
6 | import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' | 10 | import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' |
11 | import { JobQueue } from '../../../lib/job-queue' | ||
12 | import { removeRedundanciesOfServer } from '../../../lib/redundancy' | ||
7 | import { | 13 | import { |
8 | asyncMiddleware, | 14 | asyncMiddleware, |
9 | authenticate, | 15 | authenticate, |
@@ -19,16 +25,10 @@ import { | |||
19 | followingSortValidator, | 25 | followingSortValidator, |
20 | followValidator, | 26 | followValidator, |
21 | getFollowerValidator, | 27 | getFollowerValidator, |
22 | removeFollowingValidator, | 28 | listFollowsValidator, |
23 | listFollowsValidator | 29 | removeFollowingValidator |
24 | } from '../../../middlewares/validators' | 30 | } from '../../../middlewares/validators' |
25 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 31 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
26 | import { JobQueue } from '../../../lib/job-queue' | ||
27 | import { removeRedundanciesOfServer } from '../../../lib/redundancy' | ||
28 | import { sequelizeTypescript } from '../../../initializers/database' | ||
29 | import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' | ||
30 | import { getServerActor } from '@server/models/application/application' | ||
31 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
32 | 32 | ||
33 | const serverFollowsRouter = express.Router() | 33 | const serverFollowsRouter = express.Router() |
34 | serverFollowsRouter.get('/following', | 34 | serverFollowsRouter.get('/following', |
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts index 6e341c0fb..a86bc7d19 100644 --- a/server/controllers/api/server/server-blocklist.ts +++ b/server/controllers/api/server/server-blocklist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | import { UserNotificationModel } from '@server/models/account/user-notification' | 4 | import { UserNotificationModel } from '@server/models/user/user-notification' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { UserRight } from '../../../../shared/models/users' | 6 | import { UserRight } from '../../../../shared/models/users' |
7 | import { getFormattedObjects } from '../../../helpers/utils' | 7 | import { getFormattedObjects } from '../../../helpers/utils' |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index e2b1ea7cd..f384f0f28 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -45,7 +45,7 @@ import { | |||
45 | usersResetPasswordValidator, | 45 | usersResetPasswordValidator, |
46 | usersVerifyEmailValidator | 46 | usersVerifyEmailValidator |
47 | } from '../../../middlewares/validators' | 47 | } from '../../../middlewares/validators' |
48 | import { UserModel } from '../../../models/account/user' | 48 | import { UserModel } from '../../../models/user/user' |
49 | import { meRouter } from './me' | 49 | import { meRouter } from './me' |
50 | import { myAbusesRouter } from './my-abuses' | 50 | import { myAbusesRouter } from './my-abuses' |
51 | import { myBlocklistRouter } from './my-blocklist' | 51 | import { myBlocklistRouter } from './my-blocklist' |
@@ -323,14 +323,20 @@ async function updateUser (req: express.Request, res: express.Response) { | |||
323 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) | 323 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) |
324 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role | 324 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role |
325 | 325 | ||
326 | if (body.password !== undefined) userToUpdate.password = body.password | 326 | const keysToUpdate: (keyof UserUpdate)[] = [ |
327 | if (body.email !== undefined) userToUpdate.email = body.email | 327 | 'password', |
328 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified | 328 | 'email', |
329 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota | 329 | 'emailVerified', |
330 | if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily | 330 | 'videoQuota', |
331 | if (body.role !== undefined) userToUpdate.role = body.role | 331 | 'videoQuotaDaily', |
332 | if (body.adminFlags !== undefined) userToUpdate.adminFlags = body.adminFlags | 332 | 'role', |
333 | if (body.pluginAuth !== undefined) userToUpdate.pluginAuth = body.pluginAuth | 333 | 'adminFlags', |
334 | 'pluginAuth' | ||
335 | ] | ||
336 | |||
337 | for (const key of keysToUpdate) { | ||
338 | if (body[key] !== undefined) userToUpdate.set(key, body[key]) | ||
339 | } | ||
334 | 340 | ||
335 | const user = await userToUpdate.save() | 341 | const user = await userToUpdate.save() |
336 | 342 | ||
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 0763d1900..a609abaa6 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -28,9 +28,10 @@ import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } fro | |||
28 | import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' | 28 | import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' |
29 | import { AccountModel } from '../../../models/account/account' | 29 | import { AccountModel } from '../../../models/account/account' |
30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
31 | import { UserModel } from '../../../models/account/user' | 31 | import { UserModel } from '../../../models/user/user' |
32 | import { VideoModel } from '../../../models/video/video' | 32 | import { VideoModel } from '../../../models/video/video' |
33 | import { VideoImportModel } from '../../../models/video/video-import' | 33 | import { VideoImportModel } from '../../../models/video/video-import' |
34 | import { AttributesOnly } from '@shared/core-utils' | ||
34 | 35 | ||
35 | const auditLogger = auditLoggerFactory('users') | 36 | const auditLogger = auditLoggerFactory('users') |
36 | 37 | ||
@@ -191,17 +192,23 @@ async function updateMe (req: express.Request, res: express.Response) { | |||
191 | 192 | ||
192 | const user = res.locals.oauth.token.user | 193 | const user = res.locals.oauth.token.user |
193 | 194 | ||
194 | if (body.password !== undefined) user.password = body.password | 195 | const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly<UserModel>)[] = [ |
195 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy | 196 | 'password', |
196 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled | 197 | 'nsfwPolicy', |
197 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo | 198 | 'webTorrentEnabled', |
198 | if (body.autoPlayNextVideo !== undefined) user.autoPlayNextVideo = body.autoPlayNextVideo | 199 | 'autoPlayVideo', |
199 | if (body.autoPlayNextVideoPlaylist !== undefined) user.autoPlayNextVideoPlaylist = body.autoPlayNextVideoPlaylist | 200 | 'autoPlayNextVideo', |
200 | if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled | 201 | 'autoPlayNextVideoPlaylist', |
201 | if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages | 202 | 'videosHistoryEnabled', |
202 | if (body.theme !== undefined) user.theme = body.theme | 203 | 'videoLanguages', |
203 | if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal | 204 | 'theme', |
204 | if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal | 205 | 'noInstanceConfigWarningModal', |
206 | 'noWelcomeModal' | ||
207 | ] | ||
208 | |||
209 | for (const key of keysToUpdate) { | ||
210 | if (body[key] !== undefined) user.set(key, body[key]) | ||
211 | } | ||
205 | 212 | ||
206 | if (body.email !== undefined) { | 213 | if (body.email !== undefined) { |
207 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | 214 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { |
@@ -215,15 +222,15 @@ async function updateMe (req: express.Request, res: express.Response) { | |||
215 | await sequelizeTypescript.transaction(async t => { | 222 | await sequelizeTypescript.transaction(async t => { |
216 | await user.save({ transaction: t }) | 223 | await user.save({ transaction: t }) |
217 | 224 | ||
218 | if (body.displayName !== undefined || body.description !== undefined) { | 225 | if (body.displayName === undefined && body.description === undefined) return |
219 | const userAccount = await AccountModel.load(user.Account.id, t) | ||
220 | 226 | ||
221 | if (body.displayName !== undefined) userAccount.name = body.displayName | 227 | const userAccount = await AccountModel.load(user.Account.id, t) |
222 | if (body.description !== undefined) userAccount.description = body.description | ||
223 | await userAccount.save({ transaction: t }) | ||
224 | 228 | ||
225 | await sendUpdateActor(userAccount, t) | 229 | if (body.displayName !== undefined) userAccount.name = body.displayName |
226 | } | 230 | if (body.description !== undefined) userAccount.description = body.description |
231 | await userAccount.save({ transaction: t }) | ||
232 | |||
233 | await sendUpdateActor(userAccount, t) | ||
227 | }) | 234 | }) |
228 | 235 | ||
229 | if (sendVerificationEmail === true) { | 236 | if (sendVerificationEmail === true) { |
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts index faaef3ac0..a1561b751 100644 --- a/server/controllers/api/users/my-blocklist.ts +++ b/server/controllers/api/users/my-blocklist.ts | |||
@@ -20,7 +20,7 @@ import { | |||
20 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | 20 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' |
21 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | 21 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' |
22 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | 22 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' |
23 | import { UserNotificationModel } from '@server/models/account/user-notification' | 23 | import { UserNotificationModel } from '@server/models/user/user-notification' |
24 | import { logger } from '@server/helpers/logger' | 24 | import { logger } from '@server/helpers/logger' |
25 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 25 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
26 | 26 | ||
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts index 72c7da373..cff1697ab 100644 --- a/server/controllers/api/users/my-history.ts +++ b/server/controllers/api/users/my-history.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | userHistoryRemoveValidator | 9 | userHistoryRemoveValidator |
10 | } from '../../../middlewares' | 10 | } from '../../../middlewares' |
11 | import { getFormattedObjects } from '../../../helpers/utils' | 11 | import { getFormattedObjects } from '../../../helpers/utils' |
12 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | 12 | import { UserVideoHistoryModel } from '../../../models/user/user-video-history' |
13 | import { sequelizeTypescript } from '../../../initializers/database' | 13 | import { sequelizeTypescript } from '../../../initializers/database' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
15 | 15 | ||
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index 0a9101a46..2909770da 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -1,5 +1,9 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | ||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
4 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { UserNotificationSetting } from '../../../../shared/models/users' | ||
6 | import { getFormattedObjects } from '../../../helpers/utils' | ||
3 | import { | 7 | import { |
4 | asyncMiddleware, | 8 | asyncMiddleware, |
5 | asyncRetryTransactionMiddleware, | 9 | asyncRetryTransactionMiddleware, |
@@ -9,17 +13,13 @@ import { | |||
9 | setDefaultSort, | 13 | setDefaultSort, |
10 | userNotificationsSortValidator | 14 | userNotificationsSortValidator |
11 | } from '../../../middlewares' | 15 | } from '../../../middlewares' |
12 | import { getFormattedObjects } from '../../../helpers/utils' | ||
13 | import { UserNotificationModel } from '../../../models/account/user-notification' | ||
14 | import { meRouter } from './me' | ||
15 | import { | 16 | import { |
16 | listUserNotificationsValidator, | 17 | listUserNotificationsValidator, |
17 | markAsReadUserNotificationsValidator, | 18 | markAsReadUserNotificationsValidator, |
18 | updateNotificationSettingsValidator | 19 | updateNotificationSettingsValidator |
19 | } from '../../../middlewares/validators/user-notifications' | 20 | } from '../../../middlewares/validators/user-notifications' |
20 | import { UserNotificationSetting } from '../../../../shared/models/users' | 21 | import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' |
21 | import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting' | 22 | import { meRouter } from './me' |
22 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
23 | 23 | ||
24 | const myNotificationsRouter = express.Router() | 24 | const myNotificationsRouter = express.Router() |
25 | 25 | ||
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index 56b93276f..46a73d49e 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -27,7 +27,7 @@ import { | |||
27 | userSubscriptionsSortValidator, | 27 | userSubscriptionsSortValidator, |
28 | videosSortValidator | 28 | videosSortValidator |
29 | } from '../../../middlewares/validators' | 29 | } from '../../../middlewares/validators' |
30 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 30 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
31 | import { VideoModel } from '../../../models/video/video' | 31 | import { VideoModel } from '../../../models/video/video' |
32 | 32 | ||
33 | const mySubscriptionsRouter = express.Router() | 33 | const mySubscriptionsRouter = express.Router() |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index a755d7e57..859d8b3c0 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -162,6 +162,7 @@ async function updateVideoChannelBanner (req: express.Request, res: express.Resp | |||
162 | 162 | ||
163 | return res.json({ banner: banner.toFormattedJSON() }) | 163 | return res.json({ banner: banner.toFormattedJSON() }) |
164 | } | 164 | } |
165 | |||
165 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | 166 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { |
166 | const avatarPhysicalFile = req.files['avatarfile'][0] | 167 | const avatarPhysicalFile = req.files['avatarfile'][0] |
167 | const videoChannel = res.locals.videoChannel | 168 | const videoChannel = res.locals.videoChannel |
@@ -221,10 +222,6 @@ async function updateVideoChannel (req: express.Request, res: express.Response) | |||
221 | 222 | ||
222 | try { | 223 | try { |
223 | await sequelizeTypescript.transaction(async t => { | 224 | await sequelizeTypescript.transaction(async t => { |
224 | const sequelizeOptions = { | ||
225 | transaction: t | ||
226 | } | ||
227 | |||
228 | if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName | 225 | if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName |
229 | if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description | 226 | if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description |
230 | 227 | ||
@@ -238,7 +235,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response) | |||
238 | } | 235 | } |
239 | } | 236 | } |
240 | 237 | ||
241 | const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault | 238 | const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault |
242 | await sendUpdateActor(videoChannelInstanceUpdated, t) | 239 | await sendUpdateActor(videoChannelInstanceUpdated, t) |
243 | 240 | ||
244 | auditLogger.update( | 241 | auditLogger.update( |
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index aab16533d..b8613699b 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -202,7 +202,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
202 | id: videoPlaylistCreated.id, | 202 | id: videoPlaylistCreated.id, |
203 | uuid: videoPlaylistCreated.uuid | 203 | uuid: videoPlaylistCreated.uuid |
204 | } | 204 | } |
205 | }).end() | 205 | }) |
206 | } | 206 | } |
207 | 207 | ||
208 | async function updateVideoPlaylist (req: express.Request, res: express.Response) { | 208 | async function updateVideoPlaylist (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index f1f53d354..cfdf2773f 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 2 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
3 | import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models' | 3 | import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models' |
4 | import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' | 4 | import { VideoCommentCreate } from '../../../../shared/models/videos/comment/video-comment.model' |
5 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | 5 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' |
6 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { getFormattedObjects } from '../../../helpers/utils' |
7 | import { sequelizeTypescript } from '../../../initializers/database' | 7 | import { sequelizeTypescript } from '../../../initializers/database' |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 3b9b887e2..0d5d7a962 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -3,7 +3,9 @@ import { move, readFile } from 'fs-extra' | |||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as parseTorrent from 'parse-torrent' | 4 | import * as parseTorrent from 'parse-torrent' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { ServerConfigManager } from '@server/lib/server-config-manager' | ||
6 | import { setVideoTags } from '@server/lib/video' | 7 | import { setVideoTags } from '@server/lib/video' |
8 | import { FilteredModelAttributes } from '@server/types' | ||
7 | import { | 9 | import { |
8 | MChannelAccountDefault, | 10 | MChannelAccountDefault, |
9 | MThumbnail, | 11 | MThumbnail, |
@@ -14,17 +16,17 @@ import { | |||
14 | MVideoThumbnail, | 16 | MVideoThumbnail, |
15 | MVideoWithBlacklistLight | 17 | MVideoWithBlacklistLight |
16 | } from '@server/types/models' | 18 | } from '@server/types/models' |
17 | import { MVideoImport, MVideoImportFormattable } from '@server/types/models/video/video-import' | 19 | import { MVideoImportFormattable } from '@server/types/models/video/video-import' |
18 | import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' | 20 | import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' |
19 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 21 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
20 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 22 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
21 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' | 23 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' |
22 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' | 24 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' |
23 | import { isArray } from '../../../helpers/custom-validators/misc' | 25 | import { isArray } from '../../../helpers/custom-validators/misc' |
24 | import { createReqFiles } from '../../../helpers/express-utils' | 26 | import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils' |
25 | import { logger } from '../../../helpers/logger' | 27 | import { logger } from '../../../helpers/logger' |
26 | import { getSecureTorrentName } from '../../../helpers/utils' | 28 | import { getSecureTorrentName } from '../../../helpers/utils' |
27 | import { getYoutubeDLInfo, getYoutubeDLSubs, YoutubeDLInfo } from '../../../helpers/youtube-dl' | 29 | import { YoutubeDL, YoutubeDLInfo } from '../../../helpers/youtube-dl' |
28 | import { CONFIG } from '../../../initializers/config' | 30 | import { CONFIG } from '../../../initializers/config' |
29 | import { MIMETYPES } from '../../../initializers/constants' | 31 | import { MIMETYPES } from '../../../initializers/constants' |
30 | import { sequelizeTypescript } from '../../../initializers/database' | 32 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -81,22 +83,15 @@ async function addTorrentImport (req: express.Request, res: express.Response, to | |||
81 | let magnetUri: string | 83 | let magnetUri: string |
82 | 84 | ||
83 | if (torrentfile) { | 85 | if (torrentfile) { |
84 | torrentName = torrentfile.originalname | 86 | const result = await processTorrentOrAbortRequest(req, res, torrentfile) |
87 | if (!result) return | ||
85 | 88 | ||
86 | // Rename the torrent to a secured name | 89 | videoName = result.name |
87 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) | 90 | torrentName = result.torrentName |
88 | await move(torrentfile.path, newTorrentPath) | ||
89 | torrentfile.path = newTorrentPath | ||
90 | |||
91 | const buf = await readFile(torrentfile.path) | ||
92 | const parsedTorrent = parseTorrent(buf) | ||
93 | |||
94 | videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[0] : parsedTorrent.name as string | ||
95 | } else { | 91 | } else { |
96 | magnetUri = body.magnetUri | 92 | const result = processMagnetURI(body) |
97 | 93 | magnetUri = result.magnetUri | |
98 | const parsed = magnetUtil.decode(magnetUri) | 94 | videoName = result.name |
99 | videoName = isArray(parsed.name) ? parsed.name[0] : parsed.name as string | ||
100 | } | 95 | } |
101 | 96 | ||
102 | const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) | 97 | const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) |
@@ -104,26 +99,26 @@ async function addTorrentImport (req: express.Request, res: express.Response, to | |||
104 | const thumbnailModel = await processThumbnail(req, video) | 99 | const thumbnailModel = await processThumbnail(req, video) |
105 | const previewModel = await processPreview(req, video) | 100 | const previewModel = await processPreview(req, video) |
106 | 101 | ||
107 | const tags = body.tags || undefined | ||
108 | const videoImportAttributes = { | ||
109 | magnetUri, | ||
110 | torrentName, | ||
111 | state: VideoImportState.PENDING, | ||
112 | userId: user.id | ||
113 | } | ||
114 | const videoImport = await insertIntoDB({ | 102 | const videoImport = await insertIntoDB({ |
115 | video, | 103 | video, |
116 | thumbnailModel, | 104 | thumbnailModel, |
117 | previewModel, | 105 | previewModel, |
118 | videoChannel: res.locals.videoChannel, | 106 | videoChannel: res.locals.videoChannel, |
119 | tags, | 107 | tags: body.tags || undefined, |
120 | videoImportAttributes, | 108 | user, |
121 | user | 109 | videoImportAttributes: { |
110 | magnetUri, | ||
111 | torrentName, | ||
112 | state: VideoImportState.PENDING, | ||
113 | userId: user.id | ||
114 | } | ||
122 | }) | 115 | }) |
123 | 116 | ||
124 | // Create job to import the video | 117 | // Create job to import the video |
125 | const payload = { | 118 | const payload = { |
126 | type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri', | 119 | type: torrentfile |
120 | ? 'torrent-file' as 'torrent-file' | ||
121 | : 'magnet-uri' as 'magnet-uri', | ||
127 | videoImportId: videoImport.id, | 122 | videoImportId: videoImport.id, |
128 | magnetUri | 123 | magnetUri |
129 | } | 124 | } |
@@ -139,10 +134,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
139 | const targetUrl = body.targetUrl | 134 | const targetUrl = body.targetUrl |
140 | const user = res.locals.oauth.token.User | 135 | const user = res.locals.oauth.token.User |
141 | 136 | ||
137 | const youtubeDL = new YoutubeDL(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) | ||
138 | |||
142 | // Get video infos | 139 | // Get video infos |
143 | let youtubeDLInfo: YoutubeDLInfo | 140 | let youtubeDLInfo: YoutubeDLInfo |
144 | try { | 141 | try { |
145 | youtubeDLInfo = await getYoutubeDLInfo(targetUrl) | 142 | youtubeDLInfo = await youtubeDL.getYoutubeDLInfo() |
146 | } catch (err) { | 143 | } catch (err) { |
147 | logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) | 144 | logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) |
148 | 145 | ||
@@ -170,45 +167,22 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
170 | previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video) | 167 | previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video) |
171 | } | 168 | } |
172 | 169 | ||
173 | const tags = body.tags || youtubeDLInfo.tags | ||
174 | const videoImportAttributes = { | ||
175 | targetUrl, | ||
176 | state: VideoImportState.PENDING, | ||
177 | userId: user.id | ||
178 | } | ||
179 | const videoImport = await insertIntoDB({ | 170 | const videoImport = await insertIntoDB({ |
180 | video, | 171 | video, |
181 | thumbnailModel, | 172 | thumbnailModel, |
182 | previewModel, | 173 | previewModel, |
183 | videoChannel: res.locals.videoChannel, | 174 | videoChannel: res.locals.videoChannel, |
184 | tags, | 175 | tags: body.tags || youtubeDLInfo.tags, |
185 | videoImportAttributes, | 176 | user, |
186 | user | 177 | videoImportAttributes: { |
178 | targetUrl, | ||
179 | state: VideoImportState.PENDING, | ||
180 | userId: user.id | ||
181 | } | ||
187 | }) | 182 | }) |
188 | 183 | ||
189 | // Get video subtitles | 184 | // Get video subtitles |
190 | try { | 185 | await processYoutubeSubtitles(youtubeDL, targetUrl, video.id) |
191 | const subtitles = await getYoutubeDLSubs(targetUrl) | ||
192 | |||
193 | logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl) | ||
194 | |||
195 | for (const subtitle of subtitles) { | ||
196 | const videoCaption = new VideoCaptionModel({ | ||
197 | videoId: video.id, | ||
198 | language: subtitle.language, | ||
199 | filename: VideoCaptionModel.generateCaptionName(subtitle.language) | ||
200 | }) as MVideoCaption | ||
201 | |||
202 | // Move physical file | ||
203 | await moveAndProcessCaptionFile(subtitle, videoCaption) | ||
204 | |||
205 | await sequelizeTypescript.transaction(async t => { | ||
206 | await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t) | ||
207 | }) | ||
208 | } | ||
209 | } catch (err) { | ||
210 | logger.warn('Cannot get video subtitles.', { err }) | ||
211 | } | ||
212 | 186 | ||
213 | // Create job to import the video | 187 | // Create job to import the video |
214 | const payload = { | 188 | const payload = { |
@@ -240,7 +214,9 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You | |||
240 | privacy: body.privacy || VideoPrivacy.PRIVATE, | 214 | privacy: body.privacy || VideoPrivacy.PRIVATE, |
241 | duration: 0, // duration will be set by the import job | 215 | duration: 0, // duration will be set by the import job |
242 | channelId: channelId, | 216 | channelId: channelId, |
243 | originallyPublishedAt: body.originallyPublishedAt || importData.originallyPublishedAt | 217 | originallyPublishedAt: body.originallyPublishedAt |
218 | ? new Date(body.originallyPublishedAt) | ||
219 | : importData.originallyPublishedAt | ||
244 | } | 220 | } |
245 | const video = new VideoModel(videoData) | 221 | const video = new VideoModel(videoData) |
246 | video.url = getLocalVideoActivityPubUrl(video) | 222 | video.url = getLocalVideoActivityPubUrl(video) |
@@ -304,7 +280,7 @@ async function insertIntoDB (parameters: { | |||
304 | previewModel: MThumbnail | 280 | previewModel: MThumbnail |
305 | videoChannel: MChannelAccountDefault | 281 | videoChannel: MChannelAccountDefault |
306 | tags: string[] | 282 | tags: string[] |
307 | videoImportAttributes: Partial<MVideoImport> | 283 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> |
308 | user: MUser | 284 | user: MUser |
309 | }): Promise<MVideoImportFormattable> { | 285 | }): Promise<MVideoImportFormattable> { |
310 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters | 286 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters |
@@ -342,3 +318,71 @@ async function insertIntoDB (parameters: { | |||
342 | 318 | ||
343 | return videoImport | 319 | return videoImport |
344 | } | 320 | } |
321 | |||
322 | async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { | ||
323 | const torrentName = torrentfile.originalname | ||
324 | |||
325 | // Rename the torrent to a secured name | ||
326 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) | ||
327 | await move(torrentfile.path, newTorrentPath, { overwrite: true }) | ||
328 | torrentfile.path = newTorrentPath | ||
329 | |||
330 | const buf = await readFile(torrentfile.path) | ||
331 | const parsedTorrent = parseTorrent(buf) as parseTorrent.Instance | ||
332 | |||
333 | if (parsedTorrent.files.length !== 1) { | ||
334 | cleanUpReqFiles(req) | ||
335 | |||
336 | res.status(HttpStatusCode.BAD_REQUEST_400) | ||
337 | .json({ | ||
338 | code: ServerErrorCode.INCORRECT_FILES_IN_TORRENT, | ||
339 | error: 'Torrents with only 1 file are supported.' | ||
340 | }) | ||
341 | |||
342 | return undefined | ||
343 | } | ||
344 | |||
345 | return { | ||
346 | name: extractNameFromArray(parsedTorrent.name), | ||
347 | torrentName | ||
348 | } | ||
349 | } | ||
350 | |||
351 | function processMagnetURI (body: VideoImportCreate) { | ||
352 | const magnetUri = body.magnetUri | ||
353 | const parsed = magnetUtil.decode(magnetUri) | ||
354 | |||
355 | return { | ||
356 | name: extractNameFromArray(parsed.name), | ||
357 | magnetUri | ||
358 | } | ||
359 | } | ||
360 | |||
361 | function extractNameFromArray (name: string | string[]) { | ||
362 | return isArray(name) ? name[0] : name | ||
363 | } | ||
364 | |||
365 | async function processYoutubeSubtitles (youtubeDL: YoutubeDL, targetUrl: string, videoId: number) { | ||
366 | try { | ||
367 | const subtitles = await youtubeDL.getYoutubeDLSubs() | ||
368 | |||
369 | logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl) | ||
370 | |||
371 | for (const subtitle of subtitles) { | ||
372 | const videoCaption = new VideoCaptionModel({ | ||
373 | videoId, | ||
374 | language: subtitle.language, | ||
375 | filename: VideoCaptionModel.generateCaptionName(subtitle.language) | ||
376 | }) as MVideoCaption | ||
377 | |||
378 | // Move physical file | ||
379 | await moveAndProcessCaptionFile(subtitle, videoCaption) | ||
380 | |||
381 | await sequelizeTypescript.transaction(async t => { | ||
382 | await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t) | ||
383 | }) | ||
384 | } | ||
385 | } catch (err) { | ||
386 | logger.warn('Cannot get video subtitles.', { err }) | ||
387 | } | ||
388 | } | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 6ec6478e4..6483d2e8a 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -1,41 +1,20 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { move } from 'fs-extra' | ||
3 | import { extname } from 'path' | ||
4 | import toInt from 'validator/lib/toInt' | 2 | import toInt from 'validator/lib/toInt' |
5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | ||
6 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | ||
7 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
8 | import { LiveManager } from '@server/lib/live-manager' | 3 | import { LiveManager } from '@server/lib/live-manager' |
9 | import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
10 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
11 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
12 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 5 | import { VideosCommonQuery } from '../../../../shared' |
13 | import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
15 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 7 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 8 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
17 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 9 | import { logger } from '../../../helpers/logger' |
18 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | ||
19 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
20 | import { getFormattedObjects } from '../../../helpers/utils' | 10 | import { getFormattedObjects } from '../../../helpers/utils' |
21 | import { CONFIG } from '../../../initializers/config' | 11 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' |
22 | import { | ||
23 | DEFAULT_AUDIO_RESOLUTION, | ||
24 | MIMETYPES, | ||
25 | VIDEO_CATEGORIES, | ||
26 | VIDEO_LANGUAGES, | ||
27 | VIDEO_LICENCES, | ||
28 | VIDEO_PRIVACIES | ||
29 | } from '../../../initializers/constants' | ||
30 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
31 | import { sendView } from '../../../lib/activitypub/send/send-view' | 13 | import { sendView } from '../../../lib/activitypub/send/send-view' |
32 | import { federateVideoIfNeeded, fetchRemoteVideoDescription } from '../../../lib/activitypub/videos' | 14 | import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos' |
33 | import { JobQueue } from '../../../lib/job-queue' | 15 | import { JobQueue } from '../../../lib/job-queue' |
34 | import { Notifier } from '../../../lib/notifier' | ||
35 | import { Hooks } from '../../../lib/plugins/hooks' | 16 | import { Hooks } from '../../../lib/plugins/hooks' |
36 | import { Redis } from '../../../lib/redis' | 17 | import { Redis } from '../../../lib/redis' |
37 | import { generateVideoMiniature } from '../../../lib/thumbnail' | ||
38 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
39 | import { | 18 | import { |
40 | asyncMiddleware, | 19 | asyncMiddleware, |
41 | asyncRetryTransactionMiddleware, | 20 | asyncRetryTransactionMiddleware, |
@@ -47,14 +26,11 @@ import { | |||
47 | setDefaultPagination, | 26 | setDefaultPagination, |
48 | setDefaultVideosSort, | 27 | setDefaultVideosSort, |
49 | videoFileMetadataGetValidator, | 28 | videoFileMetadataGetValidator, |
50 | videosAddValidator, | ||
51 | videosCustomGetValidator, | 29 | videosCustomGetValidator, |
52 | videosGetValidator, | 30 | videosGetValidator, |
53 | videosRemoveValidator, | 31 | videosRemoveValidator, |
54 | videosSortValidator, | 32 | videosSortValidator |
55 | videosUpdateValidator | ||
56 | } from '../../../middlewares' | 33 | } from '../../../middlewares' |
57 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
58 | import { VideoModel } from '../../../models/video/video' | 34 | import { VideoModel } from '../../../models/video/video' |
59 | import { VideoFileModel } from '../../../models/video/video-file' | 35 | import { VideoFileModel } from '../../../models/video/video-file' |
60 | import { blacklistRouter } from './blacklist' | 36 | import { blacklistRouter } from './blacklist' |
@@ -64,30 +40,13 @@ import { videoImportsRouter } from './import' | |||
64 | import { liveRouter } from './live' | 40 | import { liveRouter } from './live' |
65 | import { ownershipVideoRouter } from './ownership' | 41 | import { ownershipVideoRouter } from './ownership' |
66 | import { rateVideoRouter } from './rate' | 42 | import { rateVideoRouter } from './rate' |
43 | import { updateRouter } from './update' | ||
44 | import { uploadRouter } from './upload' | ||
67 | import { watchingRouter } from './watching' | 45 | import { watchingRouter } from './watching' |
68 | 46 | ||
69 | const lTags = loggerTagsFactory('api', 'video') | ||
70 | const auditLogger = auditLoggerFactory('videos') | 47 | const auditLogger = auditLoggerFactory('videos') |
71 | const videosRouter = express.Router() | 48 | const videosRouter = express.Router() |
72 | 49 | ||
73 | const reqVideoFileAdd = createReqFiles( | ||
74 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | ||
75 | Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), | ||
76 | { | ||
77 | videofile: CONFIG.STORAGE.TMP_DIR, | ||
78 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
79 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
80 | } | ||
81 | ) | ||
82 | const reqVideoFileUpdate = createReqFiles( | ||
83 | [ 'thumbnailfile', 'previewfile' ], | ||
84 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
85 | { | ||
86 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
87 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
88 | } | ||
89 | ) | ||
90 | |||
91 | videosRouter.use('/', blacklistRouter) | 50 | videosRouter.use('/', blacklistRouter) |
92 | videosRouter.use('/', rateVideoRouter) | 51 | videosRouter.use('/', rateVideoRouter) |
93 | videosRouter.use('/', videoCommentRouter) | 52 | videosRouter.use('/', videoCommentRouter) |
@@ -96,6 +55,8 @@ videosRouter.use('/', videoImportsRouter) | |||
96 | videosRouter.use('/', ownershipVideoRouter) | 55 | videosRouter.use('/', ownershipVideoRouter) |
97 | videosRouter.use('/', watchingRouter) | 56 | videosRouter.use('/', watchingRouter) |
98 | videosRouter.use('/', liveRouter) | 57 | videosRouter.use('/', liveRouter) |
58 | videosRouter.use('/', uploadRouter) | ||
59 | videosRouter.use('/', updateRouter) | ||
99 | 60 | ||
100 | videosRouter.get('/categories', listVideoCategories) | 61 | videosRouter.get('/categories', listVideoCategories) |
101 | videosRouter.get('/licences', listVideoLicences) | 62 | videosRouter.get('/licences', listVideoLicences) |
@@ -111,18 +72,6 @@ videosRouter.get('/', | |||
111 | commonVideosFiltersValidator, | 72 | commonVideosFiltersValidator, |
112 | asyncMiddleware(listVideos) | 73 | asyncMiddleware(listVideos) |
113 | ) | 74 | ) |
114 | videosRouter.put('/:id', | ||
115 | authenticate, | ||
116 | reqVideoFileUpdate, | ||
117 | asyncMiddleware(videosUpdateValidator), | ||
118 | asyncRetryTransactionMiddleware(updateVideo) | ||
119 | ) | ||
120 | videosRouter.post('/upload', | ||
121 | authenticate, | ||
122 | reqVideoFileAdd, | ||
123 | asyncMiddleware(videosAddValidator), | ||
124 | asyncRetryTransactionMiddleware(addVideo) | ||
125 | ) | ||
126 | 75 | ||
127 | videosRouter.get('/:id/description', | 76 | videosRouter.get('/:id/description', |
128 | asyncMiddleware(videosGetValidator), | 77 | asyncMiddleware(videosGetValidator), |
@@ -157,263 +106,23 @@ export { | |||
157 | 106 | ||
158 | // --------------------------------------------------------------------------- | 107 | // --------------------------------------------------------------------------- |
159 | 108 | ||
160 | function listVideoCategories (req: express.Request, res: express.Response) { | 109 | function listVideoCategories (_req: express.Request, res: express.Response) { |
161 | res.json(VIDEO_CATEGORIES) | 110 | res.json(VIDEO_CATEGORIES) |
162 | } | 111 | } |
163 | 112 | ||
164 | function listVideoLicences (req: express.Request, res: express.Response) { | 113 | function listVideoLicences (_req: express.Request, res: express.Response) { |
165 | res.json(VIDEO_LICENCES) | 114 | res.json(VIDEO_LICENCES) |
166 | } | 115 | } |
167 | 116 | ||
168 | function listVideoLanguages (req: express.Request, res: express.Response) { | 117 | function listVideoLanguages (_req: express.Request, res: express.Response) { |
169 | res.json(VIDEO_LANGUAGES) | 118 | res.json(VIDEO_LANGUAGES) |
170 | } | 119 | } |
171 | 120 | ||
172 | function listVideoPrivacies (req: express.Request, res: express.Response) { | 121 | function listVideoPrivacies (_req: express.Request, res: express.Response) { |
173 | res.json(VIDEO_PRIVACIES) | 122 | res.json(VIDEO_PRIVACIES) |
174 | } | 123 | } |
175 | 124 | ||
176 | async function addVideo (req: express.Request, res: express.Response) { | 125 | async function getVideo (_req: express.Request, res: express.Response) { |
177 | // Uploading the video could be long | ||
178 | // Set timeout to 10 minutes, as Express's default is 2 minutes | ||
179 | req.setTimeout(1000 * 60 * 10, () => { | ||
180 | logger.error('Upload video has timed out.') | ||
181 | return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408) | ||
182 | }) | ||
183 | |||
184 | const videoPhysicalFile = req.files['videofile'][0] | ||
185 | const videoInfo: VideoCreate = req.body | ||
186 | |||
187 | const videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id) | ||
188 | videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED | ||
189 | videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware | ||
190 | |||
191 | const video = new VideoModel(videoData) as MVideoFullLight | ||
192 | video.VideoChannel = res.locals.videoChannel | ||
193 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | ||
194 | |||
195 | const videoFile = new VideoFileModel({ | ||
196 | extname: extname(videoPhysicalFile.filename), | ||
197 | size: videoPhysicalFile.size, | ||
198 | videoStreamingPlaylistId: null, | ||
199 | metadata: await getMetadataFromFile(videoPhysicalFile.path) | ||
200 | }) | ||
201 | |||
202 | if (videoFile.isAudio()) { | ||
203 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION | ||
204 | } else { | ||
205 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
206 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | ||
207 | } | ||
208 | |||
209 | videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname) | ||
210 | |||
211 | // Move physical file | ||
212 | const destination = getVideoFilePath(video, videoFile) | ||
213 | await move(videoPhysicalFile.path, destination) | ||
214 | // This is important in case if there is another attempt in the retry process | ||
215 | videoPhysicalFile.filename = getVideoFilePath(video, videoFile) | ||
216 | videoPhysicalFile.path = destination | ||
217 | |||
218 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
219 | video, | ||
220 | files: req.files, | ||
221 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | ||
222 | }) | ||
223 | |||
224 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
225 | const sequelizeOptions = { transaction: t } | ||
226 | |||
227 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | ||
228 | |||
229 | await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
230 | await videoCreated.addAndSaveThumbnail(previewModel, t) | ||
231 | |||
232 | // Do not forget to add video channel information to the created video | ||
233 | videoCreated.VideoChannel = res.locals.videoChannel | ||
234 | |||
235 | videoFile.videoId = video.id | ||
236 | await videoFile.save(sequelizeOptions) | ||
237 | |||
238 | video.VideoFiles = [ videoFile ] | ||
239 | |||
240 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) | ||
241 | |||
242 | // Schedule an update in the future? | ||
243 | if (videoInfo.scheduleUpdate) { | ||
244 | await ScheduleVideoUpdateModel.create({ | ||
245 | videoId: video.id, | ||
246 | updateAt: videoInfo.scheduleUpdate.updateAt, | ||
247 | privacy: videoInfo.scheduleUpdate.privacy || null | ||
248 | }, { transaction: t }) | ||
249 | } | ||
250 | |||
251 | await autoBlacklistVideoIfNeeded({ | ||
252 | video, | ||
253 | user: res.locals.oauth.token.User, | ||
254 | isRemote: false, | ||
255 | isNew: true, | ||
256 | transaction: t | ||
257 | }) | ||
258 | |||
259 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | ||
260 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) | ||
261 | |||
262 | return { videoCreated } | ||
263 | }) | ||
264 | |||
265 | // Create the torrent file in async way because it could be long | ||
266 | createTorrentAndSetInfoHashAsync(video, videoFile) | ||
267 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) | ||
268 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) | ||
269 | .then(refreshedVideo => { | ||
270 | if (!refreshedVideo) return | ||
271 | |||
272 | // Only federate and notify after the torrent creation | ||
273 | Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo) | ||
274 | |||
275 | return retryTransactionWrapper(() => { | ||
276 | return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) | ||
277 | }) | ||
278 | }) | ||
279 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) | ||
280 | |||
281 | if (video.state === VideoState.TO_TRANSCODE) { | ||
282 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) | ||
283 | } | ||
284 | |||
285 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) | ||
286 | |||
287 | return res.json({ | ||
288 | video: { | ||
289 | id: videoCreated.id, | ||
290 | uuid: videoCreated.uuid | ||
291 | } | ||
292 | }) | ||
293 | } | ||
294 | |||
295 | async function updateVideo (req: express.Request, res: express.Response) { | ||
296 | const videoInstance = res.locals.videoAll | ||
297 | const videoFieldsSave = videoInstance.toJSON() | ||
298 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | ||
299 | const videoInfoToUpdate: VideoUpdate = req.body | ||
300 | |||
301 | const wasConfidentialVideo = videoInstance.isConfidential() | ||
302 | const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation() | ||
303 | |||
304 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
305 | video: videoInstance, | ||
306 | files: req.files, | ||
307 | fallback: () => Promise.resolve(undefined), | ||
308 | automaticallyGenerated: false | ||
309 | }) | ||
310 | |||
311 | try { | ||
312 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { | ||
313 | const sequelizeOptions = { transaction: t } | ||
314 | const oldVideoChannel = videoInstance.VideoChannel | ||
315 | |||
316 | if (videoInfoToUpdate.name !== undefined) videoInstance.name = videoInfoToUpdate.name | ||
317 | if (videoInfoToUpdate.category !== undefined) videoInstance.category = videoInfoToUpdate.category | ||
318 | if (videoInfoToUpdate.licence !== undefined) videoInstance.licence = videoInfoToUpdate.licence | ||
319 | if (videoInfoToUpdate.language !== undefined) videoInstance.language = videoInfoToUpdate.language | ||
320 | if (videoInfoToUpdate.nsfw !== undefined) videoInstance.nsfw = videoInfoToUpdate.nsfw | ||
321 | if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.waitTranscoding = videoInfoToUpdate.waitTranscoding | ||
322 | if (videoInfoToUpdate.support !== undefined) videoInstance.support = videoInfoToUpdate.support | ||
323 | if (videoInfoToUpdate.description !== undefined) videoInstance.description = videoInfoToUpdate.description | ||
324 | if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.commentsEnabled = videoInfoToUpdate.commentsEnabled | ||
325 | if (videoInfoToUpdate.downloadEnabled !== undefined) videoInstance.downloadEnabled = videoInfoToUpdate.downloadEnabled | ||
326 | |||
327 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { | ||
328 | videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) | ||
329 | } | ||
330 | |||
331 | let isNewVideo = false | ||
332 | if (videoInfoToUpdate.privacy !== undefined) { | ||
333 | isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) | ||
334 | |||
335 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) | ||
336 | videoInstance.setPrivacy(newPrivacy) | ||
337 | |||
338 | // Unfederate the video if the new privacy is not compatible with federation | ||
339 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | ||
340 | await VideoModel.sendDelete(videoInstance, { transaction: t }) | ||
341 | } | ||
342 | } | ||
343 | |||
344 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight | ||
345 | |||
346 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
347 | if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) | ||
348 | |||
349 | // Video tags update? | ||
350 | if (videoInfoToUpdate.tags !== undefined) { | ||
351 | await setVideoTags({ | ||
352 | video: videoInstanceUpdated, | ||
353 | tags: videoInfoToUpdate.tags, | ||
354 | transaction: t | ||
355 | }) | ||
356 | } | ||
357 | |||
358 | // Video channel update? | ||
359 | if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { | ||
360 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | ||
361 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | ||
362 | |||
363 | if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | ||
364 | } | ||
365 | |||
366 | // Schedule an update in the future? | ||
367 | if (videoInfoToUpdate.scheduleUpdate) { | ||
368 | await ScheduleVideoUpdateModel.upsert({ | ||
369 | videoId: videoInstanceUpdated.id, | ||
370 | updateAt: videoInfoToUpdate.scheduleUpdate.updateAt, | ||
371 | privacy: videoInfoToUpdate.scheduleUpdate.privacy || null | ||
372 | }, { transaction: t }) | ||
373 | } else if (videoInfoToUpdate.scheduleUpdate === null) { | ||
374 | await ScheduleVideoUpdateModel.deleteByVideoId(videoInstanceUpdated.id, t) | ||
375 | } | ||
376 | |||
377 | await autoBlacklistVideoIfNeeded({ | ||
378 | video: videoInstanceUpdated, | ||
379 | user: res.locals.oauth.token.User, | ||
380 | isRemote: false, | ||
381 | isNew: false, | ||
382 | transaction: t | ||
383 | }) | ||
384 | |||
385 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | ||
386 | |||
387 | auditLogger.update( | ||
388 | getAuditIdFromRes(res), | ||
389 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | ||
390 | oldVideoAuditView | ||
391 | ) | ||
392 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid)) | ||
393 | |||
394 | return videoInstanceUpdated | ||
395 | }) | ||
396 | |||
397 | if (wasConfidentialVideo) { | ||
398 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) | ||
399 | } | ||
400 | |||
401 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body }) | ||
402 | } catch (err) { | ||
403 | // Force fields we want to update | ||
404 | // If the transaction is retried, sequelize will think the object has not changed | ||
405 | // So it will skip the SQL request, even if the last one was ROLLBACKed! | ||
406 | resetSequelizeInstance(videoInstance, videoFieldsSave) | ||
407 | |||
408 | throw err | ||
409 | } | ||
410 | |||
411 | return res.type('json') | ||
412 | .status(HttpStatusCode.NO_CONTENT_204) | ||
413 | .end() | ||
414 | } | ||
415 | |||
416 | async function getVideo (req: express.Request, res: express.Response) { | ||
417 | // We need more attributes | 126 | // We need more attributes |
418 | const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null | 127 | const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null |
419 | 128 | ||
@@ -475,13 +184,10 @@ async function viewVideo (req: express.Request, res: express.Response) { | |||
475 | 184 | ||
476 | async function getVideoDescription (req: express.Request, res: express.Response) { | 185 | async function getVideoDescription (req: express.Request, res: express.Response) { |
477 | const videoInstance = res.locals.videoAll | 186 | const videoInstance = res.locals.videoAll |
478 | let description = '' | ||
479 | 187 | ||
480 | if (videoInstance.isOwned()) { | 188 | const description = videoInstance.isOwned() |
481 | description = videoInstance.description | 189 | ? videoInstance.description |
482 | } else { | 190 | : await fetchRemoteVideoDescription(videoInstance) |
483 | description = await fetchRemoteVideoDescription(videoInstance) | ||
484 | } | ||
485 | 191 | ||
486 | return res.json({ description }) | 192 | return res.json({ description }) |
487 | } | 193 | } |
@@ -523,7 +229,7 @@ async function listVideos (req: express.Request, res: express.Response) { | |||
523 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 229 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
524 | } | 230 | } |
525 | 231 | ||
526 | async function removeVideo (req: express.Request, res: express.Response) { | 232 | async function removeVideo (_req: express.Request, res: express.Response) { |
527 | const videoInstance = res.locals.videoAll | 233 | const videoInstance = res.locals.videoAll |
528 | 234 | ||
529 | await sequelizeTypescript.transaction(async t => { | 235 | await sequelizeTypescript.transaction(async t => { |
@@ -539,17 +245,3 @@ async function removeVideo (req: express.Request, res: express.Response) { | |||
539 | .status(HttpStatusCode.NO_CONTENT_204) | 245 | .status(HttpStatusCode.NO_CONTENT_204) |
540 | .end() | 246 | .end() |
541 | } | 247 | } |
542 | |||
543 | async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) { | ||
544 | await createTorrentAndSetInfoHash(video, fileArg) | ||
545 | |||
546 | // Refresh videoFile because the createTorrentAndSetInfoHash could be long | ||
547 | const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id) | ||
548 | // File does not exist anymore, remove the generated torrent | ||
549 | if (!refreshedFile) return fileArg.removeTorrent() | ||
550 | |||
551 | refreshedFile.infoHash = fileArg.infoHash | ||
552 | refreshedFile.torrentFilename = fileArg.torrentFilename | ||
553 | |||
554 | return refreshedFile.save() | ||
555 | } | ||
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts index a85d7c30b..6102f28dc 100644 --- a/server/controllers/api/videos/ownership.ts +++ b/server/controllers/api/videos/ownership.ts | |||
@@ -99,7 +99,7 @@ async function listVideoOwnership (req: express.Request, res: express.Response) | |||
99 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 99 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
100 | } | 100 | } |
101 | 101 | ||
102 | async function acceptOwnership (req: express.Request, res: express.Response) { | 102 | function acceptOwnership (req: express.Request, res: express.Response) { |
103 | return sequelizeTypescript.transaction(async t => { | 103 | return sequelizeTypescript.transaction(async t => { |
104 | const videoChangeOwnership = res.locals.videoChangeOwnership | 104 | const videoChangeOwnership = res.locals.videoChangeOwnership |
105 | const channel = res.locals.videoChannel | 105 | const channel = res.locals.videoChannel |
@@ -126,7 +126,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) { | |||
126 | }) | 126 | }) |
127 | } | 127 | } |
128 | 128 | ||
129 | async function refuseOwnership (req: express.Request, res: express.Response) { | 129 | function refuseOwnership (req: express.Request, res: express.Response) { |
130 | return sequelizeTypescript.transaction(async t => { | 130 | return sequelizeTypescript.transaction(async t => { |
131 | const videoChangeOwnership = res.locals.videoChangeOwnership | 131 | const videoChangeOwnership = res.locals.videoChangeOwnership |
132 | 132 | ||
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts new file mode 100644 index 000000000..2450abd0e --- /dev/null +++ b/server/controllers/api/videos/update.ts | |||
@@ -0,0 +1,191 @@ | |||
1 | import * as express from 'express' | ||
2 | import { Transaction } from 'sequelize/types' | ||
3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | ||
4 | import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
5 | import { FilteredModelAttributes } from '@server/types' | ||
6 | import { MVideoFullLight } from '@server/types/models' | ||
7 | import { VideoUpdate } from '../../../../shared' | ||
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' | ||
9 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
10 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | ||
11 | import { createReqFiles } from '../../../helpers/express-utils' | ||
12 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
13 | import { CONFIG } from '../../../initializers/config' | ||
14 | import { MIMETYPES } from '../../../initializers/constants' | ||
15 | import { sequelizeTypescript } from '../../../initializers/database' | ||
16 | import { federateVideoIfNeeded } from '../../../lib/activitypub/videos' | ||
17 | import { Notifier } from '../../../lib/notifier' | ||
18 | import { Hooks } from '../../../lib/plugins/hooks' | ||
19 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
20 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' | ||
21 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
22 | import { VideoModel } from '../../../models/video/video' | ||
23 | |||
24 | const lTags = loggerTagsFactory('api', 'video') | ||
25 | const auditLogger = auditLoggerFactory('videos') | ||
26 | const updateRouter = express.Router() | ||
27 | |||
28 | const reqVideoFileUpdate = createReqFiles( | ||
29 | [ 'thumbnailfile', 'previewfile' ], | ||
30 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
31 | { | ||
32 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
33 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
34 | } | ||
35 | ) | ||
36 | |||
37 | updateRouter.put('/:id', | ||
38 | authenticate, | ||
39 | reqVideoFileUpdate, | ||
40 | asyncMiddleware(videosUpdateValidator), | ||
41 | asyncRetryTransactionMiddleware(updateVideo) | ||
42 | ) | ||
43 | |||
44 | // --------------------------------------------------------------------------- | ||
45 | |||
46 | export { | ||
47 | updateRouter | ||
48 | } | ||
49 | |||
50 | // --------------------------------------------------------------------------- | ||
51 | |||
52 | export async function updateVideo (req: express.Request, res: express.Response) { | ||
53 | const videoInstance = res.locals.videoAll | ||
54 | const videoFieldsSave = videoInstance.toJSON() | ||
55 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | ||
56 | const videoInfoToUpdate: VideoUpdate = req.body | ||
57 | |||
58 | const wasConfidentialVideo = videoInstance.isConfidential() | ||
59 | const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation() | ||
60 | |||
61 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
62 | video: videoInstance, | ||
63 | files: req.files, | ||
64 | fallback: () => Promise.resolve(undefined), | ||
65 | automaticallyGenerated: false | ||
66 | }) | ||
67 | |||
68 | try { | ||
69 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { | ||
70 | const sequelizeOptions = { transaction: t } | ||
71 | const oldVideoChannel = videoInstance.VideoChannel | ||
72 | |||
73 | const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [ | ||
74 | 'name', | ||
75 | 'category', | ||
76 | 'licence', | ||
77 | 'language', | ||
78 | 'nsfw', | ||
79 | 'waitTranscoding', | ||
80 | 'support', | ||
81 | 'description', | ||
82 | 'commentsEnabled', | ||
83 | 'downloadEnabled' | ||
84 | ] | ||
85 | |||
86 | for (const key of keysToUpdate) { | ||
87 | if (videoInfoToUpdate[key] !== undefined) videoInstance.set(key, videoInfoToUpdate[key]) | ||
88 | } | ||
89 | |||
90 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { | ||
91 | videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) | ||
92 | } | ||
93 | |||
94 | // Privacy update? | ||
95 | let isNewVideo = false | ||
96 | if (videoInfoToUpdate.privacy !== undefined) { | ||
97 | isNewVideo = await updateVideoPrivacy({ videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction: t }) | ||
98 | } | ||
99 | |||
100 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight | ||
101 | |||
102 | // Thumbnail & preview updates? | ||
103 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
104 | if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) | ||
105 | |||
106 | // Video tags update? | ||
107 | if (videoInfoToUpdate.tags !== undefined) { | ||
108 | await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t }) | ||
109 | } | ||
110 | |||
111 | // Video channel update? | ||
112 | if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { | ||
113 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | ||
114 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | ||
115 | |||
116 | if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | ||
117 | } | ||
118 | |||
119 | // Schedule an update in the future? | ||
120 | await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t) | ||
121 | |||
122 | await autoBlacklistVideoIfNeeded({ | ||
123 | video: videoInstanceUpdated, | ||
124 | user: res.locals.oauth.token.User, | ||
125 | isRemote: false, | ||
126 | isNew: false, | ||
127 | transaction: t | ||
128 | }) | ||
129 | |||
130 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | ||
131 | |||
132 | auditLogger.update( | ||
133 | getAuditIdFromRes(res), | ||
134 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | ||
135 | oldVideoAuditView | ||
136 | ) | ||
137 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid)) | ||
138 | |||
139 | return videoInstanceUpdated | ||
140 | }) | ||
141 | |||
142 | if (wasConfidentialVideo) { | ||
143 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) | ||
144 | } | ||
145 | |||
146 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body }) | ||
147 | } catch (err) { | ||
148 | // Force fields we want to update | ||
149 | // If the transaction is retried, sequelize will think the object has not changed | ||
150 | // So it will skip the SQL request, even if the last one was ROLLBACKed! | ||
151 | resetSequelizeInstance(videoInstance, videoFieldsSave) | ||
152 | |||
153 | throw err | ||
154 | } | ||
155 | |||
156 | return res.type('json') | ||
157 | .status(HttpStatusCode.NO_CONTENT_204) | ||
158 | .end() | ||
159 | } | ||
160 | |||
161 | async function updateVideoPrivacy (options: { | ||
162 | videoInstance: MVideoFullLight | ||
163 | videoInfoToUpdate: VideoUpdate | ||
164 | hadPrivacyForFederation: boolean | ||
165 | transaction: Transaction | ||
166 | }) { | ||
167 | const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options | ||
168 | const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) | ||
169 | |||
170 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) | ||
171 | videoInstance.setPrivacy(newPrivacy) | ||
172 | |||
173 | // Unfederate the video if the new privacy is not compatible with federation | ||
174 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | ||
175 | await VideoModel.sendDelete(videoInstance, { transaction }) | ||
176 | } | ||
177 | |||
178 | return isNewVideo | ||
179 | } | ||
180 | |||
181 | function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) { | ||
182 | if (videoInfoToUpdate.scheduleUpdate) { | ||
183 | return ScheduleVideoUpdateModel.upsert({ | ||
184 | videoId: videoInstance.id, | ||
185 | updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt), | ||
186 | privacy: videoInfoToUpdate.scheduleUpdate.privacy || null | ||
187 | }, { transaction }) | ||
188 | } else if (videoInfoToUpdate.scheduleUpdate === null) { | ||
189 | return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) | ||
190 | } | ||
191 | } | ||
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts new file mode 100644 index 000000000..ebc17c760 --- /dev/null +++ b/server/controllers/api/videos/upload.ts | |||
@@ -0,0 +1,269 @@ | |||
1 | import * as express from 'express' | ||
2 | import { move } from 'fs-extra' | ||
3 | import { extname } from 'path' | ||
4 | import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | ||
6 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
7 | import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
8 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
9 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | ||
10 | import { uploadx } from '@uploadx/core' | ||
11 | import { VideoCreate, VideoState } from '../../../../shared' | ||
12 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' | ||
13 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
14 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
15 | import { createReqFiles } from '../../../helpers/express-utils' | ||
16 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | ||
17 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
18 | import { CONFIG } from '../../../initializers/config' | ||
19 | import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants' | ||
20 | import { sequelizeTypescript } from '../../../initializers/database' | ||
21 | import { federateVideoIfNeeded } from '../../../lib/activitypub/videos' | ||
22 | import { Notifier } from '../../../lib/notifier' | ||
23 | import { Hooks } from '../../../lib/plugins/hooks' | ||
24 | import { generateVideoMiniature } from '../../../lib/thumbnail' | ||
25 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
26 | import { | ||
27 | asyncMiddleware, | ||
28 | asyncRetryTransactionMiddleware, | ||
29 | authenticate, | ||
30 | videosAddLegacyValidator, | ||
31 | videosAddResumableInitValidator, | ||
32 | videosAddResumableValidator | ||
33 | } from '../../../middlewares' | ||
34 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
35 | import { VideoModel } from '../../../models/video/video' | ||
36 | import { VideoFileModel } from '../../../models/video/video-file' | ||
37 | |||
38 | const lTags = loggerTagsFactory('api', 'video') | ||
39 | const auditLogger = auditLoggerFactory('videos') | ||
40 | const uploadRouter = express.Router() | ||
41 | const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() }) | ||
42 | |||
43 | const reqVideoFileAdd = createReqFiles( | ||
44 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | ||
45 | Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), | ||
46 | { | ||
47 | videofile: CONFIG.STORAGE.TMP_DIR, | ||
48 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
49 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
50 | } | ||
51 | ) | ||
52 | |||
53 | const reqVideoFileAddResumable = createReqFiles( | ||
54 | [ 'thumbnailfile', 'previewfile' ], | ||
55 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
56 | { | ||
57 | thumbnailfile: getResumableUploadPath(), | ||
58 | previewfile: getResumableUploadPath() | ||
59 | } | ||
60 | ) | ||
61 | |||
62 | uploadRouter.post('/upload', | ||
63 | authenticate, | ||
64 | reqVideoFileAdd, | ||
65 | asyncMiddleware(videosAddLegacyValidator), | ||
66 | asyncRetryTransactionMiddleware(addVideoLegacy) | ||
67 | ) | ||
68 | |||
69 | uploadRouter.post('/upload-resumable', | ||
70 | authenticate, | ||
71 | reqVideoFileAddResumable, | ||
72 | asyncMiddleware(videosAddResumableInitValidator), | ||
73 | uploadxMiddleware | ||
74 | ) | ||
75 | |||
76 | uploadRouter.delete('/upload-resumable', | ||
77 | authenticate, | ||
78 | uploadxMiddleware | ||
79 | ) | ||
80 | |||
81 | uploadRouter.put('/upload-resumable', | ||
82 | authenticate, | ||
83 | uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes | ||
84 | asyncMiddleware(videosAddResumableValidator), | ||
85 | asyncMiddleware(addVideoResumable) | ||
86 | ) | ||
87 | |||
88 | // --------------------------------------------------------------------------- | ||
89 | |||
90 | export { | ||
91 | uploadRouter | ||
92 | } | ||
93 | |||
94 | // --------------------------------------------------------------------------- | ||
95 | |||
96 | export async function addVideoLegacy (req: express.Request, res: express.Response) { | ||
97 | // Uploading the video could be long | ||
98 | // Set timeout to 10 minutes, as Express's default is 2 minutes | ||
99 | req.setTimeout(1000 * 60 * 10, () => { | ||
100 | logger.error('Upload video has timed out.') | ||
101 | return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408) | ||
102 | }) | ||
103 | |||
104 | const videoPhysicalFile = req.files['videofile'][0] | ||
105 | const videoInfo: VideoCreate = req.body | ||
106 | const files = req.files | ||
107 | |||
108 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
109 | } | ||
110 | |||
111 | export async function addVideoResumable (_req: express.Request, res: express.Response) { | ||
112 | const videoPhysicalFile = res.locals.videoFileResumable | ||
113 | const videoInfo = videoPhysicalFile.metadata | ||
114 | const files = { previewfile: videoInfo.previewfile } | ||
115 | |||
116 | // Don't need the meta file anymore | ||
117 | await deleteResumableUploadMetaFile(videoPhysicalFile.path) | ||
118 | |||
119 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
120 | } | ||
121 | |||
122 | async function addVideo (options: { | ||
123 | res: express.Response | ||
124 | videoPhysicalFile: express.VideoUploadFile | ||
125 | videoInfo: VideoCreate | ||
126 | files: express.UploadFiles | ||
127 | }) { | ||
128 | const { res, videoPhysicalFile, videoInfo, files } = options | ||
129 | const videoChannel = res.locals.videoChannel | ||
130 | const user = res.locals.oauth.token.User | ||
131 | |||
132 | const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) | ||
133 | |||
134 | videoData.state = CONFIG.TRANSCODING.ENABLED | ||
135 | ? VideoState.TO_TRANSCODE | ||
136 | : VideoState.PUBLISHED | ||
137 | |||
138 | videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware | ||
139 | |||
140 | const video = new VideoModel(videoData) as MVideoFullLight | ||
141 | video.VideoChannel = videoChannel | ||
142 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | ||
143 | |||
144 | const videoFile = await buildNewFile(video, videoPhysicalFile) | ||
145 | |||
146 | // Move physical file | ||
147 | const destination = getVideoFilePath(video, videoFile) | ||
148 | await move(videoPhysicalFile.path, destination) | ||
149 | // This is important in case if there is another attempt in the retry process | ||
150 | videoPhysicalFile.filename = getVideoFilePath(video, videoFile) | ||
151 | videoPhysicalFile.path = destination | ||
152 | |||
153 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
154 | video, | ||
155 | files, | ||
156 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | ||
157 | }) | ||
158 | |||
159 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
160 | const sequelizeOptions = { transaction: t } | ||
161 | |||
162 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | ||
163 | |||
164 | await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
165 | await videoCreated.addAndSaveThumbnail(previewModel, t) | ||
166 | |||
167 | // Do not forget to add video channel information to the created video | ||
168 | videoCreated.VideoChannel = res.locals.videoChannel | ||
169 | |||
170 | videoFile.videoId = video.id | ||
171 | await videoFile.save(sequelizeOptions) | ||
172 | |||
173 | video.VideoFiles = [ videoFile ] | ||
174 | |||
175 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) | ||
176 | |||
177 | // Schedule an update in the future? | ||
178 | if (videoInfo.scheduleUpdate) { | ||
179 | await ScheduleVideoUpdateModel.create({ | ||
180 | videoId: video.id, | ||
181 | updateAt: new Date(videoInfo.scheduleUpdate.updateAt), | ||
182 | privacy: videoInfo.scheduleUpdate.privacy || null | ||
183 | }, sequelizeOptions) | ||
184 | } | ||
185 | |||
186 | // Channel has a new content, set as updated | ||
187 | await videoCreated.VideoChannel.setAsUpdated(t) | ||
188 | |||
189 | await autoBlacklistVideoIfNeeded({ | ||
190 | video, | ||
191 | user, | ||
192 | isRemote: false, | ||
193 | isNew: true, | ||
194 | transaction: t | ||
195 | }) | ||
196 | |||
197 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | ||
198 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) | ||
199 | |||
200 | return { videoCreated } | ||
201 | }) | ||
202 | |||
203 | createTorrentFederate(video, videoFile) | ||
204 | |||
205 | if (video.state === VideoState.TO_TRANSCODE) { | ||
206 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) | ||
207 | } | ||
208 | |||
209 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) | ||
210 | |||
211 | return res.json({ | ||
212 | video: { | ||
213 | id: videoCreated.id, | ||
214 | uuid: videoCreated.uuid | ||
215 | } | ||
216 | }) | ||
217 | } | ||
218 | |||
219 | async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) { | ||
220 | const videoFile = new VideoFileModel({ | ||
221 | extname: extname(videoPhysicalFile.filename), | ||
222 | size: videoPhysicalFile.size, | ||
223 | videoStreamingPlaylistId: null, | ||
224 | metadata: await getMetadataFromFile(videoPhysicalFile.path) | ||
225 | }) | ||
226 | |||
227 | if (videoFile.isAudio()) { | ||
228 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION | ||
229 | } else { | ||
230 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
231 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | ||
232 | } | ||
233 | |||
234 | videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname) | ||
235 | |||
236 | return videoFile | ||
237 | } | ||
238 | |||
239 | async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) { | ||
240 | await createTorrentAndSetInfoHash(video, fileArg) | ||
241 | |||
242 | // Refresh videoFile because the createTorrentAndSetInfoHash could be long | ||
243 | const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id) | ||
244 | // File does not exist anymore, remove the generated torrent | ||
245 | if (!refreshedFile) return fileArg.removeTorrent() | ||
246 | |||
247 | refreshedFile.infoHash = fileArg.infoHash | ||
248 | refreshedFile.torrentFilename = fileArg.torrentFilename | ||
249 | |||
250 | return refreshedFile.save() | ||
251 | } | ||
252 | |||
253 | function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void { | ||
254 | // Create the torrent file in async way because it could be long | ||
255 | createTorrentAndSetInfoHashAsync(video, videoFile) | ||
256 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) | ||
257 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) | ||
258 | .then(refreshedVideo => { | ||
259 | if (!refreshedVideo) return | ||
260 | |||
261 | // Only federate and notify after the torrent creation | ||
262 | Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo) | ||
263 | |||
264 | return retryTransactionWrapper(() => { | ||
265 | return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) | ||
266 | }) | ||
267 | }) | ||
268 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) | ||
269 | } | ||
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts index 627f12aa9..08190e583 100644 --- a/server/controllers/api/videos/watching.ts +++ b/server/controllers/api/videos/watching.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { UserWatchingVideo } from '../../../../shared' | 2 | import { UserWatchingVideo } from '../../../../shared' |
3 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' | 3 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' |
4 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | 4 | import { UserVideoHistoryModel } from '../../../models/user/user-video-history' |
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
6 | 6 | ||
7 | const watchingRouter = express.Router() | 7 | const watchingRouter = express.Router() |