aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers/api
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers/api')
-rw-r--r--server/controllers/api/abuse.ts8
-rw-r--r--server/controllers/api/bulk.ts2
-rw-r--r--server/controllers/api/config.ts25
-rw-r--r--server/controllers/api/custom-page.ts47
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/jobs.ts2
-rw-r--r--server/controllers/api/oauth-clients.ts8
-rw-r--r--server/controllers/api/plugins.ts53
-rw-r--r--server/controllers/api/search.ts286
-rw-r--r--server/controllers/api/search/index.ts16
-rw-r--r--server/controllers/api/search/search-video-channels.ts150
-rw-r--r--server/controllers/api/search/search-video-playlists.ts129
-rw-r--r--server/controllers/api/search/search-videos.ts153
-rw-r--r--server/controllers/api/server/debug.ts3
-rw-r--r--server/controllers/api/server/follows.ts20
-rw-r--r--server/controllers/api/server/redundancy.ts6
-rw-r--r--server/controllers/api/server/server-blocklist.ts2
-rw-r--r--server/controllers/api/users/index.ts28
-rw-r--r--server/controllers/api/users/me.ts53
-rw-r--r--server/controllers/api/users/my-blocklist.ts2
-rw-r--r--server/controllers/api/users/my-history.ts2
-rw-r--r--server/controllers/api/users/my-notifications.ts14
-rw-r--r--server/controllers/api/users/my-subscriptions.ts2
-rw-r--r--server/controllers/api/users/token.ts15
-rw-r--r--server/controllers/api/video-channel.ts17
-rw-r--r--server/controllers/api/video-playlist.ts23
-rw-r--r--server/controllers/api/videos/blacklist.ts10
-rw-r--r--server/controllers/api/videos/comment.ts7
-rw-r--r--server/controllers/api/videos/import.ts189
-rw-r--r--server/controllers/api/videos/index.ts454
-rw-r--r--server/controllers/api/videos/live.ts13
-rw-r--r--server/controllers/api/videos/ownership.ts12
-rw-r--r--server/controllers/api/videos/update.ts193
-rw-r--r--server/controllers/api/videos/upload.ts278
-rw-r--r--server/controllers/api/videos/watching.ts11
35 files changed, 1335 insertions, 900 deletions
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
index 0ab74bdff..ba5b94840 100644
--- a/server/controllers/api/abuse.ts
+++ b/server/controllers/api/abuse.ts
@@ -24,6 +24,7 @@ import {
24 deleteAbuseMessageValidator, 24 deleteAbuseMessageValidator,
25 ensureUserHasRight, 25 ensureUserHasRight,
26 getAbuseValidator, 26 getAbuseValidator,
27 openapiOperationDoc,
27 paginationValidator, 28 paginationValidator,
28 setDefaultPagination, 29 setDefaultPagination,
29 setDefaultSort 30 setDefaultSort
@@ -33,6 +34,7 @@ import { AccountModel } from '../../models/account/account'
33const abuseRouter = express.Router() 34const abuseRouter = express.Router()
34 35
35abuseRouter.get('/', 36abuseRouter.get('/',
37 openapiOperationDoc({ operationId: 'getAbuses' }),
36 authenticate, 38 authenticate,
37 ensureUserHasRight(UserRight.MANAGE_ABUSES), 39 ensureUserHasRight(UserRight.MANAGE_ABUSES),
38 paginationValidator, 40 paginationValidator,
@@ -142,7 +144,7 @@ async function updateAbuse (req: express.Request, res: express.Response) {
142 144
143 // Do not send the delete to other instances, we updated OUR copy of this abuse 145 // Do not send the delete to other instances, we updated OUR copy of this abuse
144 146
145 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 147 return res.status(HttpStatusCode.NO_CONTENT_204).end()
146} 148}
147 149
148async function deleteAbuse (req: express.Request, res: express.Response) { 150async function deleteAbuse (req: express.Request, res: express.Response) {
@@ -154,7 +156,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) {
154 156
155 // Do not send the delete to other instances, we delete OUR copy of this abuse 157 // Do not send the delete to other instances, we delete OUR copy of this abuse
156 158
157 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 159 return res.status(HttpStatusCode.NO_CONTENT_204).end()
158} 160}
159 161
160async function reportAbuse (req: express.Request, res: express.Response) { 162async function reportAbuse (req: express.Request, res: express.Response) {
@@ -244,5 +246,5 @@ async function deleteAbuseMessage (req: express.Request, res: express.Response)
244 return abuseMessage.destroy({ transaction: t }) 246 return abuseMessage.destroy({ transaction: t })
245 }) 247 })
246 248
247 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 249 return res.status(HttpStatusCode.NO_CONTENT_204).end()
248} 250}
diff --git a/server/controllers/api/bulk.ts b/server/controllers/api/bulk.ts
index 649351029..192daccde 100644
--- a/server/controllers/api/bulk.ts
+++ b/server/controllers/api/bulk.ts
@@ -34,7 +34,7 @@ async function bulkRemoveCommentsOf (req: express.Request, res: express.Response
34 const comments = await VideoCommentModel.listForBulkDelete(account, filter) 34 const comments = await VideoCommentModel.listForBulkDelete(account, filter)
35 35
36 // Don't wait result 36 // Don't wait result
37 res.sendStatus(HttpStatusCode.NO_CONTENT_204) 37 res.status(HttpStatusCode.NO_CONTENT_204).end()
38 38
39 for (const comment of comments) { 39 for (const comment of comments) {
40 await removeComment(comment) 40 await removeComment(comment)
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 2ddb73519..9bd8c21c5 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -1,8 +1,8 @@
1import { ServerConfigManager } from '@server/lib/server-config-manager'
1import * as express from 'express' 2import * as express from 'express'
2import { remove, writeJSON } from 'fs-extra' 3import { remove, writeJSON } from 'fs-extra'
3import { snakeCase } from 'lodash' 4import { snakeCase } from 'lodash'
4import validator from 'validator' 5import validator from 'validator'
5import { getServerConfig } from '@server/lib/config'
6import { UserRight } from '../../../shared' 6import { UserRight } from '../../../shared'
7import { About } from '../../../shared/models/server/about.model' 7import { About } from '../../../shared/models/server/about.model'
8import { CustomConfig } from '../../../shared/models/server/custom-config.model' 8import { CustomConfig } from '../../../shared/models/server/custom-config.model'
@@ -10,37 +10,47 @@ import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '..
10import { objectConverter } from '../../helpers/core-utils' 10import { objectConverter } from '../../helpers/core-utils'
11import { CONFIG, reloadConfig } from '../../initializers/config' 11import { CONFIG, reloadConfig } from '../../initializers/config'
12import { ClientHtml } from '../../lib/client-html' 12import { ClientHtml } from '../../lib/client-html'
13import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' 13import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares'
14import { customConfigUpdateValidator } from '../../middlewares/validators/config' 14import { customConfigUpdateValidator } from '../../middlewares/validators/config'
15 15
16const configRouter = express.Router() 16const configRouter = express.Router()
17 17
18const auditLogger = auditLoggerFactory('config') 18const auditLogger = auditLoggerFactory('config')
19 19
20configRouter.get('/about', getAbout)
21configRouter.get('/', 20configRouter.get('/',
21 openapiOperationDoc({ operationId: 'getConfig' }),
22 asyncMiddleware(getConfig) 22 asyncMiddleware(getConfig)
23) 23)
24 24
25configRouter.get('/about',
26 openapiOperationDoc({ operationId: 'getAbout' }),
27 getAbout
28)
29
25configRouter.get('/custom', 30configRouter.get('/custom',
31 openapiOperationDoc({ operationId: 'getCustomConfig' }),
26 authenticate, 32 authenticate,
27 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), 33 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
28 getCustomConfig 34 getCustomConfig
29) 35)
36
30configRouter.put('/custom', 37configRouter.put('/custom',
38 openapiOperationDoc({ operationId: 'putCustomConfig' }),
31 authenticate, 39 authenticate,
32 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), 40 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
33 customConfigUpdateValidator, 41 customConfigUpdateValidator,
34 asyncMiddleware(updateCustomConfig) 42 asyncMiddleware(updateCustomConfig)
35) 43)
44
36configRouter.delete('/custom', 45configRouter.delete('/custom',
46 openapiOperationDoc({ operationId: 'delCustomConfig' }),
37 authenticate, 47 authenticate,
38 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), 48 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
39 asyncMiddleware(deleteCustomConfig) 49 asyncMiddleware(deleteCustomConfig)
40) 50)
41 51
42async function getConfig (req: express.Request, res: express.Response) { 52async function getConfig (req: express.Request, res: express.Response) {
43 const json = await getServerConfig(req.ip) 53 const json = await ServerConfigManager.Instance.getServerConfig(req.ip)
44 54
45 return res.json(json) 55 return res.json(json)
46} 56}
@@ -67,13 +77,13 @@ function getAbout (req: express.Request, res: express.Response) {
67 } 77 }
68 } 78 }
69 79
70 return res.json(about).end() 80 return res.json(about)
71} 81}
72 82
73function getCustomConfig (req: express.Request, res: express.Response) { 83function getCustomConfig (req: express.Request, res: express.Response) {
74 const data = customConfig() 84 const data = customConfig()
75 85
76 return res.json(data).end() 86 return res.json(data)
77} 87}
78 88
79async function deleteCustomConfig (req: express.Request, res: express.Response) { 89async function deleteCustomConfig (req: express.Request, res: express.Response) {
@@ -171,7 +181,8 @@ function customConfig (): CustomConfig {
171 signup: { 181 signup: {
172 enabled: CONFIG.SIGNUP.ENABLED, 182 enabled: CONFIG.SIGNUP.ENABLED,
173 limit: CONFIG.SIGNUP.LIMIT, 183 limit: CONFIG.SIGNUP.LIMIT,
174 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION 184 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION,
185 minimumAge: CONFIG.SIGNUP.MINIMUM_AGE
175 }, 186 },
176 admin: { 187 admin: {
177 email: CONFIG.ADMIN.EMAIL 188 email: CONFIG.ADMIN.EMAIL
diff --git a/server/controllers/api/custom-page.ts b/server/controllers/api/custom-page.ts
new file mode 100644
index 000000000..c19f03c56
--- /dev/null
+++ b/server/controllers/api/custom-page.ts
@@ -0,0 +1,47 @@
1import * as express from 'express'
2import { ServerConfigManager } from '@server/lib/server-config-manager'
3import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
4import { HttpStatusCode } from '@shared/core-utils'
5import { UserRight } from '@shared/models'
6import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
7
8const customPageRouter = express.Router()
9
10customPageRouter.get('/homepage/instance',
11 asyncMiddleware(getInstanceHomepage)
12)
13
14customPageRouter.put('/homepage/instance',
15 authenticate,
16 ensureUserHasRight(UserRight.MANAGE_INSTANCE_CUSTOM_PAGE),
17 asyncMiddleware(updateInstanceHomepage)
18)
19
20// ---------------------------------------------------------------------------
21
22export {
23 customPageRouter
24}
25
26// ---------------------------------------------------------------------------
27
28async function getInstanceHomepage (req: express.Request, res: express.Response) {
29 const page = await ActorCustomPageModel.loadInstanceHomepage()
30 if (!page) {
31 return res.fail({
32 status: HttpStatusCode.NOT_FOUND_404,
33 message: 'Instance homepage could not be found'
34 })
35 }
36
37 return res.json(page.toFormattedJSON())
38}
39
40async function updateInstanceHomepage (req: express.Request, res: express.Response) {
41 const content = req.body.content
42
43 await ActorCustomPageModel.updateInstanceHomepage(content)
44 ServerConfigManager.Instance.updateHomepageState(content)
45
46 return res.status(HttpStatusCode.NO_CONTENT_204).end()
47}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 7ade1df3a..28378654a 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -8,6 +8,7 @@ import { abuseRouter } from './abuse'
8import { accountsRouter } from './accounts' 8import { accountsRouter } from './accounts'
9import { bulkRouter } from './bulk' 9import { bulkRouter } from './bulk'
10import { configRouter } from './config' 10import { configRouter } from './config'
11import { customPageRouter } from './custom-page'
11import { jobsRouter } from './jobs' 12import { jobsRouter } from './jobs'
12import { oauthClientsRouter } from './oauth-clients' 13import { oauthClientsRouter } from './oauth-clients'
13import { overviewsRouter } from './overviews' 14import { overviewsRouter } from './overviews'
@@ -47,6 +48,7 @@ apiRouter.use('/jobs', jobsRouter)
47apiRouter.use('/search', searchRouter) 48apiRouter.use('/search', searchRouter)
48apiRouter.use('/overviews', overviewsRouter) 49apiRouter.use('/overviews', overviewsRouter)
49apiRouter.use('/plugins', pluginRouter) 50apiRouter.use('/plugins', pluginRouter)
51apiRouter.use('/custom-pages', customPageRouter)
50apiRouter.use('/ping', pong) 52apiRouter.use('/ping', pong)
51apiRouter.use('/*', badRequest) 53apiRouter.use('/*', badRequest)
52 54
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index d7cee1605..9e333322b 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -9,6 +9,7 @@ import {
9 authenticate, 9 authenticate,
10 ensureUserHasRight, 10 ensureUserHasRight,
11 jobsSortValidator, 11 jobsSortValidator,
12 openapiOperationDoc,
12 paginationValidatorBuilder, 13 paginationValidatorBuilder,
13 setDefaultPagination, 14 setDefaultPagination,
14 setDefaultSort 15 setDefaultSort
@@ -18,6 +19,7 @@ import { listJobsValidator } from '../../middlewares/validators/jobs'
18const jobsRouter = express.Router() 19const jobsRouter = express.Router()
19 20
20jobsRouter.get('/:state?', 21jobsRouter.get('/:state?',
22 openapiOperationDoc({ operationId: 'getJobs' }),
21 authenticate, 23 authenticate,
22 ensureUserHasRight(UserRight.MANAGE_JOBS), 24 ensureUserHasRight(UserRight.MANAGE_JOBS),
23 paginationValidatorBuilder([ 'jobs' ]), 25 paginationValidatorBuilder([ 'jobs' ]),
diff --git a/server/controllers/api/oauth-clients.ts b/server/controllers/api/oauth-clients.ts
index c21e2298d..15bbf5c4d 100644
--- a/server/controllers/api/oauth-clients.ts
+++ b/server/controllers/api/oauth-clients.ts
@@ -3,12 +3,13 @@ import { OAuthClientLocal } from '../../../shared'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { CONFIG } from '../../initializers/config' 5import { CONFIG } from '../../initializers/config'
6import { asyncMiddleware } from '../../middlewares' 6import { asyncMiddleware, openapiOperationDoc } from '../../middlewares'
7import { OAuthClientModel } from '../../models/oauth/oauth-client' 7import { OAuthClientModel } from '../../models/oauth/oauth-client'
8 8
9const oauthClientsRouter = express.Router() 9const oauthClientsRouter = express.Router()
10 10
11oauthClientsRouter.get('/local', 11oauthClientsRouter.get('/local',
12 openapiOperationDoc({ operationId: 'getOAuthClient' }),
12 asyncMiddleware(getLocalClient) 13 asyncMiddleware(getLocalClient)
13) 14)
14 15
@@ -24,7 +25,10 @@ async function getLocalClient (req: express.Request, res: express.Response, next
24 // Don't make this check if this is a test instance 25 // Don't make this check if this is a test instance
25 if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { 26 if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) {
26 logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) 27 logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe)
27 return res.type('json').status(HttpStatusCode.FORBIDDEN_403).end() 28 return res.fail({
29 status: HttpStatusCode.FORBIDDEN_403,
30 message: `Getting client tokens for host ${req.get('host')} is forbidden`
31 })
28 } 32 }
29 33
30 const client = await OAuthClientModel.loadFirstClient() 34 const client = await OAuthClientModel.loadFirstClient()
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index a186de010..1e6a02c49 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -1,16 +1,19 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils' 2import { logger } from '@server/helpers/logger'
3import { getFormattedObjects } from '@server/helpers/utils'
4import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index'
5import { PluginManager } from '@server/lib/plugins/plugin-manager'
3import { 6import {
4 asyncMiddleware, 7 asyncMiddleware,
5 authenticate, 8 authenticate,
9 availablePluginsSortValidator,
6 ensureUserHasRight, 10 ensureUserHasRight,
11 openapiOperationDoc,
7 paginationValidator, 12 paginationValidator,
13 pluginsSortValidator,
8 setDefaultPagination, 14 setDefaultPagination,
9 setDefaultSort 15 setDefaultSort
10} from '../../middlewares' 16} from '@server/middlewares'
11import { availablePluginsSortValidator, pluginsSortValidator } from '../../middlewares/validators'
12import { PluginModel } from '../../models/server/plugin'
13import { UserRight } from '../../../shared/models/users'
14import { 17import {
15 existingPluginValidator, 18 existingPluginValidator,
16 installOrUpdatePluginValidator, 19 installOrUpdatePluginValidator,
@@ -18,20 +21,22 @@ import {
18 listPluginsValidator, 21 listPluginsValidator,
19 uninstallPluginValidator, 22 uninstallPluginValidator,
20 updatePluginSettingsValidator 23 updatePluginSettingsValidator
21} from '../../middlewares/validators/plugins' 24} from '@server/middlewares/validators/plugins'
22import { PluginManager } from '../../lib/plugins/plugin-manager' 25import { PluginModel } from '@server/models/server/plugin'
23import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' 26import { HttpStatusCode } from '@shared/core-utils'
24import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' 27import {
25import { logger } from '../../helpers/logger' 28 InstallOrUpdatePlugin,
26import { listAvailablePluginsFromIndex } from '../../lib/plugins/plugin-index' 29 ManagePlugin,
27import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' 30 PeertubePluginIndexList,
28import { RegisteredServerSettings } from '../../../shared/models/plugins/register-server-setting.model' 31 PublicServerSetting,
29import { PublicServerSetting } from '../../../shared/models/plugins/public-server.setting' 32 RegisteredServerSettings,
30import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 33 UserRight
34} from '@shared/models'
31 35
32const pluginRouter = express.Router() 36const pluginRouter = express.Router()
33 37
34pluginRouter.get('/available', 38pluginRouter.get('/available',
39 openapiOperationDoc({ operationId: 'getAvailablePlugins' }),
35 authenticate, 40 authenticate,
36 ensureUserHasRight(UserRight.MANAGE_PLUGINS), 41 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
37 listAvailablePluginsValidator, 42 listAvailablePluginsValidator,
@@ -43,6 +48,7 @@ pluginRouter.get('/available',
43) 48)
44 49
45pluginRouter.get('/', 50pluginRouter.get('/',
51 openapiOperationDoc({ operationId: 'getPlugins' }),
46 authenticate, 52 authenticate,
47 ensureUserHasRight(UserRight.MANAGE_PLUGINS), 53 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
48 listPluginsValidator, 54 listPluginsValidator,
@@ -81,6 +87,7 @@ pluginRouter.get('/:npmName',
81) 87)
82 88
83pluginRouter.post('/install', 89pluginRouter.post('/install',
90 openapiOperationDoc({ operationId: 'addPlugin' }),
84 authenticate, 91 authenticate,
85 ensureUserHasRight(UserRight.MANAGE_PLUGINS), 92 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
86 installOrUpdatePluginValidator, 93 installOrUpdatePluginValidator,
@@ -88,6 +95,7 @@ pluginRouter.post('/install',
88) 95)
89 96
90pluginRouter.post('/update', 97pluginRouter.post('/update',
98 openapiOperationDoc({ operationId: 'updatePlugin' }),
91 authenticate, 99 authenticate,
92 ensureUserHasRight(UserRight.MANAGE_PLUGINS), 100 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
93 installOrUpdatePluginValidator, 101 installOrUpdatePluginValidator,
@@ -95,6 +103,7 @@ pluginRouter.post('/update',
95) 103)
96 104
97pluginRouter.post('/uninstall', 105pluginRouter.post('/uninstall',
106 openapiOperationDoc({ operationId: 'uninstallPlugin' }),
98 authenticate, 107 authenticate,
99 ensureUserHasRight(UserRight.MANAGE_PLUGINS), 108 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
100 uninstallPluginValidator, 109 uninstallPluginValidator,
@@ -141,7 +150,7 @@ async function installPlugin (req: express.Request, res: express.Response) {
141 return res.json(plugin.toFormattedJSON()) 150 return res.json(plugin.toFormattedJSON())
142 } catch (err) { 151 } catch (err) {
143 logger.warn('Cannot install plugin %s.', toInstall, { err }) 152 logger.warn('Cannot install plugin %s.', toInstall, { err })
144 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) 153 return res.fail({ message: 'Cannot install plugin ' + toInstall })
145 } 154 }
146} 155}
147 156
@@ -156,7 +165,7 @@ async function updatePlugin (req: express.Request, res: express.Response) {
156 return res.json(plugin.toFormattedJSON()) 165 return res.json(plugin.toFormattedJSON())
157 } catch (err) { 166 } catch (err) {
158 logger.warn('Cannot update plugin %s.', toUpdate, { err }) 167 logger.warn('Cannot update plugin %s.', toUpdate, { err })
159 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) 168 return res.fail({ message: 'Cannot update plugin ' + toUpdate })
160 } 169 }
161} 170}
162 171
@@ -165,7 +174,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) {
165 174
166 await PluginManager.Instance.uninstall(body.npmName) 175 await PluginManager.Instance.uninstall(body.npmName)
167 176
168 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 177 return res.status(HttpStatusCode.NO_CONTENT_204).end()
169} 178}
170 179
171function getPublicPluginSettings (req: express.Request, res: express.Response) { 180function getPublicPluginSettings (req: express.Request, res: express.Response) {
@@ -194,7 +203,7 @@ async function updatePluginSettings (req: express.Request, res: express.Response
194 203
195 await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings) 204 await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings)
196 205
197 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 206 return res.status(HttpStatusCode.NO_CONTENT_204).end()
198} 207}
199 208
200async function listAvailablePlugins (req: express.Request, res: express.Response) { 209async function listAvailablePlugins (req: express.Request, res: express.Response) {
@@ -203,8 +212,10 @@ async function listAvailablePlugins (req: express.Request, res: express.Response
203 const resultList = await listAvailablePluginsFromIndex(query) 212 const resultList = await listAvailablePluginsFromIndex(query)
204 213
205 if (!resultList) { 214 if (!resultList) {
206 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) 215 return res.fail({
207 .json({ error: 'Plugin index unavailable. Please retry later' }) 216 status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
217 message: 'Plugin index unavailable. Please retry later'
218 })
208 } 219 }
209 220
210 return res.json(resultList) 221 return res.json(resultList)
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
deleted file mode 100644
index f0cdf3a89..000000000
--- a/server/controllers/api/search.ts
+++ /dev/null
@@ -1,286 +0,0 @@
1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { doJSONRequest } from '@server/helpers/requests'
4import { CONFIG } from '@server/initializers/config'
5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos'
6import { Hooks } from '@server/lib/plugins/hooks'
7import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
8import { getServerActor } from '@server/models/application/application'
9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
11import { ResultList, Video, VideoChannel } from '@shared/models'
12import { SearchTargetQuery } from '@shared/models/search/search-target-query.model'
13import { VideoChannelsSearchQuery, VideosSearchQuery } from '../../../shared/models/search'
14import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
15import { logger } from '../../helpers/logger'
16import { getFormattedObjects } from '../../helpers/utils'
17import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
18import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub/actor'
19import {
20 asyncMiddleware,
21 commonVideosFiltersValidator,
22 optionalAuthenticate,
23 paginationValidator,
24 setDefaultPagination,
25 setDefaultSearchSort,
26 videoChannelsListSearchValidator,
27 videoChannelsSearchSortValidator,
28 videosSearchSortValidator,
29 videosSearchValidator
30} from '../../middlewares'
31import { VideoModel } from '../../models/video/video'
32import { VideoChannelModel } from '../../models/video/video-channel'
33import { MChannelAccountDefault, MVideoAccountLightBlacklistAllFiles } from '../../types/models'
34
35const searchRouter = express.Router()
36
37searchRouter.get('/videos',
38 paginationValidator,
39 setDefaultPagination,
40 videosSearchSortValidator,
41 setDefaultSearchSort,
42 optionalAuthenticate,
43 commonVideosFiltersValidator,
44 videosSearchValidator,
45 asyncMiddleware(searchVideos)
46)
47
48searchRouter.get('/video-channels',
49 paginationValidator,
50 setDefaultPagination,
51 videoChannelsSearchSortValidator,
52 setDefaultSearchSort,
53 optionalAuthenticate,
54 videoChannelsListSearchValidator,
55 asyncMiddleware(searchVideoChannels)
56)
57
58// ---------------------------------------------------------------------------
59
60export { searchRouter }
61
62// ---------------------------------------------------------------------------
63
64function searchVideoChannels (req: express.Request, res: express.Response) {
65 const query: VideoChannelsSearchQuery = req.query
66 const search = query.search
67
68 const isURISearch = search.startsWith('http://') || search.startsWith('https://')
69
70 const parts = search.split('@')
71
72 // Handle strings like @toto@example.com
73 if (parts.length === 3 && parts[0].length === 0) parts.shift()
74 const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' '))
75
76 if (isURISearch || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res)
77
78 // @username -> username to search in DB
79 if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '')
80
81 if (isSearchIndexSearch(query)) {
82 return searchVideoChannelsIndex(query, res)
83 }
84
85 return searchVideoChannelsDB(query, res)
86}
87
88async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
89 const result = await buildMutedForSearchIndex(res)
90
91 const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
92
93 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
94
95 try {
96 logger.debug('Doing video channels search index request on %s.', url, { body })
97
98 const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
99 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
100
101 return res.json(jsonResult)
102 } catch (err) {
103 logger.warn('Cannot use search index to make video channels search.', { err })
104
105 return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
106 }
107}
108
109async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
110 const serverActor = await getServerActor()
111
112 const apiOptions = await Hooks.wrapObject({
113 actorId: serverActor.id,
114 search: query.search,
115 start: query.start,
116 count: query.count,
117 sort: query.sort
118 }, 'filter:api.search.video-channels.local.list.params')
119
120 const resultList = await Hooks.wrapPromiseFun(
121 VideoChannelModel.searchForApi,
122 apiOptions,
123 'filter:api.search.video-channels.local.list.result'
124 )
125
126 return res.json(getFormattedObjects(resultList.data, resultList.total))
127}
128
129async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) {
130 let videoChannel: MChannelAccountDefault
131 let uri = search
132
133 if (isWebfingerSearch) {
134 try {
135 uri = await loadActorUrlOrGetFromWebfinger(search)
136 } catch (err) {
137 logger.warn('Cannot load actor URL or get from webfinger.', { search, err })
138
139 return res.json({ total: 0, data: [] })
140 }
141 }
142
143 if (isUserAbleToSearchRemoteURI(res)) {
144 try {
145 const actor = await getOrCreateActorAndServerAndModel(uri, 'all', true, true)
146 videoChannel = actor.VideoChannel
147 } catch (err) {
148 logger.info('Cannot search remote video channel %s.', uri, { err })
149 }
150 } else {
151 videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(uri)
152 }
153
154 return res.json({
155 total: videoChannel ? 1 : 0,
156 data: videoChannel ? [ videoChannel.toFormattedJSON() ] : []
157 })
158}
159
160function searchVideos (req: express.Request, res: express.Response) {
161 const query: VideosSearchQuery = req.query
162 const search = query.search
163
164 if (search && (search.startsWith('http://') || search.startsWith('https://'))) {
165 return searchVideoURI(search, res)
166 }
167
168 if (isSearchIndexSearch(query)) {
169 return searchVideosIndex(query, res)
170 }
171
172 return searchVideosDB(query, res)
173}
174
175async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
176 const result = await buildMutedForSearchIndex(res)
177
178 let body: VideosSearchQuery = Object.assign(query, result)
179
180 // Use the default instance NSFW policy if not specified
181 if (!body.nsfw) {
182 const nsfwPolicy = res.locals.oauth
183 ? res.locals.oauth.token.User.nsfwPolicy
184 : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
185
186 body.nsfw = nsfwPolicy === 'do_not_list'
187 ? 'false'
188 : 'both'
189 }
190
191 body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
192
193 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
194
195 try {
196 logger.debug('Doing videos search index request on %s.', url, { body })
197
198 const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
199 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
200
201 return res.json(jsonResult)
202 } catch (err) {
203 logger.warn('Cannot use search index to make video search.', { err })
204
205 return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
206 }
207}
208
209async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
210 const apiOptions = await Hooks.wrapObject(Object.assign(query, {
211 includeLocalVideos: true,
212 nsfw: buildNSFWFilter(res, query.nsfw),
213 filter: query.filter,
214 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
215 }), 'filter:api.search.videos.local.list.params')
216
217 const resultList = await Hooks.wrapPromiseFun(
218 VideoModel.searchAndPopulateAccountAndServer,
219 apiOptions,
220 'filter:api.search.videos.local.list.result'
221 )
222
223 return res.json(getFormattedObjects(resultList.data, resultList.total))
224}
225
226async function searchVideoURI (url: string, res: express.Response) {
227 let video: MVideoAccountLightBlacklistAllFiles
228
229 // Check if we can fetch a remote video with the URL
230 if (isUserAbleToSearchRemoteURI(res)) {
231 try {
232 const syncParam = {
233 likes: false,
234 dislikes: false,
235 shares: false,
236 comments: false,
237 thumbnail: true,
238 refreshVideo: false
239 }
240
241 const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam })
242 video = result ? result.video : undefined
243 } catch (err) {
244 logger.info('Cannot search remote video %s.', url, { err })
245 }
246 } else {
247 video = await VideoModel.loadByUrlAndPopulateAccount(url)
248 }
249
250 return res.json({
251 total: video ? 1 : 0,
252 data: video ? [ video.toFormattedJSON() ] : []
253 })
254}
255
256function isSearchIndexSearch (query: SearchTargetQuery) {
257 if (query.searchTarget === 'search-index') return true
258
259 const searchIndexConfig = CONFIG.SEARCH.SEARCH_INDEX
260
261 if (searchIndexConfig.ENABLED !== true) return false
262
263 if (searchIndexConfig.DISABLE_LOCAL_SEARCH) return true
264 if (searchIndexConfig.IS_DEFAULT_SEARCH && !query.searchTarget) return true
265
266 return false
267}
268
269async function buildMutedForSearchIndex (res: express.Response) {
270 const serverActor = await getServerActor()
271 const accountIds = [ serverActor.Account.id ]
272
273 if (res.locals.oauth) {
274 accountIds.push(res.locals.oauth.token.User.Account.id)
275 }
276
277 const [ blockedHosts, blockedAccounts ] = await Promise.all([
278 ServerBlocklistModel.listHostsBlockedBy(accountIds),
279 AccountBlocklistModel.listHandlesBlockedBy(accountIds)
280 ])
281
282 return {
283 blockedHosts,
284 blockedAccounts
285 }
286}
diff --git a/server/controllers/api/search/index.ts b/server/controllers/api/search/index.ts
new file mode 100644
index 000000000..67adbb307
--- /dev/null
+++ b/server/controllers/api/search/index.ts
@@ -0,0 +1,16 @@
1import * as express from 'express'
2import { searchChannelsRouter } from './search-video-channels'
3import { searchPlaylistsRouter } from './search-video-playlists'
4import { searchVideosRouter } from './search-videos'
5
6const searchRouter = express.Router()
7
8searchRouter.use('/', searchVideosRouter)
9searchRouter.use('/', searchChannelsRouter)
10searchRouter.use('/', searchPlaylistsRouter)
11
12// ---------------------------------------------------------------------------
13
14export {
15 searchRouter
16}
diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts
new file mode 100644
index 000000000..16beeed60
--- /dev/null
+++ b/server/controllers/api/search/search-video-channels.ts
@@ -0,0 +1,150 @@
1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { doJSONRequest } from '@server/helpers/requests'
4import { CONFIG } from '@server/initializers/config'
5import { WEBSERVER } from '@server/initializers/constants'
6import { Hooks } from '@server/lib/plugins/hooks'
7import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
8import { getServerActor } from '@server/models/application/application'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10import { ResultList, VideoChannel } from '@shared/models'
11import { VideoChannelsSearchQuery } from '../../../../shared/models/search'
12import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
13import { logger } from '../../../helpers/logger'
14import { getFormattedObjects } from '../../../helpers/utils'
15import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../../lib/activitypub/actors'
16import {
17 asyncMiddleware,
18 openapiOperationDoc,
19 optionalAuthenticate,
20 paginationValidator,
21 setDefaultPagination,
22 setDefaultSearchSort,
23 videoChannelsListSearchValidator,
24 videoChannelsSearchSortValidator
25} from '../../../middlewares'
26import { VideoChannelModel } from '../../../models/video/video-channel'
27import { MChannelAccountDefault } from '../../../types/models'
28
29const searchChannelsRouter = express.Router()
30
31searchChannelsRouter.get('/video-channels',
32 openapiOperationDoc({ operationId: 'searchChannels' }),
33 paginationValidator,
34 setDefaultPagination,
35 videoChannelsSearchSortValidator,
36 setDefaultSearchSort,
37 optionalAuthenticate,
38 videoChannelsListSearchValidator,
39 asyncMiddleware(searchVideoChannels)
40)
41
42// ---------------------------------------------------------------------------
43
44export { searchChannelsRouter }
45
46// ---------------------------------------------------------------------------
47
48function searchVideoChannels (req: express.Request, res: express.Response) {
49 const query: VideoChannelsSearchQuery = req.query
50 const search = query.search
51
52 const parts = search.split('@')
53
54 // Handle strings like @toto@example.com
55 if (parts.length === 3 && parts[0].length === 0) parts.shift()
56 const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' '))
57
58 if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res)
59
60 // @username -> username to search in DB
61 if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '')
62
63 if (isSearchIndexSearch(query)) {
64 return searchVideoChannelsIndex(query, res)
65 }
66
67 return searchVideoChannelsDB(query, res)
68}
69
70async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
71 const result = await buildMutedForSearchIndex(res)
72
73 const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
74
75 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
76
77 try {
78 logger.debug('Doing video channels search index request on %s.', url, { body })
79
80 const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
81 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
82
83 return res.json(jsonResult)
84 } catch (err) {
85 logger.warn('Cannot use search index to make video channels search.', { err })
86
87 return res.fail({
88 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
89 message: 'Cannot use search index to make video channels search'
90 })
91 }
92}
93
94async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
95 const serverActor = await getServerActor()
96
97 const apiOptions = await Hooks.wrapObject({
98 actorId: serverActor.id,
99 search: query.search,
100 start: query.start,
101 count: query.count,
102 sort: query.sort
103 }, 'filter:api.search.video-channels.local.list.params')
104
105 const resultList = await Hooks.wrapPromiseFun(
106 VideoChannelModel.searchForApi,
107 apiOptions,
108 'filter:api.search.video-channels.local.list.result'
109 )
110
111 return res.json(getFormattedObjects(resultList.data, resultList.total))
112}
113
114async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) {
115 let videoChannel: MChannelAccountDefault
116 let uri = search
117
118 if (isWebfingerSearch) {
119 try {
120 uri = await loadActorUrlOrGetFromWebfinger(search)
121 } catch (err) {
122 logger.warn('Cannot load actor URL or get from webfinger.', { search, err })
123
124 return res.json({ total: 0, data: [] })
125 }
126 }
127
128 if (isUserAbleToSearchRemoteURI(res)) {
129 try {
130 const actor = await getOrCreateAPActor(uri, 'all', true, true)
131 videoChannel = actor.VideoChannel
132 } catch (err) {
133 logger.info('Cannot search remote video channel %s.', uri, { err })
134 }
135 } else {
136 videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(uri))
137 }
138
139 return res.json({
140 total: videoChannel ? 1 : 0,
141 data: videoChannel ? [ videoChannel.toFormattedJSON() ] : []
142 })
143}
144
145function sanitizeLocalUrl (url: string) {
146 if (!url) return ''
147
148 // Handle alternative channel URLs
149 return url.replace(new RegExp('^' + WEBSERVER.URL + '/c/'), WEBSERVER.URL + '/video-channels/')
150}
diff --git a/server/controllers/api/search/search-video-playlists.ts b/server/controllers/api/search/search-video-playlists.ts
new file mode 100644
index 000000000..b231ff1e2
--- /dev/null
+++ b/server/controllers/api/search/search-video-playlists.ts
@@ -0,0 +1,129 @@
1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils'
4import { logger } from '@server/helpers/logger'
5import { doJSONRequest } from '@server/helpers/requests'
6import { getFormattedObjects } from '@server/helpers/utils'
7import { CONFIG } from '@server/initializers/config'
8import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get'
9import { Hooks } from '@server/lib/plugins/hooks'
10import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
11import { getServerActor } from '@server/models/application/application'
12import { VideoPlaylistModel } from '@server/models/video/video-playlist'
13import { MVideoPlaylistFullSummary } from '@server/types/models'
14import { HttpStatusCode } from '@shared/core-utils'
15import { ResultList, VideoPlaylist, VideoPlaylistsSearchQuery } from '@shared/models'
16import {
17 asyncMiddleware,
18 openapiOperationDoc,
19 optionalAuthenticate,
20 paginationValidator,
21 setDefaultPagination,
22 setDefaultSearchSort,
23 videoPlaylistsListSearchValidator,
24 videoPlaylistsSearchSortValidator
25} from '../../../middlewares'
26import { WEBSERVER } from '@server/initializers/constants'
27
28const searchPlaylistsRouter = express.Router()
29
30searchPlaylistsRouter.get('/video-playlists',
31 openapiOperationDoc({ operationId: 'searchPlaylists' }),
32 paginationValidator,
33 setDefaultPagination,
34 videoPlaylistsSearchSortValidator,
35 setDefaultSearchSort,
36 optionalAuthenticate,
37 videoPlaylistsListSearchValidator,
38 asyncMiddleware(searchVideoPlaylists)
39)
40
41// ---------------------------------------------------------------------------
42
43export { searchPlaylistsRouter }
44
45// ---------------------------------------------------------------------------
46
47function searchVideoPlaylists (req: express.Request, res: express.Response) {
48 const query: VideoPlaylistsSearchQuery = req.query
49 const search = query.search
50
51 if (isURISearch(search)) return searchVideoPlaylistsURI(search, res)
52
53 if (isSearchIndexSearch(query)) {
54 return searchVideoPlaylistsIndex(query, res)
55 }
56
57 return searchVideoPlaylistsDB(query, res)
58}
59
60async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res: express.Response) {
61 const result = await buildMutedForSearchIndex(res)
62
63 const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params')
64
65 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-playlists'
66
67 try {
68 logger.debug('Doing video playlists search index request on %s.', url, { body })
69
70 const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoPlaylist>>(url, { method: 'POST', json: body })
71 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-playlists.index.list.result')
72
73 return res.json(jsonResult)
74 } catch (err) {
75 logger.warn('Cannot use search index to make video playlists search.', { err })
76
77 return res.fail({
78 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
79 message: 'Cannot use search index to make video playlists search'
80 })
81 }
82}
83
84async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: express.Response) {
85 const serverActor = await getServerActor()
86
87 const apiOptions = await Hooks.wrapObject({
88 followerActorId: serverActor.id,
89 search: query.search,
90 start: query.start,
91 count: query.count,
92 sort: query.sort
93 }, 'filter:api.search.video-playlists.local.list.params')
94
95 const resultList = await Hooks.wrapPromiseFun(
96 VideoPlaylistModel.searchForApi,
97 apiOptions,
98 'filter:api.search.video-playlists.local.list.result'
99 )
100
101 return res.json(getFormattedObjects(resultList.data, resultList.total))
102}
103
104async function searchVideoPlaylistsURI (search: string, res: express.Response) {
105 let videoPlaylist: MVideoPlaylistFullSummary
106
107 if (isUserAbleToSearchRemoteURI(res)) {
108 try {
109 videoPlaylist = await getOrCreateAPVideoPlaylist(search)
110 } catch (err) {
111 logger.info('Cannot search remote video playlist %s.', search, { err })
112 }
113 } else {
114 videoPlaylist = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(sanitizeLocalUrl(search))
115 }
116
117 return res.json({
118 total: videoPlaylist ? 1 : 0,
119 data: videoPlaylist ? [ videoPlaylist.toFormattedJSON() ] : []
120 })
121}
122
123function sanitizeLocalUrl (url: string) {
124 if (!url) return ''
125
126 // Handle alternative channel URLs
127 return url.replace(new RegExp('^' + WEBSERVER.URL + '/videos/watch/playlist/'), WEBSERVER.URL + '/video-playlists/')
128 .replace(new RegExp('^' + WEBSERVER.URL + '/w/p/'), WEBSERVER.URL + '/video-playlists/')
129}
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts
new file mode 100644
index 000000000..b626baa28
--- /dev/null
+++ b/server/controllers/api/search/search-videos.ts
@@ -0,0 +1,153 @@
1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { doJSONRequest } from '@server/helpers/requests'
4import { CONFIG } from '@server/initializers/config'
5import { WEBSERVER } from '@server/initializers/constants'
6import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
7import { Hooks } from '@server/lib/plugins/hooks'
8import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10import { ResultList, Video } from '@shared/models'
11import { VideosSearchQuery } from '../../../../shared/models/search'
12import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
13import { logger } from '../../../helpers/logger'
14import { getFormattedObjects } from '../../../helpers/utils'
15import {
16 asyncMiddleware,
17 commonVideosFiltersValidator,
18 openapiOperationDoc,
19 optionalAuthenticate,
20 paginationValidator,
21 setDefaultPagination,
22 setDefaultSearchSort,
23 videosSearchSortValidator,
24 videosSearchValidator
25} from '../../../middlewares'
26import { VideoModel } from '../../../models/video/video'
27import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models'
28
29const searchVideosRouter = express.Router()
30
31searchVideosRouter.get('/videos',
32 openapiOperationDoc({ operationId: 'searchVideos' }),
33 paginationValidator,
34 setDefaultPagination,
35 videosSearchSortValidator,
36 setDefaultSearchSort,
37 optionalAuthenticate,
38 commonVideosFiltersValidator,
39 videosSearchValidator,
40 asyncMiddleware(searchVideos)
41)
42
43// ---------------------------------------------------------------------------
44
45export { searchVideosRouter }
46
47// ---------------------------------------------------------------------------
48
49function searchVideos (req: express.Request, res: express.Response) {
50 const query: VideosSearchQuery = req.query
51 const search = query.search
52
53 if (isURISearch(search)) {
54 return searchVideoURI(search, res)
55 }
56
57 if (isSearchIndexSearch(query)) {
58 return searchVideosIndex(query, res)
59 }
60
61 return searchVideosDB(query, res)
62}
63
64async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
65 const result = await buildMutedForSearchIndex(res)
66
67 let body: VideosSearchQuery = Object.assign(query, result)
68
69 // Use the default instance NSFW policy if not specified
70 if (!body.nsfw) {
71 const nsfwPolicy = res.locals.oauth
72 ? res.locals.oauth.token.User.nsfwPolicy
73 : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
74
75 body.nsfw = nsfwPolicy === 'do_not_list'
76 ? 'false'
77 : 'both'
78 }
79
80 body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
81
82 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
83
84 try {
85 logger.debug('Doing videos search index request on %s.', url, { body })
86
87 const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
88 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
89
90 return res.json(jsonResult)
91 } catch (err) {
92 logger.warn('Cannot use search index to make video search.', { err })
93
94 return res.fail({
95 status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
96 message: 'Cannot use search index to make video search'
97 })
98 }
99}
100
101async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
102 const apiOptions = await Hooks.wrapObject(Object.assign(query, {
103 includeLocalVideos: true,
104 nsfw: buildNSFWFilter(res, query.nsfw),
105 filter: query.filter,
106 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
107 }), 'filter:api.search.videos.local.list.params')
108
109 const resultList = await Hooks.wrapPromiseFun(
110 VideoModel.searchAndPopulateAccountAndServer,
111 apiOptions,
112 'filter:api.search.videos.local.list.result'
113 )
114
115 return res.json(getFormattedObjects(resultList.data, resultList.total))
116}
117
118async function searchVideoURI (url: string, res: express.Response) {
119 let video: MVideoAccountLightBlacklistAllFiles
120
121 // Check if we can fetch a remote video with the URL
122 if (isUserAbleToSearchRemoteURI(res)) {
123 try {
124 const syncParam = {
125 likes: false,
126 dislikes: false,
127 shares: false,
128 comments: false,
129 thumbnail: true,
130 refreshVideo: false
131 }
132
133 const result = await getOrCreateAPVideo({ videoObject: url, syncParam })
134 video = result ? result.video : undefined
135 } catch (err) {
136 logger.info('Cannot search remote video %s.', url, { err })
137 }
138 } else {
139 video = await VideoModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(url))
140 }
141
142 return res.json({
143 total: video ? 1 : 0,
144 data: video ? [ video.toFormattedJSON() ] : []
145 })
146}
147
148function sanitizeLocalUrl (url: string) {
149 if (!url) return ''
150
151 // Handle alternative video URLs
152 return url.replace(new RegExp('^' + WEBSERVER.URL + '/w/'), WEBSERVER.URL + '/videos/watch/')
153}
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts
index ff0d9ca3c..a6e9147f3 100644
--- a/server/controllers/api/server/debug.ts
+++ b/server/controllers/api/server/debug.ts
@@ -1,5 +1,6 @@
1import { InboxManager } from '@server/lib/activitypub/inbox-manager' 1import { InboxManager } from '@server/lib/activitypub/inbox-manager'
2import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' 2import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
3import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
3import { SendDebugCommand } from '@shared/models' 4import { SendDebugCommand } from '@shared/models'
4import * as express from 'express' 5import * as express from 'express'
5import { UserRight } from '../../../../shared/models/users' 6import { UserRight } from '../../../../shared/models/users'
@@ -41,5 +42,5 @@ async function runCommand (req: express.Request, res: express.Response) {
41 await RemoveDanglingResumableUploadsScheduler.Instance.execute() 42 await RemoveDanglingResumableUploadsScheduler.Instance.execute()
42 } 43 }
43 44
44 return res.sendStatus(204) 45 return res.status(HttpStatusCode.NO_CONTENT_204).end()
45} 46}
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index 80025bc5b..12357a2ca 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -1,9 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getServerActor } from '@server/models/application/application'
3import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
2import { UserRight } from '../../../../shared/models/users' 4import { UserRight } from '../../../../shared/models/users'
3import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
5import { SERVER_ACTOR_NAME } from '../../../initializers/constants' 7import { SERVER_ACTOR_NAME } from '../../../initializers/constants'
8import { sequelizeTypescript } from '../../../initializers/database'
9import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
6import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' 10import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
11import { JobQueue } from '../../../lib/job-queue'
12import { removeRedundanciesOfServer } from '../../../lib/redundancy'
7import { 13import {
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'
25import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 31import { ActorFollowModel } from '../../../models/actor/actor-follow'
26import { JobQueue } from '../../../lib/job-queue'
27import { removeRedundanciesOfServer } from '../../../lib/redundancy'
28import { sequelizeTypescript } from '../../../initializers/database'
29import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
30import { getServerActor } from '@server/models/application/application'
31import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
32 32
33const serverFollowsRouter = express.Router() 33const serverFollowsRouter = express.Router()
34serverFollowsRouter.get('/following', 34serverFollowsRouter.get('/following',
@@ -176,7 +176,7 @@ async function removeOrRejectFollower (req: express.Request, res: express.Respon
176async function acceptFollower (req: express.Request, res: express.Response) { 176async function acceptFollower (req: express.Request, res: express.Response) {
177 const follow = res.locals.follow 177 const follow = res.locals.follow
178 178
179 await sendAccept(follow) 179 sendAccept(follow)
180 180
181 follow.state = 'accepted' 181 follow.state = 'accepted'
182 await follow.save() 182 await follow.save()
diff --git a/server/controllers/api/server/redundancy.ts b/server/controllers/api/server/redundancy.ts
index 7c13dc21b..bc593ad43 100644
--- a/server/controllers/api/server/redundancy.ts
+++ b/server/controllers/api/server/redundancy.ts
@@ -90,13 +90,13 @@ async function addVideoRedundancy (req: express.Request, res: express.Response)
90 payload 90 payload
91 }) 91 })
92 92
93 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 93 return res.status(HttpStatusCode.NO_CONTENT_204).end()
94} 94}
95 95
96async function removeVideoRedundancyController (req: express.Request, res: express.Response) { 96async function removeVideoRedundancyController (req: express.Request, res: express.Response) {
97 await removeVideoRedundancy(res.locals.videoRedundancy) 97 await removeVideoRedundancy(res.locals.videoRedundancy)
98 98
99 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 99 return res.status(HttpStatusCode.NO_CONTENT_204).end()
100} 100}
101 101
102async function updateRedundancy (req: express.Request, res: express.Response) { 102async function updateRedundancy (req: express.Request, res: express.Response) {
@@ -110,5 +110,5 @@ async function updateRedundancy (req: express.Request, res: express.Response) {
110 removeRedundanciesOfServer(server.id) 110 removeRedundanciesOfServer(server.id)
111 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) 111 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err }))
112 112
113 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 113 return res.status(HttpStatusCode.NO_CONTENT_204).end()
114} 114}
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 @@
1import 'multer' 1import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { logger } from '@server/helpers/logger' 3import { logger } from '@server/helpers/logger'
4import { UserNotificationModel } from '@server/models/account/user-notification' 4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { UserRight } from '../../../../shared/models/users' 6import { UserRight } from '../../../../shared/models/users'
7import { getFormattedObjects } from '../../../helpers/utils' 7import { getFormattedObjects } from '../../../helpers/utils'
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index e2b1ea7cd..d907b49bf 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'
48import { UserModel } from '../../../models/account/user' 48import { UserModel } from '../../../models/user/user'
49import { meRouter } from './me' 49import { meRouter } from './me'
50import { myAbusesRouter } from './my-abuses' 50import { myAbusesRouter } from './my-abuses'
51import { myBlocklistRouter } from './my-blocklist' 51import { myBlocklistRouter } from './my-blocklist'
@@ -314,7 +314,7 @@ async function removeUser (req: express.Request, res: express.Response) {
314 314
315 Hooks.runAction('action:api.user.deleted', { user }) 315 Hooks.runAction('action:api.user.deleted', { user })
316 316
317 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 317 return res.status(HttpStatusCode.NO_CONTENT_204).end()
318} 318}
319 319
320async function updateUser (req: express.Request, res: express.Response) { 320async function updateUser (req: express.Request, res: express.Response) {
@@ -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
@@ -343,7 +349,7 @@ async function updateUser (req: express.Request, res: express.Response) {
343 349
344 // Don't need to send this update to followers, these attributes are not federated 350 // Don't need to send this update to followers, these attributes are not federated
345 351
346 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 352 return res.status(HttpStatusCode.NO_CONTENT_204).end()
347} 353}
348 354
349async function askResetUserPassword (req: express.Request, res: express.Response) { 355async function askResetUserPassword (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 0763d1900..1f2b2f9dd 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
11import { MIMETYPES } from '../../../initializers/constants' 11import { MIMETYPES } from '../../../initializers/constants'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { sendUpdateActor } from '../../../lib/activitypub/send' 13import { sendUpdateActor } from '../../../lib/activitypub/send'
14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image' 14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/local-actor'
15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' 15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
16import { 16import {
17 asyncMiddleware, 17 asyncMiddleware,
@@ -28,9 +28,10 @@ import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } fro
28import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' 28import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
29import { AccountModel } from '../../../models/account/account' 29import { AccountModel } from '../../../models/account/account'
30import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 30import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
31import { UserModel } from '../../../models/account/user' 31import { UserModel } from '../../../models/user/user'
32import { VideoModel } from '../../../models/video/video' 32import { VideoModel } from '../../../models/video/video'
33import { VideoImportModel } from '../../../models/video/video-import' 33import { VideoImportModel } from '../../../models/video/video-import'
34import { AttributesOnly } from '@shared/core-utils'
34 35
35const auditLogger = auditLoggerFactory('users') 36const auditLogger = auditLoggerFactory('users')
36 37
@@ -182,7 +183,7 @@ async function deleteMe (req: express.Request, res: express.Response) {
182 183
183 await user.destroy() 184 await user.destroy()
184 185
185 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 186 return res.status(HttpStatusCode.NO_CONTENT_204).end()
186} 187}
187 188
188async function updateMe (req: express.Request, res: express.Response) { 189async function updateMe (req: express.Request, res: express.Response) {
@@ -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,22 +222,22 @@ 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) {
230 await sendVerifyUserEmail(user, true) 237 await sendVerifyUserEmail(user, true)
231 } 238 }
232 239
233 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 240 return res.status(HttpStatusCode.NO_CONTENT_204).end()
234} 241}
235 242
236async function updateMyAvatar (req: express.Request, res: express.Response) { 243async function updateMyAvatar (req: express.Request, res: express.Response) {
@@ -250,5 +257,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
250 const userAccount = await AccountModel.load(user.Account.id) 257 const userAccount = await AccountModel.load(user.Account.id)
251 await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) 258 await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
252 259
253 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 260 return res.status(HttpStatusCode.NO_CONTENT_204).end()
254} 261}
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 {
20import { AccountBlocklistModel } from '../../../models/account/account-blocklist' 20import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
21import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' 21import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
22import { ServerBlocklistModel } from '../../../models/server/server-blocklist' 22import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
23import { UserNotificationModel } from '@server/models/account/user-notification' 23import { UserNotificationModel } from '@server/models/user/user-notification'
24import { logger } from '@server/helpers/logger' 24import { logger } from '@server/helpers/logger'
25import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 25import { 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'
11import { getFormattedObjects } from '../../../helpers/utils' 11import { getFormattedObjects } from '../../../helpers/utils'
12import { UserVideoHistoryModel } from '../../../models/account/user-video-history' 12import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
13import { sequelizeTypescript } from '../../../initializers/database' 13import { sequelizeTypescript } from '../../../initializers/database'
14import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 14import { 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 @@
1import * as express from 'express'
2import 'multer' 1import 'multer'
2import * as express from 'express'
3import { UserNotificationModel } from '@server/models/user/user-notification'
4import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
5import { UserNotificationSetting } from '../../../../shared/models/users'
6import { getFormattedObjects } from '../../../helpers/utils'
3import { 7import {
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'
12import { getFormattedObjects } from '../../../helpers/utils'
13import { UserNotificationModel } from '../../../models/account/user-notification'
14import { meRouter } from './me'
15import { 16import {
16 listUserNotificationsValidator, 17 listUserNotificationsValidator,
17 markAsReadUserNotificationsValidator, 18 markAsReadUserNotificationsValidator,
18 updateNotificationSettingsValidator 19 updateNotificationSettingsValidator
19} from '../../../middlewares/validators/user-notifications' 20} from '../../../middlewares/validators/user-notifications'
20import { UserNotificationSetting } from '../../../../shared/models/users' 21import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting'
21import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting' 22import { meRouter } from './me'
22import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
23 23
24const myNotificationsRouter = express.Router() 24const 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'
30import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 30import { ActorFollowModel } from '../../../models/actor/actor-follow'
31import { VideoModel } from '../../../models/video/video' 31import { VideoModel } from '../../../models/video/video'
32 32
33const mySubscriptionsRouter = express.Router() 33const mySubscriptionsRouter = express.Router()
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 694bb0a92..b405ddbf4 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -1,13 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { v4 as uuidv4 } from 'uuid'
4import { logger } from '@server/helpers/logger' 3import { logger } from '@server/helpers/logger'
4import { buildUUID } from '@server/helpers/uuid'
5import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
6import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' 6import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
7import { handleOAuthToken } from '@server/lib/auth/oauth' 7import { handleOAuthToken } from '@server/lib/auth/oauth'
8import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' 8import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
9import { Hooks } from '@server/lib/plugins/hooks' 9import { Hooks } from '@server/lib/plugins/hooks'
10import { asyncMiddleware, authenticate } from '@server/middlewares' 10import { asyncMiddleware, authenticate, openapiOperationDoc } from '@server/middlewares'
11import { ScopedToken } from '@shared/models/users/user-scoped-token' 11import { ScopedToken } from '@shared/models/users/user-scoped-token'
12 12
13const tokensRouter = express.Router() 13const tokensRouter = express.Router()
@@ -19,10 +19,12 @@ const loginRateLimiter = RateLimit({
19 19
20tokensRouter.post('/token', 20tokensRouter.post('/token',
21 loginRateLimiter, 21 loginRateLimiter,
22 openapiOperationDoc({ operationId: 'getOAuthToken' }),
22 asyncMiddleware(handleToken) 23 asyncMiddleware(handleToken)
23) 24)
24 25
25tokensRouter.post('/revoke-token', 26tokensRouter.post('/revoke-token',
27 openapiOperationDoc({ operationId: 'revokeOAuthToken' }),
26 authenticate, 28 authenticate,
27 asyncMiddleware(handleTokenRevocation) 29 asyncMiddleware(handleTokenRevocation)
28) 30)
@@ -78,9 +80,10 @@ async function handleToken (req: express.Request, res: express.Response, next: e
78 } catch (err) { 80 } catch (err) {
79 logger.warn('Login error', { err }) 81 logger.warn('Login error', { err })
80 82
81 return res.status(err.code || 400).json({ 83 return res.fail({
82 code: err.name, 84 status: err.code,
83 error: err.message 85 message: err.message,
86 type: err.name
84 }) 87 })
85 } 88 }
86} 89}
@@ -104,7 +107,7 @@ function getScopedTokens (req: express.Request, res: express.Response) {
104async function renewScopedTokens (req: express.Request, res: express.Response) { 107async function renewScopedTokens (req: express.Request, res: express.Response) {
105 const user = res.locals.oauth.token.user 108 const user = res.locals.oauth.token.user
106 109
107 user.feedToken = uuidv4() 110 user.feedToken = buildUUID()
108 await user.save() 111 await user.save()
109 112
110 return res.json({ 113 return res.json({
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index a755d7e57..bc8d203b0 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -13,8 +13,8 @@ import { CONFIG } from '../../initializers/config'
13import { MIMETYPES } from '../../initializers/constants' 13import { MIMETYPES } from '../../initializers/constants'
14import { sequelizeTypescript } from '../../initializers/database' 14import { sequelizeTypescript } from '../../initializers/database'
15import { sendUpdateActor } from '../../lib/activitypub/send' 15import { sendUpdateActor } from '../../lib/activitypub/send'
16import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image'
17import { JobQueue } from '../../lib/job-queue' 16import { JobQueue } from '../../lib/job-queue'
17import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/local-actor'
18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
19import { 19import {
20 asyncMiddleware, 20 asyncMiddleware,
@@ -32,7 +32,7 @@ import {
32 videoChannelsUpdateValidator, 32 videoChannelsUpdateValidator,
33 videoPlaylistsSortValidator 33 videoPlaylistsSortValidator
34} from '../../middlewares' 34} from '../../middlewares'
35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' 35import { videoChannelsListValidator, videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
36import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' 36import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image'
37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
38import { AccountModel } from '../../models/account/account' 38import { AccountModel } from '../../models/account/account'
@@ -51,7 +51,7 @@ videoChannelRouter.get('/',
51 videoChannelsSortValidator, 51 videoChannelsSortValidator,
52 setDefaultSort, 52 setDefaultSort,
53 setDefaultPagination, 53 setDefaultPagination,
54 videoChannelsOwnSearchValidator, 54 videoChannelsListValidator,
55 asyncMiddleware(listVideoChannels) 55 asyncMiddleware(listVideoChannels)
56) 56)
57 57
@@ -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
165async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { 166async 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
@@ -179,7 +180,7 @@ async function deleteVideoChannelAvatar (req: express.Request, res: express.Resp
179 180
180 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) 181 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
181 182
182 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 183 return res.status(HttpStatusCode.NO_CONTENT_204).end()
183} 184}
184 185
185async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { 186async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
@@ -187,7 +188,7 @@ async function deleteVideoChannelBanner (req: express.Request, res: express.Resp
187 188
188 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) 189 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
189 190
190 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 191 return res.status(HttpStatusCode.NO_CONTENT_204).end()
191} 192}
192 193
193async function addVideoChannel (req: express.Request, res: express.Response) { 194async function addVideoChannel (req: express.Request, res: express.Response) {
@@ -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..87a6f6bbe 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -1,7 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { join } from 'path' 2import { join } from 'path'
3import { uuidToShort } from '@server/helpers/uuid'
4import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists'
5import { Hooks } from '@server/lib/plugins/hooks'
3import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
4import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' 7import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' 9import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model'
6import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' 10import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
7import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' 11import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
@@ -17,8 +21,7 @@ import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constant
17import { sequelizeTypescript } from '../../initializers/database' 21import { sequelizeTypescript } from '../../initializers/database'
18import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' 22import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
19import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' 23import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
20import { JobQueue } from '../../lib/job-queue' 24import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail'
21import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail'
22import { 25import {
23 asyncMiddleware, 26 asyncMiddleware,
24 asyncRetryTransactionMiddleware, 27 asyncRetryTransactionMiddleware,
@@ -42,7 +45,6 @@ import {
42import { AccountModel } from '../../models/account/account' 45import { AccountModel } from '../../models/account/account'
43import { VideoPlaylistModel } from '../../models/video/video-playlist' 46import { VideoPlaylistModel } from '../../models/video/video-playlist'
44import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' 47import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
45import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
46 48
47const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) 49const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
48 50
@@ -144,9 +146,7 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
144function getVideoPlaylist (req: express.Request, res: express.Response) { 146function getVideoPlaylist (req: express.Request, res: express.Response) {
145 const videoPlaylist = res.locals.videoPlaylistSummary 147 const videoPlaylist = res.locals.videoPlaylistSummary
146 148
147 if (videoPlaylist.isOutdated()) { 149 scheduleRefreshIfNeeded(videoPlaylist)
148 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } })
149 }
150 150
151 return res.json(videoPlaylist.toFormattedJSON()) 151 return res.json(videoPlaylist.toFormattedJSON())
152} 152}
@@ -173,7 +173,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
173 173
174 const thumbnailField = req.files['thumbnailfile'] 174 const thumbnailField = req.files['thumbnailfile']
175 const thumbnailModel = thumbnailField 175 const thumbnailModel = thumbnailField
176 ? await createPlaylistMiniatureFromExisting({ 176 ? await updatePlaylistMiniatureFromExisting({
177 inputPath: thumbnailField[0].path, 177 inputPath: thumbnailField[0].path,
178 playlist: videoPlaylist, 178 playlist: videoPlaylist,
179 automaticallyGenerated: false 179 automaticallyGenerated: false
@@ -200,9 +200,10 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
200 return res.json({ 200 return res.json({
201 videoPlaylist: { 201 videoPlaylist: {
202 id: videoPlaylistCreated.id, 202 id: videoPlaylistCreated.id,
203 shortUUID: uuidToShort(videoPlaylistCreated.uuid),
203 uuid: videoPlaylistCreated.uuid 204 uuid: videoPlaylistCreated.uuid
204 } 205 }
205 }).end() 206 })
206} 207}
207 208
208async function updateVideoPlaylist (req: express.Request, res: express.Response) { 209async function updateVideoPlaylist (req: express.Request, res: express.Response) {
@@ -215,7 +216,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
215 216
216 const thumbnailField = req.files['thumbnailfile'] 217 const thumbnailField = req.files['thumbnailfile']
217 const thumbnailModel = thumbnailField 218 const thumbnailModel = thumbnailField
218 ? await createPlaylistMiniatureFromExisting({ 219 ? await updatePlaylistMiniatureFromExisting({
219 inputPath: thumbnailField[0].path, 220 inputPath: thumbnailField[0].path,
220 playlist: videoPlaylistInstance, 221 playlist: videoPlaylistInstance,
221 automaticallyGenerated: false 222 automaticallyGenerated: false
@@ -332,6 +333,8 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
332 333
333 logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) 334 logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
334 335
336 Hooks.runAction('action:api.video-playlist-element.created', { playlistElement })
337
335 return res.json({ 338 return res.json({
336 videoPlaylistElement: { 339 videoPlaylistElement: {
337 id: playlistElement.id 340 id: playlistElement.id
@@ -482,7 +485,7 @@ async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbn
482 } 485 }
483 486
484 const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) 487 const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename)
485 const thumbnailModel = await createPlaylistMiniatureFromExisting({ 488 const thumbnailModel = await updatePlaylistMiniatureFromExisting({
486 inputPath, 489 inputPath,
487 playlist: videoPlaylist, 490 playlist: videoPlaylist,
488 automaticallyGenerated: true, 491 automaticallyGenerated: true,
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index fa8448c86..530e17965 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -9,6 +9,7 @@ import {
9 authenticate, 9 authenticate,
10 blacklistSortValidator, 10 blacklistSortValidator,
11 ensureUserHasRight, 11 ensureUserHasRight,
12 openapiOperationDoc,
12 paginationValidator, 13 paginationValidator,
13 setBlacklistSort, 14 setBlacklistSort,
14 setDefaultPagination, 15 setDefaultPagination,
@@ -23,6 +24,7 @@ import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-c
23const blacklistRouter = express.Router() 24const blacklistRouter = express.Router()
24 25
25blacklistRouter.post('/:videoId/blacklist', 26blacklistRouter.post('/:videoId/blacklist',
27 openapiOperationDoc({ operationId: 'addVideoBlock' }),
26 authenticate, 28 authenticate,
27 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 29 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
28 asyncMiddleware(videosBlacklistAddValidator), 30 asyncMiddleware(videosBlacklistAddValidator),
@@ -30,6 +32,7 @@ blacklistRouter.post('/:videoId/blacklist',
30) 32)
31 33
32blacklistRouter.get('/blacklist', 34blacklistRouter.get('/blacklist',
35 openapiOperationDoc({ operationId: 'getVideoBlocks' }),
33 authenticate, 36 authenticate,
34 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 37 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
35 paginationValidator, 38 paginationValidator,
@@ -48,6 +51,7 @@ blacklistRouter.put('/:videoId/blacklist',
48) 51)
49 52
50blacklistRouter.delete('/:videoId/blacklist', 53blacklistRouter.delete('/:videoId/blacklist',
54 openapiOperationDoc({ operationId: 'delVideoBlock' }),
51 authenticate, 55 authenticate,
52 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 56 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
53 asyncMiddleware(videosBlacklistRemoveValidator), 57 asyncMiddleware(videosBlacklistRemoveValidator),
@@ -70,7 +74,7 @@ async function addVideoToBlacklistController (req: express.Request, res: express
70 74
71 logger.info('Video %s blacklisted.', videoInstance.uuid) 75 logger.info('Video %s blacklisted.', videoInstance.uuid)
72 76
73 return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) 77 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
74} 78}
75 79
76async function updateVideoBlacklistController (req: express.Request, res: express.Response) { 80async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
@@ -82,7 +86,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
82 return videoBlacklist.save({ transaction: t }) 86 return videoBlacklist.save({ transaction: t })
83 }) 87 })
84 88
85 return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) 89 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
86} 90}
87 91
88async function listBlacklist (req: express.Request, res: express.Response) { 92async function listBlacklist (req: express.Request, res: express.Response) {
@@ -105,5 +109,5 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
105 109
106 logger.info('Video %s removed from blacklist.', video.uuid) 110 logger.info('Video %s removed from blacklist.', video.uuid)
107 111
108 return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) 112 return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
109} 113}
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index f1f53d354..e6f28c1cb 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 2import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
3import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models' 3import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models'
4import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' 4import { VideoCommentCreate } from '../../../../shared/models/videos/comment/video-comment.model'
5import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' 5import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
6import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
7import { sequelizeTypescript } from '../../../initializers/database' 7import { sequelizeTypescript } from '../../../initializers/database'
@@ -166,7 +166,10 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
166 } 166 }
167 167
168 if (resultList.data.length === 0) { 168 if (resultList.data.length === 0) {
169 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 169 return res.fail({
170 status: HttpStatusCode.NOT_FOUND_404,
171 message: 'No comments were found'
172 })
170 } 173 }
171 174
172 return res.json(buildFormattedCommentTree(resultList)) 175 return res.json(buildFormattedCommentTree(resultList))
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 3b9b887e2..de9a5308a 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'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as parseTorrent from 'parse-torrent' 4import * as parseTorrent from 'parse-torrent'
5import { join } from 'path' 5import { join } from 'path'
6import { ServerConfigManager } from '@server/lib/server-config-manager'
6import { setVideoTags } from '@server/lib/video' 7import { setVideoTags } from '@server/lib/video'
8import { FilteredModelAttributes } from '@server/types'
7import { 9import {
8 MChannelAccountDefault, 10 MChannelAccountDefault,
9 MThumbnail, 11 MThumbnail,
@@ -14,23 +16,22 @@ import {
14 MVideoThumbnail, 16 MVideoThumbnail,
15 MVideoWithBlacklistLight 17 MVideoWithBlacklistLight
16} from '@server/types/models' 18} from '@server/types/models'
17import { MVideoImport, MVideoImportFormattable } from '@server/types/models/video/video-import' 19import { MVideoImportFormattable } from '@server/types/models/video/video-import'
18import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' 20import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
19import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
20import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
21import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 22import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
22import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' 23import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
23import { isArray } from '../../../helpers/custom-validators/misc' 24import { isArray } from '../../../helpers/custom-validators/misc'
24import { createReqFiles } from '../../../helpers/express-utils' 25import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
25import { logger } from '../../../helpers/logger' 26import { logger } from '../../../helpers/logger'
26import { getSecureTorrentName } from '../../../helpers/utils' 27import { getSecureTorrentName } from '../../../helpers/utils'
27import { getYoutubeDLInfo, getYoutubeDLSubs, YoutubeDLInfo } from '../../../helpers/youtube-dl' 28import { YoutubeDL, YoutubeDLInfo } from '../../../helpers/youtube-dl'
28import { CONFIG } from '../../../initializers/config' 29import { CONFIG } from '../../../initializers/config'
29import { MIMETYPES } from '../../../initializers/constants' 30import { MIMETYPES } from '../../../initializers/constants'
30import { sequelizeTypescript } from '../../../initializers/database' 31import { sequelizeTypescript } from '../../../initializers/database'
31import { getLocalVideoActivityPubUrl } from '../../../lib/activitypub/url' 32import { getLocalVideoActivityPubUrl } from '../../../lib/activitypub/url'
32import { JobQueue } from '../../../lib/job-queue/job-queue' 33import { JobQueue } from '../../../lib/job-queue/job-queue'
33import { createVideoMiniatureFromExisting, createVideoMiniatureFromUrl } from '../../../lib/thumbnail' 34import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail'
34import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' 35import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
35import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' 36import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
36import { VideoModel } from '../../../models/video/video' 37import { VideoModel } from '../../../models/video/video'
@@ -81,22 +82,15 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
81 let magnetUri: string 82 let magnetUri: string
82 83
83 if (torrentfile) { 84 if (torrentfile) {
84 torrentName = torrentfile.originalname 85 const result = await processTorrentOrAbortRequest(req, res, torrentfile)
86 if (!result) return
85 87
86 // Rename the torrent to a secured name 88 videoName = result.name
87 const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) 89 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 { 90 } else {
96 magnetUri = body.magnetUri 91 const result = processMagnetURI(body)
97 92 magnetUri = result.magnetUri
98 const parsed = magnetUtil.decode(magnetUri) 93 videoName = result.name
99 videoName = isArray(parsed.name) ? parsed.name[0] : parsed.name as string
100 } 94 }
101 95
102 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) 96 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
@@ -104,26 +98,26 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
104 const thumbnailModel = await processThumbnail(req, video) 98 const thumbnailModel = await processThumbnail(req, video)
105 const previewModel = await processPreview(req, video) 99 const previewModel = await processPreview(req, video)
106 100
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({ 101 const videoImport = await insertIntoDB({
115 video, 102 video,
116 thumbnailModel, 103 thumbnailModel,
117 previewModel, 104 previewModel,
118 videoChannel: res.locals.videoChannel, 105 videoChannel: res.locals.videoChannel,
119 tags, 106 tags: body.tags || undefined,
120 videoImportAttributes, 107 user,
121 user 108 videoImportAttributes: {
109 magnetUri,
110 torrentName,
111 state: VideoImportState.PENDING,
112 userId: user.id
113 }
122 }) 114 })
123 115
124 // Create job to import the video 116 // Create job to import the video
125 const payload = { 117 const payload = {
126 type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri', 118 type: torrentfile
119 ? 'torrent-file' as 'torrent-file'
120 : 'magnet-uri' as 'magnet-uri',
127 videoImportId: videoImport.id, 121 videoImportId: videoImport.id,
128 magnetUri 122 magnetUri
129 } 123 }
@@ -139,17 +133,21 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
139 const targetUrl = body.targetUrl 133 const targetUrl = body.targetUrl
140 const user = res.locals.oauth.token.User 134 const user = res.locals.oauth.token.User
141 135
136 const youtubeDL = new YoutubeDL(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
137
142 // Get video infos 138 // Get video infos
143 let youtubeDLInfo: YoutubeDLInfo 139 let youtubeDLInfo: YoutubeDLInfo
144 try { 140 try {
145 youtubeDLInfo = await getYoutubeDLInfo(targetUrl) 141 youtubeDLInfo = await youtubeDL.getYoutubeDLInfo()
146 } catch (err) { 142 } catch (err) {
147 logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) 143 logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
148 144
149 return res.status(HttpStatusCode.BAD_REQUEST_400) 145 return res.fail({
150 .json({ 146 message: 'Cannot fetch remote information of this URL.',
151 error: 'Cannot fetch remote information of this URL.' 147 data: {
152 }) 148 targetUrl
149 }
150 })
153 } 151 }
154 152
155 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) 153 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
@@ -170,45 +168,22 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
170 previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video) 168 previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video)
171 } 169 }
172 170
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({ 171 const videoImport = await insertIntoDB({
180 video, 172 video,
181 thumbnailModel, 173 thumbnailModel,
182 previewModel, 174 previewModel,
183 videoChannel: res.locals.videoChannel, 175 videoChannel: res.locals.videoChannel,
184 tags, 176 tags: body.tags || youtubeDLInfo.tags,
185 videoImportAttributes, 177 user,
186 user 178 videoImportAttributes: {
179 targetUrl,
180 state: VideoImportState.PENDING,
181 userId: user.id
182 }
187 }) 183 })
188 184
189 // Get video subtitles 185 // Get video subtitles
190 try { 186 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 187
213 // Create job to import the video 188 // Create job to import the video
214 const payload = { 189 const payload = {
@@ -240,7 +215,9 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
240 privacy: body.privacy || VideoPrivacy.PRIVATE, 215 privacy: body.privacy || VideoPrivacy.PRIVATE,
241 duration: 0, // duration will be set by the import job 216 duration: 0, // duration will be set by the import job
242 channelId: channelId, 217 channelId: channelId,
243 originallyPublishedAt: body.originallyPublishedAt || importData.originallyPublishedAt 218 originallyPublishedAt: body.originallyPublishedAt
219 ? new Date(body.originallyPublishedAt)
220 : importData.originallyPublishedAt
244 } 221 }
245 const video = new VideoModel(videoData) 222 const video = new VideoModel(videoData)
246 video.url = getLocalVideoActivityPubUrl(video) 223 video.url = getLocalVideoActivityPubUrl(video)
@@ -253,7 +230,7 @@ async function processThumbnail (req: express.Request, video: MVideoThumbnail) {
253 if (thumbnailField) { 230 if (thumbnailField) {
254 const thumbnailPhysicalFile = thumbnailField[0] 231 const thumbnailPhysicalFile = thumbnailField[0]
255 232
256 return createVideoMiniatureFromExisting({ 233 return updateVideoMiniatureFromExisting({
257 inputPath: thumbnailPhysicalFile.path, 234 inputPath: thumbnailPhysicalFile.path,
258 video, 235 video,
259 type: ThumbnailType.MINIATURE, 236 type: ThumbnailType.MINIATURE,
@@ -269,7 +246,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr
269 if (previewField) { 246 if (previewField) {
270 const previewPhysicalFile = previewField[0] 247 const previewPhysicalFile = previewField[0]
271 248
272 return createVideoMiniatureFromExisting({ 249 return updateVideoMiniatureFromExisting({
273 inputPath: previewPhysicalFile.path, 250 inputPath: previewPhysicalFile.path,
274 video, 251 video,
275 type: ThumbnailType.PREVIEW, 252 type: ThumbnailType.PREVIEW,
@@ -282,7 +259,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr
282 259
283async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { 260async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) {
284 try { 261 try {
285 return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.MINIATURE }) 262 return updateVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.MINIATURE })
286 } catch (err) { 263 } catch (err) {
287 logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err }) 264 logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err })
288 return undefined 265 return undefined
@@ -291,7 +268,7 @@ async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) {
291 268
292async function processPreviewFromUrl (url: string, video: MVideoThumbnail) { 269async function processPreviewFromUrl (url: string, video: MVideoThumbnail) {
293 try { 270 try {
294 return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.PREVIEW }) 271 return updateVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.PREVIEW })
295 } catch (err) { 272 } catch (err) {
296 logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err }) 273 logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err })
297 return undefined 274 return undefined
@@ -304,7 +281,7 @@ async function insertIntoDB (parameters: {
304 previewModel: MThumbnail 281 previewModel: MThumbnail
305 videoChannel: MChannelAccountDefault 282 videoChannel: MChannelAccountDefault
306 tags: string[] 283 tags: string[]
307 videoImportAttributes: Partial<MVideoImport> 284 videoImportAttributes: FilteredModelAttributes<VideoImportModel>
308 user: MUser 285 user: MUser
309}): Promise<MVideoImportFormattable> { 286}): Promise<MVideoImportFormattable> {
310 const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters 287 const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
@@ -342,3 +319,69 @@ async function insertIntoDB (parameters: {
342 319
343 return videoImport 320 return videoImport
344} 321}
322
323async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
324 const torrentName = torrentfile.originalname
325
326 // Rename the torrent to a secured name
327 const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
328 await move(torrentfile.path, newTorrentPath, { overwrite: true })
329 torrentfile.path = newTorrentPath
330
331 const buf = await readFile(torrentfile.path)
332 const parsedTorrent = parseTorrent(buf) as parseTorrent.Instance
333
334 if (parsedTorrent.files.length !== 1) {
335 cleanUpReqFiles(req)
336
337 res.fail({
338 type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
339 message: 'Torrents with only 1 file are supported.'
340 })
341 return undefined
342 }
343
344 return {
345 name: extractNameFromArray(parsedTorrent.name),
346 torrentName
347 }
348}
349
350function processMagnetURI (body: VideoImportCreate) {
351 const magnetUri = body.magnetUri
352 const parsed = magnetUtil.decode(magnetUri)
353
354 return {
355 name: extractNameFromArray(parsed.name),
356 magnetUri
357 }
358}
359
360function extractNameFromArray (name: string | string[]) {
361 return isArray(name) ? name[0] : name
362}
363
364async function processYoutubeSubtitles (youtubeDL: YoutubeDL, targetUrl: string, videoId: number) {
365 try {
366 const subtitles = await youtubeDL.getYoutubeDLSubs()
367
368 logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
369
370 for (const subtitle of subtitles) {
371 const videoCaption = new VideoCaptionModel({
372 videoId,
373 language: subtitle.language,
374 filename: VideoCaptionModel.generateCaptionName(subtitle.language)
375 }) as MVideoCaption
376
377 // Move physical file
378 await moveAndProcessCaptionFile(subtitle, videoCaption)
379
380 await sequelizeTypescript.transaction(async t => {
381 await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
382 })
383 }
384 } catch (err) {
385 logger.warn('Cannot get video subtitles.', { err })
386 }
387}
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index c32626d30..74b100e59 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -1,43 +1,22 @@
1import * as express from 'express' 1import * as express from 'express'
2import { move } from 'fs-extra'
3import { extname } from 'path'
4import toInt from 'validator/lib/toInt' 2import toInt from 'validator/lib/toInt'
5import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' 3import { doJSONRequest } from '@server/helpers/requests'
6import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 4import { LiveManager } from '@server/lib/live'
7import { changeVideoChannelShare } from '@server/lib/activitypub/share' 5import { openapiOperationDoc } from '@server/middlewares/doc'
8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
9import { LiveManager } from '@server/lib/live-manager'
10import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
11import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
12import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
13import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 7import { MVideoAccountLight } from '@server/types/models'
14import { uploadx } from '@uploadx/core' 8import { VideosCommonQuery } from '../../../../shared'
15import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
16import { HttpStatusCode } from '../../../../shared/core-utils/miscs' 9import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
17import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 10import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
18import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 11import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
19import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 12import { logger } from '../../../helpers/logger'
20import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
21import { logger, loggerTagsFactory } from '../../../helpers/logger'
22import { getFormattedObjects } from '../../../helpers/utils' 13import { getFormattedObjects } from '../../../helpers/utils'
23import { CONFIG } from '../../../initializers/config' 14import { REMOTE_SCHEME, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
24import {
25 DEFAULT_AUDIO_RESOLUTION,
26 MIMETYPES,
27 VIDEO_CATEGORIES,
28 VIDEO_LANGUAGES,
29 VIDEO_LICENCES,
30 VIDEO_PRIVACIES
31} from '../../../initializers/constants'
32import { sequelizeTypescript } from '../../../initializers/database' 15import { sequelizeTypescript } from '../../../initializers/database'
33import { sendView } from '../../../lib/activitypub/send/send-view' 16import { sendView } from '../../../lib/activitypub/send/send-view'
34import { federateVideoIfNeeded, fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
35import { JobQueue } from '../../../lib/job-queue' 17import { JobQueue } from '../../../lib/job-queue'
36import { Notifier } from '../../../lib/notifier'
37import { Hooks } from '../../../lib/plugins/hooks' 18import { Hooks } from '../../../lib/plugins/hooks'
38import { Redis } from '../../../lib/redis' 19import { Redis } from '../../../lib/redis'
39import { generateVideoMiniature } from '../../../lib/thumbnail'
40import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
41import { 20import {
42 asyncMiddleware, 21 asyncMiddleware,
43 asyncRetryTransactionMiddleware, 22 asyncRetryTransactionMiddleware,
@@ -49,16 +28,11 @@ import {
49 setDefaultPagination, 28 setDefaultPagination,
50 setDefaultVideosSort, 29 setDefaultVideosSort,
51 videoFileMetadataGetValidator, 30 videoFileMetadataGetValidator,
52 videosAddLegacyValidator,
53 videosAddResumableInitValidator,
54 videosAddResumableValidator,
55 videosCustomGetValidator, 31 videosCustomGetValidator,
56 videosGetValidator, 32 videosGetValidator,
57 videosRemoveValidator, 33 videosRemoveValidator,
58 videosSortValidator, 34 videosSortValidator
59 videosUpdateValidator
60} from '../../../middlewares' 35} from '../../../middlewares'
61import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
62import { VideoModel } from '../../../models/video/video' 36import { VideoModel } from '../../../models/video/video'
63import { VideoFileModel } from '../../../models/video/video-file' 37import { VideoFileModel } from '../../../models/video/video-file'
64import { blacklistRouter } from './blacklist' 38import { blacklistRouter } from './blacklist'
@@ -68,40 +42,12 @@ import { videoImportsRouter } from './import'
68import { liveRouter } from './live' 42import { liveRouter } from './live'
69import { ownershipVideoRouter } from './ownership' 43import { ownershipVideoRouter } from './ownership'
70import { rateVideoRouter } from './rate' 44import { rateVideoRouter } from './rate'
45import { updateRouter } from './update'
46import { uploadRouter } from './upload'
71import { watchingRouter } from './watching' 47import { watchingRouter } from './watching'
72 48
73const lTags = loggerTagsFactory('api', 'video')
74const auditLogger = auditLoggerFactory('videos') 49const auditLogger = auditLoggerFactory('videos')
75const videosRouter = express.Router() 50const videosRouter = express.Router()
76const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
77
78const reqVideoFileAdd = createReqFiles(
79 [ 'videofile', 'thumbnailfile', 'previewfile' ],
80 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
81 {
82 videofile: CONFIG.STORAGE.TMP_DIR,
83 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
84 previewfile: CONFIG.STORAGE.TMP_DIR
85 }
86)
87
88const reqVideoFileAddResumable = createReqFiles(
89 [ 'thumbnailfile', 'previewfile' ],
90 MIMETYPES.IMAGE.MIMETYPE_EXT,
91 {
92 thumbnailfile: getResumableUploadPath(),
93 previewfile: getResumableUploadPath()
94 }
95)
96
97const reqVideoFileUpdate = createReqFiles(
98 [ 'thumbnailfile', 'previewfile' ],
99 MIMETYPES.IMAGE.MIMETYPE_EXT,
100 {
101 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
102 previewfile: CONFIG.STORAGE.TMP_DIR
103 }
104)
105 51
106videosRouter.use('/', blacklistRouter) 52videosRouter.use('/', blacklistRouter)
107videosRouter.use('/', rateVideoRouter) 53videosRouter.use('/', rateVideoRouter)
@@ -111,13 +57,28 @@ videosRouter.use('/', videoImportsRouter)
111videosRouter.use('/', ownershipVideoRouter) 57videosRouter.use('/', ownershipVideoRouter)
112videosRouter.use('/', watchingRouter) 58videosRouter.use('/', watchingRouter)
113videosRouter.use('/', liveRouter) 59videosRouter.use('/', liveRouter)
60videosRouter.use('/', uploadRouter)
61videosRouter.use('/', updateRouter)
114 62
115videosRouter.get('/categories', listVideoCategories) 63videosRouter.get('/categories',
116videosRouter.get('/licences', listVideoLicences) 64 openapiOperationDoc({ operationId: 'getCategories' }),
117videosRouter.get('/languages', listVideoLanguages) 65 listVideoCategories
118videosRouter.get('/privacies', listVideoPrivacies) 66)
67videosRouter.get('/licences',
68 openapiOperationDoc({ operationId: 'getLicences' }),
69 listVideoLicences
70)
71videosRouter.get('/languages',
72 openapiOperationDoc({ operationId: 'getLanguages' }),
73 listVideoLanguages
74)
75videosRouter.get('/privacies',
76 openapiOperationDoc({ operationId: 'getPrivacies' }),
77 listVideoPrivacies
78)
119 79
120videosRouter.get('/', 80videosRouter.get('/',
81 openapiOperationDoc({ operationId: 'getVideos' }),
121 paginationValidator, 82 paginationValidator,
122 videosSortValidator, 83 videosSortValidator,
123 setDefaultVideosSort, 84 setDefaultVideosSort,
@@ -127,40 +88,8 @@ videosRouter.get('/',
127 asyncMiddleware(listVideos) 88 asyncMiddleware(listVideos)
128) 89)
129 90
130videosRouter.post('/upload',
131 authenticate,
132 reqVideoFileAdd,
133 asyncMiddleware(videosAddLegacyValidator),
134 asyncRetryTransactionMiddleware(addVideoLegacy)
135)
136
137videosRouter.post('/upload-resumable',
138 authenticate,
139 reqVideoFileAddResumable,
140 asyncMiddleware(videosAddResumableInitValidator),
141 uploadxMiddleware
142)
143
144videosRouter.delete('/upload-resumable',
145 authenticate,
146 uploadxMiddleware
147)
148
149videosRouter.put('/upload-resumable',
150 authenticate,
151 uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
152 asyncMiddleware(videosAddResumableValidator),
153 asyncMiddleware(addVideoResumable)
154)
155
156videosRouter.put('/:id',
157 authenticate,
158 reqVideoFileUpdate,
159 asyncMiddleware(videosUpdateValidator),
160 asyncRetryTransactionMiddleware(updateVideo)
161)
162
163videosRouter.get('/:id/description', 91videosRouter.get('/:id/description',
92 openapiOperationDoc({ operationId: 'getVideoDesc' }),
164 asyncMiddleware(videosGetValidator), 93 asyncMiddleware(videosGetValidator),
165 asyncMiddleware(getVideoDescription) 94 asyncMiddleware(getVideoDescription)
166) 95)
@@ -169,17 +98,20 @@ videosRouter.get('/:id/metadata/:videoFileId',
169 asyncMiddleware(getVideoFileMetadata) 98 asyncMiddleware(getVideoFileMetadata)
170) 99)
171videosRouter.get('/:id', 100videosRouter.get('/:id',
101 openapiOperationDoc({ operationId: 'getVideo' }),
172 optionalAuthenticate, 102 optionalAuthenticate,
173 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), 103 asyncMiddleware(videosCustomGetValidator('for-api')),
174 asyncMiddleware(checkVideoFollowConstraints), 104 asyncMiddleware(checkVideoFollowConstraints),
175 asyncMiddleware(getVideo) 105 asyncMiddleware(getVideo)
176) 106)
177videosRouter.post('/:id/views', 107videosRouter.post('/:id/views',
108 openapiOperationDoc({ operationId: 'addView' }),
178 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), 109 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
179 asyncMiddleware(viewVideo) 110 asyncMiddleware(viewVideo)
180) 111)
181 112
182videosRouter.delete('/:id', 113videosRouter.delete('/:id',
114 openapiOperationDoc({ operationId: 'delVideo' }),
183 authenticate, 115 authenticate,
184 asyncMiddleware(videosRemoveValidator), 116 asyncMiddleware(videosRemoveValidator),
185 asyncRetryTransactionMiddleware(removeVideo) 117 asyncRetryTransactionMiddleware(removeVideo)
@@ -209,287 +141,8 @@ function listVideoPrivacies (_req: express.Request, res: express.Response) {
209 res.json(VIDEO_PRIVACIES) 141 res.json(VIDEO_PRIVACIES)
210} 142}
211 143
212async function addVideoLegacy (req: express.Request, res: express.Response) { 144async function getVideo (_req: express.Request, res: express.Response) {
213 // Uploading the video could be long 145 const video = res.locals.videoAPI
214 // Set timeout to 10 minutes, as Express's default is 2 minutes
215 req.setTimeout(1000 * 60 * 10, () => {
216 logger.error('Upload video has timed out.')
217 return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408)
218 })
219
220 const videoPhysicalFile = req.files['videofile'][0]
221 const videoInfo: VideoCreate = req.body
222 const files = req.files
223
224 return addVideo({ res, videoPhysicalFile, videoInfo, files })
225}
226
227async function addVideoResumable (_req: express.Request, res: express.Response) {
228 const videoPhysicalFile = res.locals.videoFileResumable
229 const videoInfo = videoPhysicalFile.metadata
230 const files = { previewfile: videoInfo.previewfile }
231
232 // Don't need the meta file anymore
233 await deleteResumableUploadMetaFile(videoPhysicalFile.path)
234
235 return addVideo({ res, videoPhysicalFile, videoInfo, files })
236}
237
238async function addVideo (options: {
239 res: express.Response
240 videoPhysicalFile: express.VideoUploadFile
241 videoInfo: VideoCreate
242 files: express.UploadFiles
243}) {
244 const { res, videoPhysicalFile, videoInfo, files } = options
245 const videoChannel = res.locals.videoChannel
246 const user = res.locals.oauth.token.User
247
248 const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
249
250 videoData.state = CONFIG.TRANSCODING.ENABLED
251 ? VideoState.TO_TRANSCODE
252 : VideoState.PUBLISHED
253
254 videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
255
256 const video = new VideoModel(videoData) as MVideoFullLight
257 video.VideoChannel = videoChannel
258 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
259
260 const videoFile = new VideoFileModel({
261 extname: extname(videoPhysicalFile.filename),
262 size: videoPhysicalFile.size,
263 videoStreamingPlaylistId: null,
264 metadata: await getMetadataFromFile(videoPhysicalFile.path)
265 })
266
267 if (videoFile.isAudio()) {
268 videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
269 } else {
270 videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
271 videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
272 }
273
274 videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
275
276 // Move physical file
277 const destination = getVideoFilePath(video, videoFile)
278 await move(videoPhysicalFile.path, destination)
279 // This is important in case if there is another attempt in the retry process
280 videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
281 videoPhysicalFile.path = destination
282
283 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
284 video,
285 files,
286 fallback: type => generateVideoMiniature({ video, videoFile, type })
287 })
288
289 const { videoCreated } = await sequelizeTypescript.transaction(async t => {
290 const sequelizeOptions = { transaction: t }
291
292 const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
293
294 await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
295 await videoCreated.addAndSaveThumbnail(previewModel, t)
296
297 // Do not forget to add video channel information to the created video
298 videoCreated.VideoChannel = res.locals.videoChannel
299
300 videoFile.videoId = video.id
301 await videoFile.save(sequelizeOptions)
302
303 video.VideoFiles = [ videoFile ]
304
305 await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
306
307 // Schedule an update in the future?
308 if (videoInfo.scheduleUpdate) {
309 await ScheduleVideoUpdateModel.create({
310 videoId: video.id,
311 updateAt: videoInfo.scheduleUpdate.updateAt,
312 privacy: videoInfo.scheduleUpdate.privacy || null
313 }, { transaction: t })
314 }
315
316 // Channel has a new content, set as updated
317 await videoCreated.VideoChannel.setAsUpdated(t)
318
319 await autoBlacklistVideoIfNeeded({
320 video,
321 user,
322 isRemote: false,
323 isNew: true,
324 transaction: t
325 })
326
327 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
328 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
329
330 return { videoCreated }
331 })
332
333 // Create the torrent file in async way because it could be long
334 createTorrentAndSetInfoHashAsync(video, videoFile)
335 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
336 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
337 .then(refreshedVideo => {
338 if (!refreshedVideo) return
339
340 // Only federate and notify after the torrent creation
341 Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
342
343 return retryTransactionWrapper(() => {
344 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
345 })
346 })
347 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
348
349 if (video.state === VideoState.TO_TRANSCODE) {
350 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
351 }
352
353 Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
354
355 return res.json({
356 video: {
357 id: videoCreated.id,
358 uuid: videoCreated.uuid
359 }
360 })
361}
362
363async function updateVideo (req: express.Request, res: express.Response) {
364 const videoInstance = res.locals.videoAll
365 const videoFieldsSave = videoInstance.toJSON()
366 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
367 const videoInfoToUpdate: VideoUpdate = req.body
368
369 const wasConfidentialVideo = videoInstance.isConfidential()
370 const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
371
372 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
373 video: videoInstance,
374 files: req.files,
375 fallback: () => Promise.resolve(undefined),
376 automaticallyGenerated: false
377 })
378
379 try {
380 const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
381 const sequelizeOptions = { transaction: t }
382 const oldVideoChannel = videoInstance.VideoChannel
383
384 if (videoInfoToUpdate.name !== undefined) videoInstance.name = videoInfoToUpdate.name
385 if (videoInfoToUpdate.category !== undefined) videoInstance.category = videoInfoToUpdate.category
386 if (videoInfoToUpdate.licence !== undefined) videoInstance.licence = videoInfoToUpdate.licence
387 if (videoInfoToUpdate.language !== undefined) videoInstance.language = videoInfoToUpdate.language
388 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.nsfw = videoInfoToUpdate.nsfw
389 if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.waitTranscoding = videoInfoToUpdate.waitTranscoding
390 if (videoInfoToUpdate.support !== undefined) videoInstance.support = videoInfoToUpdate.support
391 if (videoInfoToUpdate.description !== undefined) videoInstance.description = videoInfoToUpdate.description
392 if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.commentsEnabled = videoInfoToUpdate.commentsEnabled
393 if (videoInfoToUpdate.downloadEnabled !== undefined) videoInstance.downloadEnabled = videoInfoToUpdate.downloadEnabled
394
395 if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) {
396 videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
397 }
398
399 let isNewVideo = false
400 if (videoInfoToUpdate.privacy !== undefined) {
401 isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
402
403 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
404 videoInstance.setPrivacy(newPrivacy)
405
406 // Unfederate the video if the new privacy is not compatible with federation
407 if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
408 await VideoModel.sendDelete(videoInstance, { transaction: t })
409 }
410 }
411
412 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
413
414 if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
415 if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
416
417 // Video tags update?
418 if (videoInfoToUpdate.tags !== undefined) {
419 await setVideoTags({
420 video: videoInstanceUpdated,
421 tags: videoInfoToUpdate.tags,
422 transaction: t
423 })
424 }
425
426 // Video channel update?
427 if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
428 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
429 videoInstanceUpdated.VideoChannel = res.locals.videoChannel
430
431 if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
432 }
433
434 // Schedule an update in the future?
435 if (videoInfoToUpdate.scheduleUpdate) {
436 await ScheduleVideoUpdateModel.upsert({
437 videoId: videoInstanceUpdated.id,
438 updateAt: videoInfoToUpdate.scheduleUpdate.updateAt,
439 privacy: videoInfoToUpdate.scheduleUpdate.privacy || null
440 }, { transaction: t })
441 } else if (videoInfoToUpdate.scheduleUpdate === null) {
442 await ScheduleVideoUpdateModel.deleteByVideoId(videoInstanceUpdated.id, t)
443 }
444
445 await autoBlacklistVideoIfNeeded({
446 video: videoInstanceUpdated,
447 user: res.locals.oauth.token.User,
448 isRemote: false,
449 isNew: false,
450 transaction: t
451 })
452
453 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
454
455 auditLogger.update(
456 getAuditIdFromRes(res),
457 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
458 oldVideoAuditView
459 )
460 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
461
462 return videoInstanceUpdated
463 })
464
465 if (wasConfidentialVideo) {
466 Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
467 }
468
469 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
470 } catch (err) {
471 // Force fields we want to update
472 // If the transaction is retried, sequelize will think the object has not changed
473 // So it will skip the SQL request, even if the last one was ROLLBACKed!
474 resetSequelizeInstance(videoInstance, videoFieldsSave)
475
476 throw err
477 }
478
479 return res.type('json')
480 .status(HttpStatusCode.NO_CONTENT_204)
481 .end()
482}
483
484async function getVideo (req: express.Request, res: express.Response) {
485 // We need more attributes
486 const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
487
488 const video = await Hooks.wrapPromiseFun(
489 VideoModel.loadForGetAPI,
490 { id: res.locals.onlyVideoWithRights.id, userId },
491 'filter:api.video.get.result'
492 )
493 146
494 if (video.isOutdated()) { 147 if (video.isOutdated()) {
495 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) 148 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
@@ -505,7 +158,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
505 const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid) 158 const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid)
506 if (exists) { 159 if (exists) {
507 logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid) 160 logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid)
508 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 161 return res.status(HttpStatusCode.NO_CONTENT_204).end()
509 } 162 }
510 163
511 const video = await VideoModel.load(immutableVideoAttrs.id) 164 const video = await VideoModel.load(immutableVideoAttrs.id)
@@ -538,18 +191,15 @@ async function viewVideo (req: express.Request, res: express.Response) {
538 191
539 Hooks.runAction('action:api.video.viewed', { video, ip }) 192 Hooks.runAction('action:api.video.viewed', { video, ip })
540 193
541 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 194 return res.status(HttpStatusCode.NO_CONTENT_204).end()
542} 195}
543 196
544async function getVideoDescription (req: express.Request, res: express.Response) { 197async function getVideoDescription (req: express.Request, res: express.Response) {
545 const videoInstance = res.locals.videoAll 198 const videoInstance = res.locals.videoAll
546 let description = ''
547 199
548 if (videoInstance.isOwned()) { 200 const description = videoInstance.isOwned()
549 description = videoInstance.description 201 ? videoInstance.description
550 } else { 202 : await fetchRemoteVideoDescription(videoInstance)
551 description = await fetchRemoteVideoDescription(videoInstance)
552 }
553 203
554 return res.json({ description }) 204 return res.json({ description })
555} 205}
@@ -591,7 +241,7 @@ async function listVideos (req: express.Request, res: express.Response) {
591 return res.json(getFormattedObjects(resultList.data, resultList.total)) 241 return res.json(getFormattedObjects(resultList.data, resultList.total))
592} 242}
593 243
594async function removeVideo (req: express.Request, res: express.Response) { 244async function removeVideo (_req: express.Request, res: express.Response) {
595 const videoInstance = res.locals.videoAll 245 const videoInstance = res.locals.videoAll
596 246
597 await sequelizeTypescript.transaction(async t => { 247 await sequelizeTypescript.transaction(async t => {
@@ -608,16 +258,14 @@ async function removeVideo (req: express.Request, res: express.Response) {
608 .end() 258 .end()
609} 259}
610 260
611async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) { 261// ---------------------------------------------------------------------------
612 await createTorrentAndSetInfoHash(video, fileArg)
613
614 // Refresh videoFile because the createTorrentAndSetInfoHash could be long
615 const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
616 // File does not exist anymore, remove the generated torrent
617 if (!refreshedFile) return fileArg.removeTorrent()
618 262
619 refreshedFile.infoHash = fileArg.infoHash 263// FIXME: Should not exist, we rely on specific API
620 refreshedFile.torrentFilename = fileArg.torrentFilename 264async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
265 const host = video.VideoChannel.Account.Actor.Server.host
266 const path = video.getDescriptionAPIPath()
267 const url = REMOTE_SCHEME.HTTP + '://' + host + path
621 268
622 return refreshedFile.save() 269 const { body } = await doJSONRequest<any>(url)
270 return body.description || ''
623} 271}
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts
index 04d2494ce..d8c51c2d4 100644
--- a/server/controllers/api/videos/live.ts
+++ b/server/controllers/api/videos/live.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { v4 as uuidv4 } from 'uuid'
3import { createReqFiles } from '@server/helpers/express-utils' 2import { createReqFiles } from '@server/helpers/express-utils'
3import { buildUUID, uuidToShort } from '@server/helpers/uuid'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' 5import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
6import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 6import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
@@ -11,12 +11,12 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator
11import { VideoLiveModel } from '@server/models/video/video-live' 11import { VideoLiveModel } from '@server/models/video/video-live'
12import { MVideoDetails, MVideoFullLight } from '@server/types/models' 12import { MVideoDetails, MVideoFullLight } from '@server/types/models'
13import { LiveVideoCreate, LiveVideoUpdate, VideoState } from '../../../../shared' 13import { LiveVideoCreate, LiveVideoUpdate, VideoState } from '../../../../shared'
14import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
14import { logger } from '../../../helpers/logger' 15import { logger } from '../../../helpers/logger'
15import { sequelizeTypescript } from '../../../initializers/database' 16import { sequelizeTypescript } from '../../../initializers/database'
16import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail' 17import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
17import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' 18import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
18import { VideoModel } from '../../../models/video/video' 19import { VideoModel } from '../../../models/video/video'
19import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
20 20
21const liveRouter = express.Router() 21const liveRouter = express.Router()
22 22
@@ -76,7 +76,7 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
76 76
77 await federateVideoIfNeeded(video, false) 77 await federateVideoIfNeeded(video, false)
78 78
79 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 79 return res.status(HttpStatusCode.NO_CONTENT_204).end()
80} 80}
81 81
82async function addLiveVideo (req: express.Request, res: express.Response) { 82async function addLiveVideo (req: express.Request, res: express.Response) {
@@ -94,13 +94,13 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
94 const videoLive = new VideoLiveModel() 94 const videoLive = new VideoLiveModel()
95 videoLive.saveReplay = videoInfo.saveReplay || false 95 videoLive.saveReplay = videoInfo.saveReplay || false
96 videoLive.permanentLive = videoInfo.permanentLive || false 96 videoLive.permanentLive = videoInfo.permanentLive || false
97 videoLive.streamKey = uuidv4() 97 videoLive.streamKey = buildUUID()
98 98
99 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ 99 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
100 video, 100 video,
101 files: req.files, 101 files: req.files,
102 fallback: type => { 102 fallback: type => {
103 return createVideoMiniatureFromExisting({ 103 return updateVideoMiniatureFromExisting({
104 inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, 104 inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND,
105 video, 105 video,
106 type, 106 type,
@@ -138,6 +138,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
138 return res.json({ 138 return res.json({
139 video: { 139 video: {
140 id: videoCreated.id, 140 id: videoCreated.id,
141 shortUUID: uuidToShort(videoCreated.uuid),
141 uuid: videoCreated.uuid 142 uuid: videoCreated.uuid
142 } 143 }
143 }) 144 })
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index a85d7c30b..1bb96e046 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -99,15 +99,15 @@ 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
102async function acceptOwnership (req: express.Request, res: express.Response) { 102function 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
106 106
107 // We need more attributes for federation 107 // We need more attributes for federation
108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) 108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id, t)
109 109
110 const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId) 110 const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId, t)
111 111
112 targetVideo.channelId = channel.id 112 targetVideo.channelId = channel.id
113 113
@@ -122,17 +122,17 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
122 videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED 122 videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED
123 await videoChangeOwnership.save({ transaction: t }) 123 await videoChangeOwnership.save({ transaction: t })
124 124
125 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 125 return res.status(HttpStatusCode.NO_CONTENT_204).end()
126 }) 126 })
127} 127}
128 128
129async function refuseOwnership (req: express.Request, res: express.Response) { 129function 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
133 videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED 133 videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED
134 await videoChangeOwnership.save({ transaction: t }) 134 await videoChangeOwnership.save({ transaction: t })
135 135
136 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 136 return res.status(HttpStatusCode.NO_CONTENT_204).end()
137 }) 137 })
138} 138}
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts
new file mode 100644
index 000000000..8affe71c6
--- /dev/null
+++ b/server/controllers/api/videos/update.ts
@@ -0,0 +1,193 @@
1import * as express from 'express'
2import { Transaction } from 'sequelize/types'
3import { changeVideoChannelShare } from '@server/lib/activitypub/share'
4import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
5import { FilteredModelAttributes } from '@server/types'
6import { MVideoFullLight } from '@server/types/models'
7import { VideoUpdate } from '../../../../shared'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
9import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
10import { resetSequelizeInstance } from '../../../helpers/database-utils'
11import { createReqFiles } from '../../../helpers/express-utils'
12import { logger, loggerTagsFactory } from '../../../helpers/logger'
13import { CONFIG } from '../../../initializers/config'
14import { MIMETYPES } from '../../../initializers/constants'
15import { sequelizeTypescript } from '../../../initializers/database'
16import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
17import { Notifier } from '../../../lib/notifier'
18import { Hooks } from '../../../lib/plugins/hooks'
19import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
20import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
21import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
22import { VideoModel } from '../../../models/video/video'
23import { openapiOperationDoc } from '@server/middlewares/doc'
24
25const lTags = loggerTagsFactory('api', 'video')
26const auditLogger = auditLoggerFactory('videos')
27const updateRouter = express.Router()
28
29const reqVideoFileUpdate = createReqFiles(
30 [ 'thumbnailfile', 'previewfile' ],
31 MIMETYPES.IMAGE.MIMETYPE_EXT,
32 {
33 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
34 previewfile: CONFIG.STORAGE.TMP_DIR
35 }
36)
37
38updateRouter.put('/:id',
39 openapiOperationDoc({ operationId: 'putVideo' }),
40 authenticate,
41 reqVideoFileUpdate,
42 asyncMiddleware(videosUpdateValidator),
43 asyncRetryTransactionMiddleware(updateVideo)
44)
45
46// ---------------------------------------------------------------------------
47
48export {
49 updateRouter
50}
51
52// ---------------------------------------------------------------------------
53
54export async function updateVideo (req: express.Request, res: express.Response) {
55 const videoInstance = res.locals.videoAll
56 const videoFieldsSave = videoInstance.toJSON()
57 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
58 const videoInfoToUpdate: VideoUpdate = req.body
59
60 const wasConfidentialVideo = videoInstance.isConfidential()
61 const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
62
63 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
64 video: videoInstance,
65 files: req.files,
66 fallback: () => Promise.resolve(undefined),
67 automaticallyGenerated: false
68 })
69
70 try {
71 const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
72 const sequelizeOptions = { transaction: t }
73 const oldVideoChannel = videoInstance.VideoChannel
74
75 const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [
76 'name',
77 'category',
78 'licence',
79 'language',
80 'nsfw',
81 'waitTranscoding',
82 'support',
83 'description',
84 'commentsEnabled',
85 'downloadEnabled'
86 ]
87
88 for (const key of keysToUpdate) {
89 if (videoInfoToUpdate[key] !== undefined) videoInstance.set(key, videoInfoToUpdate[key])
90 }
91
92 if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) {
93 videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
94 }
95
96 // Privacy update?
97 let isNewVideo = false
98 if (videoInfoToUpdate.privacy !== undefined) {
99 isNewVideo = await updateVideoPrivacy({ videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction: t })
100 }
101
102 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
103
104 // Thumbnail & preview updates?
105 if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
106 if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
107
108 // Video tags update?
109 if (videoInfoToUpdate.tags !== undefined) {
110 await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t })
111 }
112
113 // Video channel update?
114 if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
115 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
116 videoInstanceUpdated.VideoChannel = res.locals.videoChannel
117
118 if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
119 }
120
121 // Schedule an update in the future?
122 await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t)
123
124 await autoBlacklistVideoIfNeeded({
125 video: videoInstanceUpdated,
126 user: res.locals.oauth.token.User,
127 isRemote: false,
128 isNew: false,
129 transaction: t
130 })
131
132 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
133
134 auditLogger.update(
135 getAuditIdFromRes(res),
136 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
137 oldVideoAuditView
138 )
139 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
140
141 return videoInstanceUpdated
142 })
143
144 if (wasConfidentialVideo) {
145 Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
146 }
147
148 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
149 } catch (err) {
150 // Force fields we want to update
151 // If the transaction is retried, sequelize will think the object has not changed
152 // So it will skip the SQL request, even if the last one was ROLLBACKed!
153 resetSequelizeInstance(videoInstance, videoFieldsSave)
154
155 throw err
156 }
157
158 return res.type('json')
159 .status(HttpStatusCode.NO_CONTENT_204)
160 .end()
161}
162
163async function updateVideoPrivacy (options: {
164 videoInstance: MVideoFullLight
165 videoInfoToUpdate: VideoUpdate
166 hadPrivacyForFederation: boolean
167 transaction: Transaction
168}) {
169 const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
170 const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
171
172 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
173 videoInstance.setPrivacy(newPrivacy)
174
175 // Unfederate the video if the new privacy is not compatible with federation
176 if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
177 await VideoModel.sendDelete(videoInstance, { transaction })
178 }
179
180 return isNewVideo
181}
182
183function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) {
184 if (videoInfoToUpdate.scheduleUpdate) {
185 return ScheduleVideoUpdateModel.upsert({
186 videoId: videoInstance.id,
187 updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt),
188 privacy: videoInfoToUpdate.scheduleUpdate.privacy || null
189 }, { transaction })
190 } else if (videoInfoToUpdate.scheduleUpdate === null) {
191 return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction)
192 }
193}
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
new file mode 100644
index 000000000..bcd21ac99
--- /dev/null
+++ b/server/controllers/api/videos/upload.ts
@@ -0,0 +1,278 @@
1import * as express from 'express'
2import { move } from 'fs-extra'
3import { getLowercaseExtension } from '@server/helpers/core-utils'
4import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
5import { uuidToShort } from '@server/helpers/uuid'
6import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
7import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
8import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
9import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
10import { openapiOperationDoc } from '@server/middlewares/doc'
11import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
12import { uploadx } from '@uploadx/core'
13import { VideoCreate, VideoState } from '../../../../shared'
14import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
15import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
16import { retryTransactionWrapper } from '../../../helpers/database-utils'
17import { createReqFiles } from '../../../helpers/express-utils'
18import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
19import { logger, loggerTagsFactory } from '../../../helpers/logger'
20import { CONFIG } from '../../../initializers/config'
21import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants'
22import { sequelizeTypescript } from '../../../initializers/database'
23import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
24import { Notifier } from '../../../lib/notifier'
25import { Hooks } from '../../../lib/plugins/hooks'
26import { generateVideoMiniature } from '../../../lib/thumbnail'
27import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
28import {
29 asyncMiddleware,
30 asyncRetryTransactionMiddleware,
31 authenticate,
32 videosAddLegacyValidator,
33 videosAddResumableInitValidator,
34 videosAddResumableValidator
35} from '../../../middlewares'
36import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
37import { VideoModel } from '../../../models/video/video'
38import { VideoFileModel } from '../../../models/video/video-file'
39
40const lTags = loggerTagsFactory('api', 'video')
41const auditLogger = auditLoggerFactory('videos')
42const uploadRouter = express.Router()
43const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
44
45const reqVideoFileAdd = createReqFiles(
46 [ 'videofile', 'thumbnailfile', 'previewfile' ],
47 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
48 {
49 videofile: CONFIG.STORAGE.TMP_DIR,
50 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
51 previewfile: CONFIG.STORAGE.TMP_DIR
52 }
53)
54
55const reqVideoFileAddResumable = createReqFiles(
56 [ 'thumbnailfile', 'previewfile' ],
57 MIMETYPES.IMAGE.MIMETYPE_EXT,
58 {
59 thumbnailfile: getResumableUploadPath(),
60 previewfile: getResumableUploadPath()
61 }
62)
63
64uploadRouter.post('/upload',
65 openapiOperationDoc({ operationId: 'uploadLegacy' }),
66 authenticate,
67 reqVideoFileAdd,
68 asyncMiddleware(videosAddLegacyValidator),
69 asyncRetryTransactionMiddleware(addVideoLegacy)
70)
71
72uploadRouter.post('/upload-resumable',
73 openapiOperationDoc({ operationId: 'uploadResumableInit' }),
74 authenticate,
75 reqVideoFileAddResumable,
76 asyncMiddleware(videosAddResumableInitValidator),
77 uploadxMiddleware
78)
79
80uploadRouter.delete('/upload-resumable',
81 authenticate,
82 uploadxMiddleware
83)
84
85uploadRouter.put('/upload-resumable',
86 openapiOperationDoc({ operationId: 'uploadResumable' }),
87 authenticate,
88 uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
89 asyncMiddleware(videosAddResumableValidator),
90 asyncMiddleware(addVideoResumable)
91)
92
93// ---------------------------------------------------------------------------
94
95export {
96 uploadRouter
97}
98
99// ---------------------------------------------------------------------------
100
101export async function addVideoLegacy (req: express.Request, res: express.Response) {
102 // Uploading the video could be long
103 // Set timeout to 10 minutes, as Express's default is 2 minutes
104 req.setTimeout(1000 * 60 * 10, () => {
105 logger.error('Video upload has timed out.')
106 return res.fail({
107 status: HttpStatusCode.REQUEST_TIMEOUT_408,
108 message: 'Video upload has timed out.'
109 })
110 })
111
112 const videoPhysicalFile = req.files['videofile'][0]
113 const videoInfo: VideoCreate = req.body
114 const files = req.files
115
116 return addVideo({ res, videoPhysicalFile, videoInfo, files })
117}
118
119export async function addVideoResumable (_req: express.Request, res: express.Response) {
120 const videoPhysicalFile = res.locals.videoFileResumable
121 const videoInfo = videoPhysicalFile.metadata
122 const files = { previewfile: videoInfo.previewfile }
123
124 // Don't need the meta file anymore
125 await deleteResumableUploadMetaFile(videoPhysicalFile.path)
126
127 return addVideo({ res, videoPhysicalFile, videoInfo, files })
128}
129
130async function addVideo (options: {
131 res: express.Response
132 videoPhysicalFile: express.VideoUploadFile
133 videoInfo: VideoCreate
134 files: express.UploadFiles
135}) {
136 const { res, videoPhysicalFile, videoInfo, files } = options
137 const videoChannel = res.locals.videoChannel
138 const user = res.locals.oauth.token.User
139
140 const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
141
142 videoData.state = CONFIG.TRANSCODING.ENABLED
143 ? VideoState.TO_TRANSCODE
144 : VideoState.PUBLISHED
145
146 videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
147
148 const video = new VideoModel(videoData) as MVideoFullLight
149 video.VideoChannel = videoChannel
150 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
151
152 const videoFile = await buildNewFile(video, videoPhysicalFile)
153
154 // Move physical file
155 const destination = getVideoFilePath(video, videoFile)
156 await move(videoPhysicalFile.path, destination)
157 // This is important in case if there is another attempt in the retry process
158 videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
159 videoPhysicalFile.path = destination
160
161 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
162 video,
163 files,
164 fallback: type => generateVideoMiniature({ video, videoFile, type })
165 })
166
167 const { videoCreated } = await sequelizeTypescript.transaction(async t => {
168 const sequelizeOptions = { transaction: t }
169
170 const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
171
172 await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
173 await videoCreated.addAndSaveThumbnail(previewModel, t)
174
175 // Do not forget to add video channel information to the created video
176 videoCreated.VideoChannel = res.locals.videoChannel
177
178 videoFile.videoId = video.id
179 await videoFile.save(sequelizeOptions)
180
181 video.VideoFiles = [ videoFile ]
182
183 await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
184
185 // Schedule an update in the future?
186 if (videoInfo.scheduleUpdate) {
187 await ScheduleVideoUpdateModel.create({
188 videoId: video.id,
189 updateAt: new Date(videoInfo.scheduleUpdate.updateAt),
190 privacy: videoInfo.scheduleUpdate.privacy || null
191 }, sequelizeOptions)
192 }
193
194 // Channel has a new content, set as updated
195 await videoCreated.VideoChannel.setAsUpdated(t)
196
197 await autoBlacklistVideoIfNeeded({
198 video,
199 user,
200 isRemote: false,
201 isNew: true,
202 transaction: t
203 })
204
205 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
206 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
207
208 return { videoCreated }
209 })
210
211 createTorrentFederate(video, videoFile)
212
213 if (video.state === VideoState.TO_TRANSCODE) {
214 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
215 }
216
217 Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
218
219 return res.json({
220 video: {
221 id: videoCreated.id,
222 shortUUID: uuidToShort(videoCreated.uuid),
223 uuid: videoCreated.uuid
224 }
225 })
226}
227
228async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) {
229 const videoFile = new VideoFileModel({
230 extname: getLowercaseExtension(videoPhysicalFile.filename),
231 size: videoPhysicalFile.size,
232 videoStreamingPlaylistId: null,
233 metadata: await getMetadataFromFile(videoPhysicalFile.path)
234 })
235
236 if (videoFile.isAudio()) {
237 videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
238 } else {
239 videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
240 videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
241 }
242
243 videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
244
245 return videoFile
246}
247
248async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) {
249 await createTorrentAndSetInfoHash(video, fileArg)
250
251 // Refresh videoFile because the createTorrentAndSetInfoHash could be long
252 const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
253 // File does not exist anymore, remove the generated torrent
254 if (!refreshedFile) return fileArg.removeTorrent()
255
256 refreshedFile.infoHash = fileArg.infoHash
257 refreshedFile.torrentFilename = fileArg.torrentFilename
258
259 return refreshedFile.save()
260}
261
262function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void {
263 // Create the torrent file in async way because it could be long
264 createTorrentAndSetInfoHashAsync(video, videoFile)
265 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
266 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
267 .then(refreshedVideo => {
268 if (!refreshedVideo) return
269
270 // Only federate and notify after the torrent creation
271 Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
272
273 return retryTransactionWrapper(() => {
274 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
275 })
276 })
277 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
278}
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts
index 627f12aa9..8b15525aa 100644
--- a/server/controllers/api/videos/watching.ts
+++ b/server/controllers/api/videos/watching.ts
@@ -1,12 +1,19 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserWatchingVideo } from '../../../../shared' 2import { UserWatchingVideo } from '../../../../shared'
3import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' 3import {
4import { UserVideoHistoryModel } from '../../../models/account/user-video-history' 4 asyncMiddleware,
5 asyncRetryTransactionMiddleware,
6 authenticate,
7 openapiOperationDoc,
8 videoWatchingValidator
9} from '../../../middlewares'
10import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
5import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 11import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
6 12
7const watchingRouter = express.Router() 13const watchingRouter = express.Router()
8 14
9watchingRouter.put('/:videoId/watching', 15watchingRouter.put('/:videoId/watching',
16 openapiOperationDoc({ operationId: 'setProgress' }),
10 authenticate, 17 authenticate,
11 asyncMiddleware(videoWatchingValidator), 18 asyncMiddleware(videoWatchingValidator),
12 asyncRetryTransactionMiddleware(userWatchVideo) 19 asyncRetryTransactionMiddleware(userWatchVideo)