diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/controllers/api/videos | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/controllers/api/videos')
19 files changed, 0 insertions, 2708 deletions
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts deleted file mode 100644 index 4103bb063..000000000 --- a/server/controllers/api/videos/blacklist.ts +++ /dev/null | |||
@@ -1,112 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { blacklistVideo, unblacklistVideo } from '@server/lib/video-blacklist' | ||
3 | import { HttpStatusCode, UserRight, VideoBlacklistCreate } from '@shared/models' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { getFormattedObjects } from '../../../helpers/utils' | ||
6 | import { sequelizeTypescript } from '../../../initializers/database' | ||
7 | import { | ||
8 | asyncMiddleware, | ||
9 | authenticate, | ||
10 | blacklistSortValidator, | ||
11 | ensureUserHasRight, | ||
12 | openapiOperationDoc, | ||
13 | paginationValidator, | ||
14 | setBlacklistSort, | ||
15 | setDefaultPagination, | ||
16 | videosBlacklistAddValidator, | ||
17 | videosBlacklistFiltersValidator, | ||
18 | videosBlacklistRemoveValidator, | ||
19 | videosBlacklistUpdateValidator | ||
20 | } from '../../../middlewares' | ||
21 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | ||
22 | |||
23 | const blacklistRouter = express.Router() | ||
24 | |||
25 | blacklistRouter.post('/:videoId/blacklist', | ||
26 | openapiOperationDoc({ operationId: 'addVideoBlock' }), | ||
27 | authenticate, | ||
28 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
29 | asyncMiddleware(videosBlacklistAddValidator), | ||
30 | asyncMiddleware(addVideoToBlacklistController) | ||
31 | ) | ||
32 | |||
33 | blacklistRouter.get('/blacklist', | ||
34 | openapiOperationDoc({ operationId: 'getVideoBlocks' }), | ||
35 | authenticate, | ||
36 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
37 | paginationValidator, | ||
38 | blacklistSortValidator, | ||
39 | setBlacklistSort, | ||
40 | setDefaultPagination, | ||
41 | videosBlacklistFiltersValidator, | ||
42 | asyncMiddleware(listBlacklist) | ||
43 | ) | ||
44 | |||
45 | blacklistRouter.put('/:videoId/blacklist', | ||
46 | authenticate, | ||
47 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
48 | asyncMiddleware(videosBlacklistUpdateValidator), | ||
49 | asyncMiddleware(updateVideoBlacklistController) | ||
50 | ) | ||
51 | |||
52 | blacklistRouter.delete('/:videoId/blacklist', | ||
53 | openapiOperationDoc({ operationId: 'delVideoBlock' }), | ||
54 | authenticate, | ||
55 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | ||
56 | asyncMiddleware(videosBlacklistRemoveValidator), | ||
57 | asyncMiddleware(removeVideoFromBlacklistController) | ||
58 | ) | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { | ||
63 | blacklistRouter | ||
64 | } | ||
65 | |||
66 | // --------------------------------------------------------------------------- | ||
67 | |||
68 | async function addVideoToBlacklistController (req: express.Request, res: express.Response) { | ||
69 | const videoInstance = res.locals.videoAll | ||
70 | const body: VideoBlacklistCreate = req.body | ||
71 | |||
72 | await blacklistVideo(videoInstance, body) | ||
73 | |||
74 | logger.info('Video %s blacklisted.', videoInstance.uuid) | ||
75 | |||
76 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
77 | } | ||
78 | |||
79 | async function updateVideoBlacklistController (req: express.Request, res: express.Response) { | ||
80 | const videoBlacklist = res.locals.videoBlacklist | ||
81 | |||
82 | if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason | ||
83 | |||
84 | await sequelizeTypescript.transaction(t => { | ||
85 | return videoBlacklist.save({ transaction: t }) | ||
86 | }) | ||
87 | |||
88 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
89 | } | ||
90 | |||
91 | async function listBlacklist (req: express.Request, res: express.Response) { | ||
92 | const resultList = await VideoBlacklistModel.listForApi({ | ||
93 | start: req.query.start, | ||
94 | count: req.query.count, | ||
95 | sort: req.query.sort, | ||
96 | search: req.query.search, | ||
97 | type: req.query.type | ||
98 | }) | ||
99 | |||
100 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
101 | } | ||
102 | |||
103 | async function removeVideoFromBlacklistController (req: express.Request, res: express.Response) { | ||
104 | const videoBlacklist = res.locals.videoBlacklist | ||
105 | const video = res.locals.videoAll | ||
106 | |||
107 | await unblacklistVideo(videoBlacklist, video) | ||
108 | |||
109 | logger.info('Video %s removed from blacklist.', video.uuid) | ||
110 | |||
111 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
112 | } | ||
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts deleted file mode 100644 index 2b511a398..000000000 --- a/server/controllers/api/videos/captions.ts +++ /dev/null | |||
@@ -1,93 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { Hooks } from '@server/lib/plugins/hooks' | ||
3 | import { MVideoCaption } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
5 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' | ||
6 | import { createReqFiles } from '../../../helpers/express-utils' | ||
7 | import { logger } from '../../../helpers/logger' | ||
8 | import { getFormattedObjects } from '../../../helpers/utils' | ||
9 | import { MIMETYPES } from '../../../initializers/constants' | ||
10 | import { sequelizeTypescript } from '../../../initializers/database' | ||
11 | import { federateVideoIfNeeded } from '../../../lib/activitypub/videos' | ||
12 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' | ||
13 | import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' | ||
14 | import { VideoCaptionModel } from '../../../models/video/video-caption' | ||
15 | |||
16 | const reqVideoCaptionAdd = createReqFiles([ 'captionfile' ], MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) | ||
17 | |||
18 | const videoCaptionsRouter = express.Router() | ||
19 | |||
20 | videoCaptionsRouter.get('/:videoId/captions', | ||
21 | asyncMiddleware(listVideoCaptionsValidator), | ||
22 | asyncMiddleware(listVideoCaptions) | ||
23 | ) | ||
24 | videoCaptionsRouter.put('/:videoId/captions/:captionLanguage', | ||
25 | authenticate, | ||
26 | reqVideoCaptionAdd, | ||
27 | asyncMiddleware(addVideoCaptionValidator), | ||
28 | asyncRetryTransactionMiddleware(addVideoCaption) | ||
29 | ) | ||
30 | videoCaptionsRouter.delete('/:videoId/captions/:captionLanguage', | ||
31 | authenticate, | ||
32 | asyncMiddleware(deleteVideoCaptionValidator), | ||
33 | asyncRetryTransactionMiddleware(deleteVideoCaption) | ||
34 | ) | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | videoCaptionsRouter | ||
40 | } | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | async function listVideoCaptions (req: express.Request, res: express.Response) { | ||
45 | const data = await VideoCaptionModel.listVideoCaptions(res.locals.onlyVideo.id) | ||
46 | |||
47 | return res.json(getFormattedObjects(data, data.length)) | ||
48 | } | ||
49 | |||
50 | async function addVideoCaption (req: express.Request, res: express.Response) { | ||
51 | const videoCaptionPhysicalFile = req.files['captionfile'][0] | ||
52 | const video = res.locals.videoAll | ||
53 | |||
54 | const captionLanguage = req.params.captionLanguage | ||
55 | |||
56 | const videoCaption = new VideoCaptionModel({ | ||
57 | videoId: video.id, | ||
58 | filename: VideoCaptionModel.generateCaptionName(captionLanguage), | ||
59 | language: captionLanguage | ||
60 | }) as MVideoCaption | ||
61 | |||
62 | // Move physical file | ||
63 | await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption) | ||
64 | |||
65 | await sequelizeTypescript.transaction(async t => { | ||
66 | await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t) | ||
67 | |||
68 | // Update video update | ||
69 | await federateVideoIfNeeded(video, false, t) | ||
70 | }) | ||
71 | |||
72 | Hooks.runAction('action:api.video-caption.created', { caption: videoCaption, req, res }) | ||
73 | |||
74 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
75 | } | ||
76 | |||
77 | async function deleteVideoCaption (req: express.Request, res: express.Response) { | ||
78 | const video = res.locals.videoAll | ||
79 | const videoCaption = res.locals.videoCaption | ||
80 | |||
81 | await sequelizeTypescript.transaction(async t => { | ||
82 | await videoCaption.destroy({ transaction: t }) | ||
83 | |||
84 | // Send video update | ||
85 | await federateVideoIfNeeded(video, false, t) | ||
86 | }) | ||
87 | |||
88 | logger.info('Video caption %s of video %s deleted.', videoCaption.language, video.uuid) | ||
89 | |||
90 | Hooks.runAction('action:api.video-caption.deleted', { caption: videoCaption, req, res }) | ||
91 | |||
92 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
93 | } | ||
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts deleted file mode 100644 index 70ca21500..000000000 --- a/server/controllers/api/videos/comment.ts +++ /dev/null | |||
@@ -1,234 +0,0 @@ | |||
1 | import { MCommentFormattable } from '@server/types/models' | ||
2 | import express from 'express' | ||
3 | |||
4 | import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models' | ||
5 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
6 | import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model' | ||
7 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | ||
8 | import { getFormattedObjects } from '../../../helpers/utils' | ||
9 | import { sequelizeTypescript } from '../../../initializers/database' | ||
10 | import { Notifier } from '../../../lib/notifier' | ||
11 | import { Hooks } from '../../../lib/plugins/hooks' | ||
12 | import { buildFormattedCommentTree, createVideoComment, removeComment } from '../../../lib/video-comment' | ||
13 | import { | ||
14 | asyncMiddleware, | ||
15 | asyncRetryTransactionMiddleware, | ||
16 | authenticate, | ||
17 | ensureUserHasRight, | ||
18 | optionalAuthenticate, | ||
19 | paginationValidator, | ||
20 | setDefaultPagination, | ||
21 | setDefaultSort | ||
22 | } from '../../../middlewares' | ||
23 | import { | ||
24 | addVideoCommentReplyValidator, | ||
25 | addVideoCommentThreadValidator, | ||
26 | listVideoCommentsValidator, | ||
27 | listVideoCommentThreadsValidator, | ||
28 | listVideoThreadCommentsValidator, | ||
29 | removeVideoCommentValidator, | ||
30 | videoCommentsValidator, | ||
31 | videoCommentThreadsSortValidator | ||
32 | } from '../../../middlewares/validators' | ||
33 | import { AccountModel } from '../../../models/account/account' | ||
34 | import { VideoCommentModel } from '../../../models/video/video-comment' | ||
35 | |||
36 | const auditLogger = auditLoggerFactory('comments') | ||
37 | const videoCommentRouter = express.Router() | ||
38 | |||
39 | videoCommentRouter.get('/:videoId/comment-threads', | ||
40 | paginationValidator, | ||
41 | videoCommentThreadsSortValidator, | ||
42 | setDefaultSort, | ||
43 | setDefaultPagination, | ||
44 | asyncMiddleware(listVideoCommentThreadsValidator), | ||
45 | optionalAuthenticate, | ||
46 | asyncMiddleware(listVideoThreads) | ||
47 | ) | ||
48 | videoCommentRouter.get('/:videoId/comment-threads/:threadId', | ||
49 | asyncMiddleware(listVideoThreadCommentsValidator), | ||
50 | optionalAuthenticate, | ||
51 | asyncMiddleware(listVideoThreadComments) | ||
52 | ) | ||
53 | |||
54 | videoCommentRouter.post('/:videoId/comment-threads', | ||
55 | authenticate, | ||
56 | asyncMiddleware(addVideoCommentThreadValidator), | ||
57 | asyncRetryTransactionMiddleware(addVideoCommentThread) | ||
58 | ) | ||
59 | videoCommentRouter.post('/:videoId/comments/:commentId', | ||
60 | authenticate, | ||
61 | asyncMiddleware(addVideoCommentReplyValidator), | ||
62 | asyncRetryTransactionMiddleware(addVideoCommentReply) | ||
63 | ) | ||
64 | videoCommentRouter.delete('/:videoId/comments/:commentId', | ||
65 | authenticate, | ||
66 | asyncMiddleware(removeVideoCommentValidator), | ||
67 | asyncRetryTransactionMiddleware(removeVideoComment) | ||
68 | ) | ||
69 | |||
70 | videoCommentRouter.get('/comments', | ||
71 | authenticate, | ||
72 | ensureUserHasRight(UserRight.SEE_ALL_COMMENTS), | ||
73 | paginationValidator, | ||
74 | videoCommentsValidator, | ||
75 | setDefaultSort, | ||
76 | setDefaultPagination, | ||
77 | listVideoCommentsValidator, | ||
78 | asyncMiddleware(listComments) | ||
79 | ) | ||
80 | |||
81 | // --------------------------------------------------------------------------- | ||
82 | |||
83 | export { | ||
84 | videoCommentRouter | ||
85 | } | ||
86 | |||
87 | // --------------------------------------------------------------------------- | ||
88 | |||
89 | async function listComments (req: express.Request, res: express.Response) { | ||
90 | const options = { | ||
91 | start: req.query.start, | ||
92 | count: req.query.count, | ||
93 | sort: req.query.sort, | ||
94 | |||
95 | isLocal: req.query.isLocal, | ||
96 | onLocalVideo: req.query.onLocalVideo, | ||
97 | search: req.query.search, | ||
98 | searchAccount: req.query.searchAccount, | ||
99 | searchVideo: req.query.searchVideo | ||
100 | } | ||
101 | |||
102 | const resultList = await VideoCommentModel.listCommentsForApi(options) | ||
103 | |||
104 | return res.json({ | ||
105 | total: resultList.total, | ||
106 | data: resultList.data.map(c => c.toFormattedAdminJSON()) | ||
107 | }) | ||
108 | } | ||
109 | |||
110 | async function listVideoThreads (req: express.Request, res: express.Response) { | ||
111 | const video = res.locals.onlyVideo | ||
112 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
113 | |||
114 | let resultList: ThreadsResultList<MCommentFormattable> | ||
115 | |||
116 | if (video.commentsEnabled === true) { | ||
117 | const apiOptions = await Hooks.wrapObject({ | ||
118 | videoId: video.id, | ||
119 | isVideoOwned: video.isOwned(), | ||
120 | start: req.query.start, | ||
121 | count: req.query.count, | ||
122 | sort: req.query.sort, | ||
123 | user | ||
124 | }, 'filter:api.video-threads.list.params') | ||
125 | |||
126 | resultList = await Hooks.wrapPromiseFun( | ||
127 | VideoCommentModel.listThreadsForApi, | ||
128 | apiOptions, | ||
129 | 'filter:api.video-threads.list.result' | ||
130 | ) | ||
131 | } else { | ||
132 | resultList = { | ||
133 | total: 0, | ||
134 | totalNotDeletedComments: 0, | ||
135 | data: [] | ||
136 | } | ||
137 | } | ||
138 | |||
139 | return res.json({ | ||
140 | ...getFormattedObjects(resultList.data, resultList.total), | ||
141 | totalNotDeletedComments: resultList.totalNotDeletedComments | ||
142 | } as VideoCommentThreads) | ||
143 | } | ||
144 | |||
145 | async function listVideoThreadComments (req: express.Request, res: express.Response) { | ||
146 | const video = res.locals.onlyVideo | ||
147 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
148 | |||
149 | let resultList: ResultList<MCommentFormattable> | ||
150 | |||
151 | if (video.commentsEnabled === true) { | ||
152 | const apiOptions = await Hooks.wrapObject({ | ||
153 | videoId: video.id, | ||
154 | threadId: res.locals.videoCommentThread.id, | ||
155 | user | ||
156 | }, 'filter:api.video-thread-comments.list.params') | ||
157 | |||
158 | resultList = await Hooks.wrapPromiseFun( | ||
159 | VideoCommentModel.listThreadCommentsForApi, | ||
160 | apiOptions, | ||
161 | 'filter:api.video-thread-comments.list.result' | ||
162 | ) | ||
163 | } else { | ||
164 | resultList = { | ||
165 | total: 0, | ||
166 | data: [] | ||
167 | } | ||
168 | } | ||
169 | |||
170 | if (resultList.data.length === 0) { | ||
171 | return res.fail({ | ||
172 | status: HttpStatusCode.NOT_FOUND_404, | ||
173 | message: 'No comments were found' | ||
174 | }) | ||
175 | } | ||
176 | |||
177 | return res.json(buildFormattedCommentTree(resultList)) | ||
178 | } | ||
179 | |||
180 | async function addVideoCommentThread (req: express.Request, res: express.Response) { | ||
181 | const videoCommentInfo: VideoCommentCreate = req.body | ||
182 | |||
183 | const comment = await sequelizeTypescript.transaction(async t => { | ||
184 | const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) | ||
185 | |||
186 | return createVideoComment({ | ||
187 | text: videoCommentInfo.text, | ||
188 | inReplyToComment: null, | ||
189 | video: res.locals.videoAll, | ||
190 | account | ||
191 | }, t) | ||
192 | }) | ||
193 | |||
194 | Notifier.Instance.notifyOnNewComment(comment) | ||
195 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | ||
196 | |||
197 | Hooks.runAction('action:api.video-thread.created', { comment, req, res }) | ||
198 | |||
199 | return res.json({ comment: comment.toFormattedJSON() }) | ||
200 | } | ||
201 | |||
202 | async function addVideoCommentReply (req: express.Request, res: express.Response) { | ||
203 | const videoCommentInfo: VideoCommentCreate = req.body | ||
204 | |||
205 | const comment = await sequelizeTypescript.transaction(async t => { | ||
206 | const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) | ||
207 | |||
208 | return createVideoComment({ | ||
209 | text: videoCommentInfo.text, | ||
210 | inReplyToComment: res.locals.videoCommentFull, | ||
211 | video: res.locals.videoAll, | ||
212 | account | ||
213 | }, t) | ||
214 | }) | ||
215 | |||
216 | Notifier.Instance.notifyOnNewComment(comment) | ||
217 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | ||
218 | |||
219 | Hooks.runAction('action:api.video-comment-reply.created', { comment, req, res }) | ||
220 | |||
221 | return res.json({ comment: comment.toFormattedJSON() }) | ||
222 | } | ||
223 | |||
224 | async function removeVideoComment (req: express.Request, res: express.Response) { | ||
225 | const videoCommentInstance = res.locals.videoCommentFull | ||
226 | |||
227 | await removeComment(videoCommentInstance, req, res) | ||
228 | |||
229 | auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON())) | ||
230 | |||
231 | return res.type('json') | ||
232 | .status(HttpStatusCode.NO_CONTENT_204) | ||
233 | .end() | ||
234 | } | ||
diff --git a/server/controllers/api/videos/files.ts b/server/controllers/api/videos/files.ts deleted file mode 100644 index 67b60ff63..000000000 --- a/server/controllers/api/videos/files.ts +++ /dev/null | |||
@@ -1,122 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import toInt from 'validator/lib/toInt' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
4 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
5 | import { updatePlaylistAfterFileChange } from '@server/lib/hls' | ||
6 | import { removeAllWebVideoFiles, removeHLSFile, removeHLSPlaylist, removeWebVideoFile } from '@server/lib/video-file' | ||
7 | import { VideoFileModel } from '@server/models/video/video-file' | ||
8 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
9 | import { | ||
10 | asyncMiddleware, | ||
11 | authenticate, | ||
12 | ensureUserHasRight, | ||
13 | videoFileMetadataGetValidator, | ||
14 | videoFilesDeleteHLSFileValidator, | ||
15 | videoFilesDeleteHLSValidator, | ||
16 | videoFilesDeleteWebVideoFileValidator, | ||
17 | videoFilesDeleteWebVideoValidator, | ||
18 | videosGetValidator | ||
19 | } from '../../../middlewares' | ||
20 | |||
21 | const lTags = loggerTagsFactory('api', 'video') | ||
22 | const filesRouter = express.Router() | ||
23 | |||
24 | filesRouter.get('/:id/metadata/:videoFileId', | ||
25 | asyncMiddleware(videosGetValidator), | ||
26 | asyncMiddleware(videoFileMetadataGetValidator), | ||
27 | asyncMiddleware(getVideoFileMetadata) | ||
28 | ) | ||
29 | |||
30 | filesRouter.delete('/:id/hls', | ||
31 | authenticate, | ||
32 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
33 | asyncMiddleware(videoFilesDeleteHLSValidator), | ||
34 | asyncMiddleware(removeHLSPlaylistController) | ||
35 | ) | ||
36 | filesRouter.delete('/:id/hls/:videoFileId', | ||
37 | authenticate, | ||
38 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
39 | asyncMiddleware(videoFilesDeleteHLSFileValidator), | ||
40 | asyncMiddleware(removeHLSFileController) | ||
41 | ) | ||
42 | |||
43 | filesRouter.delete( | ||
44 | [ '/:id/webtorrent', '/:id/web-videos' ], // TODO: remove webtorrent in V7 | ||
45 | authenticate, | ||
46 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
47 | asyncMiddleware(videoFilesDeleteWebVideoValidator), | ||
48 | asyncMiddleware(removeAllWebVideoFilesController) | ||
49 | ) | ||
50 | filesRouter.delete( | ||
51 | [ '/:id/webtorrent/:videoFileId', '/:id/web-videos/:videoFileId' ], // TODO: remove webtorrent in V7 | ||
52 | authenticate, | ||
53 | ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), | ||
54 | asyncMiddleware(videoFilesDeleteWebVideoFileValidator), | ||
55 | asyncMiddleware(removeWebVideoFileController) | ||
56 | ) | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
59 | |||
60 | export { | ||
61 | filesRouter | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | async function getVideoFileMetadata (req: express.Request, res: express.Response) { | ||
67 | const videoFile = await VideoFileModel.loadWithMetadata(toInt(req.params.videoFileId)) | ||
68 | |||
69 | return res.json(videoFile.metadata) | ||
70 | } | ||
71 | |||
72 | // --------------------------------------------------------------------------- | ||
73 | |||
74 | async function removeHLSPlaylistController (req: express.Request, res: express.Response) { | ||
75 | const video = res.locals.videoAll | ||
76 | |||
77 | logger.info('Deleting HLS playlist of %s.', video.url, lTags(video.uuid)) | ||
78 | await removeHLSPlaylist(video) | ||
79 | |||
80 | await federateVideoIfNeeded(video, false, undefined) | ||
81 | |||
82 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
83 | } | ||
84 | |||
85 | async function removeHLSFileController (req: express.Request, res: express.Response) { | ||
86 | const video = res.locals.videoAll | ||
87 | const videoFileId = +req.params.videoFileId | ||
88 | |||
89 | logger.info('Deleting HLS file %d of %s.', videoFileId, video.url, lTags(video.uuid)) | ||
90 | |||
91 | const playlist = await removeHLSFile(video, videoFileId) | ||
92 | if (playlist) await updatePlaylistAfterFileChange(video, playlist) | ||
93 | |||
94 | await federateVideoIfNeeded(video, false, undefined) | ||
95 | |||
96 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
97 | } | ||
98 | |||
99 | // --------------------------------------------------------------------------- | ||
100 | |||
101 | async function removeAllWebVideoFilesController (req: express.Request, res: express.Response) { | ||
102 | const video = res.locals.videoAll | ||
103 | |||
104 | logger.info('Deleting Web Video files of %s.', video.url, lTags(video.uuid)) | ||
105 | |||
106 | await removeAllWebVideoFiles(video) | ||
107 | await federateVideoIfNeeded(video, false, undefined) | ||
108 | |||
109 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
110 | } | ||
111 | |||
112 | async function removeWebVideoFileController (req: express.Request, res: express.Response) { | ||
113 | const video = res.locals.videoAll | ||
114 | |||
115 | const videoFileId = +req.params.videoFileId | ||
116 | logger.info('Deleting Web Video file %d of %s.', videoFileId, video.url, lTags(video.uuid)) | ||
117 | |||
118 | await removeWebVideoFile(video, videoFileId) | ||
119 | await federateVideoIfNeeded(video, false, undefined) | ||
120 | |||
121 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
122 | } | ||
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts deleted file mode 100644 index defe9efd4..000000000 --- a/server/controllers/api/videos/import.ts +++ /dev/null | |||
@@ -1,262 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { move, readFile } from 'fs-extra' | ||
3 | import { decode } from 'magnet-uri' | ||
4 | import parseTorrent, { Instance } from 'parse-torrent' | ||
5 | import { join } from 'path' | ||
6 | import { buildYoutubeDLImport, buildVideoFromImport, insertFromImportIntoDB, YoutubeDlImportError } from '@server/lib/video-pre-import' | ||
7 | import { MThumbnail, MVideoThumbnail } from '@server/types/models' | ||
8 | import { HttpStatusCode, ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState } from '@shared/models' | ||
9 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' | ||
10 | import { isArray } from '../../../helpers/custom-validators/misc' | ||
11 | import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils' | ||
12 | import { logger } from '../../../helpers/logger' | ||
13 | import { getSecureTorrentName } from '../../../helpers/utils' | ||
14 | import { CONFIG } from '../../../initializers/config' | ||
15 | import { MIMETYPES } from '../../../initializers/constants' | ||
16 | import { JobQueue } from '../../../lib/job-queue/job-queue' | ||
17 | import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' | ||
18 | import { | ||
19 | asyncMiddleware, | ||
20 | asyncRetryTransactionMiddleware, | ||
21 | authenticate, | ||
22 | videoImportAddValidator, | ||
23 | videoImportCancelValidator, | ||
24 | videoImportDeleteValidator | ||
25 | } from '../../../middlewares' | ||
26 | |||
27 | const auditLogger = auditLoggerFactory('video-imports') | ||
28 | const videoImportsRouter = express.Router() | ||
29 | |||
30 | const reqVideoFileImport = createReqFiles( | ||
31 | [ 'thumbnailfile', 'previewfile', 'torrentfile' ], | ||
32 | { ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT } | ||
33 | ) | ||
34 | |||
35 | videoImportsRouter.post('/imports', | ||
36 | authenticate, | ||
37 | reqVideoFileImport, | ||
38 | asyncMiddleware(videoImportAddValidator), | ||
39 | asyncRetryTransactionMiddleware(handleVideoImport) | ||
40 | ) | ||
41 | |||
42 | videoImportsRouter.post('/imports/:id/cancel', | ||
43 | authenticate, | ||
44 | asyncMiddleware(videoImportCancelValidator), | ||
45 | asyncRetryTransactionMiddleware(cancelVideoImport) | ||
46 | ) | ||
47 | |||
48 | videoImportsRouter.delete('/imports/:id', | ||
49 | authenticate, | ||
50 | asyncMiddleware(videoImportDeleteValidator), | ||
51 | asyncRetryTransactionMiddleware(deleteVideoImport) | ||
52 | ) | ||
53 | |||
54 | // --------------------------------------------------------------------------- | ||
55 | |||
56 | export { | ||
57 | videoImportsRouter | ||
58 | } | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | async function deleteVideoImport (req: express.Request, res: express.Response) { | ||
63 | const videoImport = res.locals.videoImport | ||
64 | |||
65 | await videoImport.destroy() | ||
66 | |||
67 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
68 | } | ||
69 | |||
70 | async function cancelVideoImport (req: express.Request, res: express.Response) { | ||
71 | const videoImport = res.locals.videoImport | ||
72 | |||
73 | videoImport.state = VideoImportState.CANCELLED | ||
74 | await videoImport.save() | ||
75 | |||
76 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
77 | } | ||
78 | |||
79 | function handleVideoImport (req: express.Request, res: express.Response) { | ||
80 | if (req.body.targetUrl) return handleYoutubeDlImport(req, res) | ||
81 | |||
82 | const file = req.files?.['torrentfile']?.[0] | ||
83 | if (req.body.magnetUri || file) return handleTorrentImport(req, res, file) | ||
84 | } | ||
85 | |||
86 | async function handleTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { | ||
87 | const body: VideoImportCreate = req.body | ||
88 | const user = res.locals.oauth.token.User | ||
89 | |||
90 | let videoName: string | ||
91 | let torrentName: string | ||
92 | let magnetUri: string | ||
93 | |||
94 | if (torrentfile) { | ||
95 | const result = await processTorrentOrAbortRequest(req, res, torrentfile) | ||
96 | if (!result) return | ||
97 | |||
98 | videoName = result.name | ||
99 | torrentName = result.torrentName | ||
100 | } else { | ||
101 | const result = processMagnetURI(body) | ||
102 | magnetUri = result.magnetUri | ||
103 | videoName = result.name | ||
104 | } | ||
105 | |||
106 | const video = await buildVideoFromImport({ | ||
107 | channelId: res.locals.videoChannel.id, | ||
108 | importData: { name: videoName }, | ||
109 | importDataOverride: body, | ||
110 | importType: 'torrent' | ||
111 | }) | ||
112 | |||
113 | const thumbnailModel = await processThumbnail(req, video) | ||
114 | const previewModel = await processPreview(req, video) | ||
115 | |||
116 | const videoImport = await insertFromImportIntoDB({ | ||
117 | video, | ||
118 | thumbnailModel, | ||
119 | previewModel, | ||
120 | videoChannel: res.locals.videoChannel, | ||
121 | tags: body.tags || undefined, | ||
122 | user, | ||
123 | videoPasswords: body.videoPasswords, | ||
124 | videoImportAttributes: { | ||
125 | magnetUri, | ||
126 | torrentName, | ||
127 | state: VideoImportState.PENDING, | ||
128 | userId: user.id | ||
129 | } | ||
130 | }) | ||
131 | |||
132 | const payload: VideoImportPayload = { | ||
133 | type: torrentfile | ||
134 | ? 'torrent-file' | ||
135 | : 'magnet-uri', | ||
136 | videoImportId: videoImport.id, | ||
137 | preventException: false | ||
138 | } | ||
139 | await JobQueue.Instance.createJob({ type: 'video-import', payload }) | ||
140 | |||
141 | auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) | ||
142 | |||
143 | return res.json(videoImport.toFormattedJSON()).end() | ||
144 | } | ||
145 | |||
146 | function statusFromYtDlImportError (err: YoutubeDlImportError): number { | ||
147 | switch (err.code) { | ||
148 | case YoutubeDlImportError.CODE.NOT_ONLY_UNICAST_URL: | ||
149 | return HttpStatusCode.FORBIDDEN_403 | ||
150 | |||
151 | case YoutubeDlImportError.CODE.FETCH_ERROR: | ||
152 | return HttpStatusCode.BAD_REQUEST_400 | ||
153 | |||
154 | default: | ||
155 | return HttpStatusCode.INTERNAL_SERVER_ERROR_500 | ||
156 | } | ||
157 | } | ||
158 | |||
159 | async function handleYoutubeDlImport (req: express.Request, res: express.Response) { | ||
160 | const body: VideoImportCreate = req.body | ||
161 | const targetUrl = body.targetUrl | ||
162 | const user = res.locals.oauth.token.User | ||
163 | |||
164 | try { | ||
165 | const { job, videoImport } = await buildYoutubeDLImport({ | ||
166 | targetUrl, | ||
167 | channel: res.locals.videoChannel, | ||
168 | importDataOverride: body, | ||
169 | thumbnailFilePath: req.files?.['thumbnailfile']?.[0].path, | ||
170 | previewFilePath: req.files?.['previewfile']?.[0].path, | ||
171 | user | ||
172 | }) | ||
173 | await JobQueue.Instance.createJob(job) | ||
174 | |||
175 | auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) | ||
176 | |||
177 | return res.json(videoImport.toFormattedJSON()).end() | ||
178 | } catch (err) { | ||
179 | logger.error('An error occurred while importing the video %s. ', targetUrl, { err }) | ||
180 | |||
181 | return res.fail({ | ||
182 | message: err.message, | ||
183 | status: statusFromYtDlImportError(err), | ||
184 | data: { | ||
185 | targetUrl | ||
186 | } | ||
187 | }) | ||
188 | } | ||
189 | } | ||
190 | |||
191 | async function processThumbnail (req: express.Request, video: MVideoThumbnail) { | ||
192 | const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined | ||
193 | if (thumbnailField) { | ||
194 | const thumbnailPhysicalFile = thumbnailField[0] | ||
195 | |||
196 | return updateLocalVideoMiniatureFromExisting({ | ||
197 | inputPath: thumbnailPhysicalFile.path, | ||
198 | video, | ||
199 | type: ThumbnailType.MINIATURE, | ||
200 | automaticallyGenerated: false | ||
201 | }) | ||
202 | } | ||
203 | |||
204 | return undefined | ||
205 | } | ||
206 | |||
207 | async function processPreview (req: express.Request, video: MVideoThumbnail): Promise<MThumbnail> { | ||
208 | const previewField = req.files ? req.files['previewfile'] : undefined | ||
209 | if (previewField) { | ||
210 | const previewPhysicalFile = previewField[0] | ||
211 | |||
212 | return updateLocalVideoMiniatureFromExisting({ | ||
213 | inputPath: previewPhysicalFile.path, | ||
214 | video, | ||
215 | type: ThumbnailType.PREVIEW, | ||
216 | automaticallyGenerated: false | ||
217 | }) | ||
218 | } | ||
219 | |||
220 | return undefined | ||
221 | } | ||
222 | |||
223 | async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { | ||
224 | const torrentName = torrentfile.originalname | ||
225 | |||
226 | // Rename the torrent to a secured name | ||
227 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) | ||
228 | await move(torrentfile.path, newTorrentPath, { overwrite: true }) | ||
229 | torrentfile.path = newTorrentPath | ||
230 | |||
231 | const buf = await readFile(torrentfile.path) | ||
232 | const parsedTorrent = parseTorrent(buf) as Instance | ||
233 | |||
234 | if (parsedTorrent.files.length !== 1) { | ||
235 | cleanUpReqFiles(req) | ||
236 | |||
237 | res.fail({ | ||
238 | type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT, | ||
239 | message: 'Torrents with only 1 file are supported.' | ||
240 | }) | ||
241 | return undefined | ||
242 | } | ||
243 | |||
244 | return { | ||
245 | name: extractNameFromArray(parsedTorrent.name), | ||
246 | torrentName | ||
247 | } | ||
248 | } | ||
249 | |||
250 | function processMagnetURI (body: VideoImportCreate) { | ||
251 | const magnetUri = body.magnetUri | ||
252 | const parsed = decode(magnetUri) | ||
253 | |||
254 | return { | ||
255 | name: extractNameFromArray(parsed.name), | ||
256 | magnetUri | ||
257 | } | ||
258 | } | ||
259 | |||
260 | function extractNameFromArray (name: string | string[]) { | ||
261 | return isArray(name) ? name[0] : name | ||
262 | } | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts deleted file mode 100644 index 3cdd42289..000000000 --- a/server/controllers/api/videos/index.ts +++ /dev/null | |||
@@ -1,228 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { MVideoAccountLight } from '@server/types/models' | ||
7 | import { HttpStatusCode } from '../../../../shared/models' | ||
8 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
9 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | ||
10 | import { logger } from '../../../helpers/logger' | ||
11 | import { getFormattedObjects } from '../../../helpers/utils' | ||
12 | import { REMOTE_SCHEME, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' | ||
13 | import { sequelizeTypescript } from '../../../initializers/database' | ||
14 | import { JobQueue } from '../../../lib/job-queue' | ||
15 | import { Hooks } from '../../../lib/plugins/hooks' | ||
16 | import { | ||
17 | apiRateLimiter, | ||
18 | asyncMiddleware, | ||
19 | asyncRetryTransactionMiddleware, | ||
20 | authenticate, | ||
21 | checkVideoFollowConstraints, | ||
22 | commonVideosFiltersValidator, | ||
23 | optionalAuthenticate, | ||
24 | paginationValidator, | ||
25 | setDefaultPagination, | ||
26 | setDefaultVideosSort, | ||
27 | videosCustomGetValidator, | ||
28 | videosGetValidator, | ||
29 | videosRemoveValidator, | ||
30 | videosSortValidator | ||
31 | } from '../../../middlewares' | ||
32 | import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' | ||
33 | import { VideoModel } from '../../../models/video/video' | ||
34 | import { blacklistRouter } from './blacklist' | ||
35 | import { videoCaptionsRouter } from './captions' | ||
36 | import { videoCommentRouter } from './comment' | ||
37 | import { filesRouter } from './files' | ||
38 | import { videoImportsRouter } from './import' | ||
39 | import { liveRouter } from './live' | ||
40 | import { ownershipVideoRouter } from './ownership' | ||
41 | import { videoPasswordRouter } from './passwords' | ||
42 | import { rateVideoRouter } from './rate' | ||
43 | import { videoSourceRouter } from './source' | ||
44 | import { statsRouter } from './stats' | ||
45 | import { storyboardRouter } from './storyboard' | ||
46 | import { studioRouter } from './studio' | ||
47 | import { tokenRouter } from './token' | ||
48 | import { transcodingRouter } from './transcoding' | ||
49 | import { updateRouter } from './update' | ||
50 | import { uploadRouter } from './upload' | ||
51 | import { viewRouter } from './view' | ||
52 | |||
53 | const auditLogger = auditLoggerFactory('videos') | ||
54 | const videosRouter = express.Router() | ||
55 | |||
56 | videosRouter.use(apiRateLimiter) | ||
57 | |||
58 | videosRouter.use('/', blacklistRouter) | ||
59 | videosRouter.use('/', statsRouter) | ||
60 | videosRouter.use('/', rateVideoRouter) | ||
61 | videosRouter.use('/', videoCommentRouter) | ||
62 | videosRouter.use('/', studioRouter) | ||
63 | videosRouter.use('/', videoCaptionsRouter) | ||
64 | videosRouter.use('/', videoImportsRouter) | ||
65 | videosRouter.use('/', ownershipVideoRouter) | ||
66 | videosRouter.use('/', viewRouter) | ||
67 | videosRouter.use('/', liveRouter) | ||
68 | videosRouter.use('/', uploadRouter) | ||
69 | videosRouter.use('/', updateRouter) | ||
70 | videosRouter.use('/', filesRouter) | ||
71 | videosRouter.use('/', transcodingRouter) | ||
72 | videosRouter.use('/', tokenRouter) | ||
73 | videosRouter.use('/', videoPasswordRouter) | ||
74 | videosRouter.use('/', storyboardRouter) | ||
75 | videosRouter.use('/', videoSourceRouter) | ||
76 | |||
77 | videosRouter.get('/categories', | ||
78 | openapiOperationDoc({ operationId: 'getCategories' }), | ||
79 | listVideoCategories | ||
80 | ) | ||
81 | videosRouter.get('/licences', | ||
82 | openapiOperationDoc({ operationId: 'getLicences' }), | ||
83 | listVideoLicences | ||
84 | ) | ||
85 | videosRouter.get('/languages', | ||
86 | openapiOperationDoc({ operationId: 'getLanguages' }), | ||
87 | listVideoLanguages | ||
88 | ) | ||
89 | videosRouter.get('/privacies', | ||
90 | openapiOperationDoc({ operationId: 'getPrivacies' }), | ||
91 | listVideoPrivacies | ||
92 | ) | ||
93 | |||
94 | videosRouter.get('/', | ||
95 | openapiOperationDoc({ operationId: 'getVideos' }), | ||
96 | paginationValidator, | ||
97 | videosSortValidator, | ||
98 | setDefaultVideosSort, | ||
99 | setDefaultPagination, | ||
100 | optionalAuthenticate, | ||
101 | commonVideosFiltersValidator, | ||
102 | asyncMiddleware(listVideos) | ||
103 | ) | ||
104 | |||
105 | // TODO: remove, deprecated in 5.0 now we send the complete description in VideoDetails | ||
106 | videosRouter.get('/:id/description', | ||
107 | openapiOperationDoc({ operationId: 'getVideoDesc' }), | ||
108 | asyncMiddleware(videosGetValidator), | ||
109 | asyncMiddleware(getVideoDescription) | ||
110 | ) | ||
111 | |||
112 | videosRouter.get('/:id', | ||
113 | openapiOperationDoc({ operationId: 'getVideo' }), | ||
114 | optionalAuthenticate, | ||
115 | asyncMiddleware(videosCustomGetValidator('for-api')), | ||
116 | asyncMiddleware(checkVideoFollowConstraints), | ||
117 | asyncMiddleware(getVideo) | ||
118 | ) | ||
119 | |||
120 | videosRouter.delete('/:id', | ||
121 | openapiOperationDoc({ operationId: 'delVideo' }), | ||
122 | authenticate, | ||
123 | asyncMiddleware(videosRemoveValidator), | ||
124 | asyncRetryTransactionMiddleware(removeVideo) | ||
125 | ) | ||
126 | |||
127 | // --------------------------------------------------------------------------- | ||
128 | |||
129 | export { | ||
130 | videosRouter | ||
131 | } | ||
132 | |||
133 | // --------------------------------------------------------------------------- | ||
134 | |||
135 | function listVideoCategories (_req: express.Request, res: express.Response) { | ||
136 | res.json(VIDEO_CATEGORIES) | ||
137 | } | ||
138 | |||
139 | function listVideoLicences (_req: express.Request, res: express.Response) { | ||
140 | res.json(VIDEO_LICENCES) | ||
141 | } | ||
142 | |||
143 | function listVideoLanguages (_req: express.Request, res: express.Response) { | ||
144 | res.json(VIDEO_LANGUAGES) | ||
145 | } | ||
146 | |||
147 | function listVideoPrivacies (_req: express.Request, res: express.Response) { | ||
148 | res.json(VIDEO_PRIVACIES) | ||
149 | } | ||
150 | |||
151 | async function getVideo (_req: express.Request, res: express.Response) { | ||
152 | const videoId = res.locals.videoAPI.id | ||
153 | const userId = res.locals.oauth?.token.User.id | ||
154 | |||
155 | const video = await Hooks.wrapObject(res.locals.videoAPI, 'filter:api.video.get.result', { id: videoId, userId }) | ||
156 | |||
157 | if (video.isOutdated()) { | ||
158 | JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) | ||
159 | } | ||
160 | |||
161 | return res.json(video.toFormattedDetailsJSON()) | ||
162 | } | ||
163 | |||
164 | async function getVideoDescription (req: express.Request, res: express.Response) { | ||
165 | const videoInstance = res.locals.videoAll | ||
166 | |||
167 | const description = videoInstance.isOwned() | ||
168 | ? videoInstance.description | ||
169 | : await fetchRemoteVideoDescription(videoInstance) | ||
170 | |||
171 | return res.json({ description }) | ||
172 | } | ||
173 | |||
174 | async function listVideos (req: express.Request, res: express.Response) { | ||
175 | const serverActor = await getServerActor() | ||
176 | |||
177 | const query = pickCommonVideoQuery(req.query) | ||
178 | const countVideos = getCountVideos(req) | ||
179 | |||
180 | const apiOptions = await Hooks.wrapObject({ | ||
181 | ...query, | ||
182 | |||
183 | displayOnlyForFollower: { | ||
184 | actorId: serverActor.id, | ||
185 | orLocalVideos: true | ||
186 | }, | ||
187 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
188 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, | ||
189 | countVideos | ||
190 | }, 'filter:api.videos.list.params') | ||
191 | |||
192 | const resultList = await Hooks.wrapPromiseFun( | ||
193 | VideoModel.listForApi, | ||
194 | apiOptions, | ||
195 | 'filter:api.videos.list.result' | ||
196 | ) | ||
197 | |||
198 | return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) | ||
199 | } | ||
200 | |||
201 | async function removeVideo (req: express.Request, res: express.Response) { | ||
202 | const videoInstance = res.locals.videoAll | ||
203 | |||
204 | await sequelizeTypescript.transaction(async t => { | ||
205 | await videoInstance.destroy({ transaction: t }) | ||
206 | }) | ||
207 | |||
208 | auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON())) | ||
209 | logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid) | ||
210 | |||
211 | Hooks.runAction('action:api.video.deleted', { video: videoInstance, req, res }) | ||
212 | |||
213 | return res.type('json') | ||
214 | .status(HttpStatusCode.NO_CONTENT_204) | ||
215 | .end() | ||
216 | } | ||
217 | |||
218 | // --------------------------------------------------------------------------- | ||
219 | |||
220 | // FIXME: Should not exist, we rely on specific API | ||
221 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { | ||
222 | const host = video.VideoChannel.Account.Actor.Server.host | ||
223 | const path = video.getDescriptionAPIPath() | ||
224 | const url = REMOTE_SCHEME.HTTP + '://' + host + path | ||
225 | |||
226 | const { body } = await doJSONRequest<any>(url) | ||
227 | return body.description || '' | ||
228 | } | ||
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts deleted file mode 100644 index e19e8c652..000000000 --- a/server/controllers/api/videos/live.ts +++ /dev/null | |||
@@ -1,224 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { exists } from '@server/helpers/custom-validators/misc' | ||
3 | import { createReqFiles } from '@server/helpers/express-utils' | ||
4 | import { getFormattedObjects } from '@server/helpers/utils' | ||
5 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' | ||
6 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
8 | import { Hooks } from '@server/lib/plugins/hooks' | ||
9 | import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
10 | import { | ||
11 | videoLiveAddValidator, | ||
12 | videoLiveFindReplaySessionValidator, | ||
13 | videoLiveGetValidator, | ||
14 | videoLiveListSessionsValidator, | ||
15 | videoLiveUpdateValidator | ||
16 | } from '@server/middlewares/validators/videos/video-live' | ||
17 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
18 | import { VideoLiveSessionModel } from '@server/models/video/video-live-session' | ||
19 | import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' | ||
20 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | ||
21 | import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoPrivacy, VideoState } from '@shared/models' | ||
22 | import { logger } from '../../../helpers/logger' | ||
23 | import { sequelizeTypescript } from '../../../initializers/database' | ||
24 | import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' | ||
25 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' | ||
26 | import { VideoModel } from '../../../models/video/video' | ||
27 | import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' | ||
28 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
29 | |||
30 | const liveRouter = express.Router() | ||
31 | |||
32 | const reqVideoFileLive = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) | ||
33 | |||
34 | liveRouter.post('/live', | ||
35 | authenticate, | ||
36 | reqVideoFileLive, | ||
37 | asyncMiddleware(videoLiveAddValidator), | ||
38 | asyncRetryTransactionMiddleware(addLiveVideo) | ||
39 | ) | ||
40 | |||
41 | liveRouter.get('/live/:videoId/sessions', | ||
42 | authenticate, | ||
43 | asyncMiddleware(videoLiveGetValidator), | ||
44 | videoLiveListSessionsValidator, | ||
45 | asyncMiddleware(getLiveVideoSessions) | ||
46 | ) | ||
47 | |||
48 | liveRouter.get('/live/:videoId', | ||
49 | optionalAuthenticate, | ||
50 | asyncMiddleware(videoLiveGetValidator), | ||
51 | getLiveVideo | ||
52 | ) | ||
53 | |||
54 | liveRouter.put('/live/:videoId', | ||
55 | authenticate, | ||
56 | asyncMiddleware(videoLiveGetValidator), | ||
57 | videoLiveUpdateValidator, | ||
58 | asyncRetryTransactionMiddleware(updateLiveVideo) | ||
59 | ) | ||
60 | |||
61 | liveRouter.get('/:videoId/live-session', | ||
62 | asyncMiddleware(videoLiveFindReplaySessionValidator), | ||
63 | getLiveReplaySession | ||
64 | ) | ||
65 | |||
66 | // --------------------------------------------------------------------------- | ||
67 | |||
68 | export { | ||
69 | liveRouter | ||
70 | } | ||
71 | |||
72 | // --------------------------------------------------------------------------- | ||
73 | |||
74 | function getLiveVideo (req: express.Request, res: express.Response) { | ||
75 | const videoLive = res.locals.videoLive | ||
76 | |||
77 | return res.json(videoLive.toFormattedJSON(canSeePrivateLiveInformation(res))) | ||
78 | } | ||
79 | |||
80 | function getLiveReplaySession (req: express.Request, res: express.Response) { | ||
81 | const session = res.locals.videoLiveSession | ||
82 | |||
83 | return res.json(session.toFormattedJSON()) | ||
84 | } | ||
85 | |||
86 | async function getLiveVideoSessions (req: express.Request, res: express.Response) { | ||
87 | const videoLive = res.locals.videoLive | ||
88 | |||
89 | const data = await VideoLiveSessionModel.listSessionsOfLiveForAPI({ videoId: videoLive.videoId }) | ||
90 | |||
91 | return res.json(getFormattedObjects(data, data.length)) | ||
92 | } | ||
93 | |||
94 | function canSeePrivateLiveInformation (res: express.Response) { | ||
95 | const user = res.locals.oauth?.token.User | ||
96 | if (!user) return false | ||
97 | |||
98 | if (user.hasRight(UserRight.GET_ANY_LIVE)) return true | ||
99 | |||
100 | const video = res.locals.videoAll | ||
101 | return video.VideoChannel.Account.userId === user.id | ||
102 | } | ||
103 | |||
104 | async function updateLiveVideo (req: express.Request, res: express.Response) { | ||
105 | const body: LiveVideoUpdate = req.body | ||
106 | |||
107 | const video = res.locals.videoAll | ||
108 | const videoLive = res.locals.videoLive | ||
109 | |||
110 | const newReplaySettingModel = await updateReplaySettings(videoLive, body) | ||
111 | if (newReplaySettingModel) videoLive.replaySettingId = newReplaySettingModel.id | ||
112 | else videoLive.replaySettingId = null | ||
113 | |||
114 | if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive | ||
115 | if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode | ||
116 | |||
117 | video.VideoLive = await videoLive.save() | ||
118 | |||
119 | await federateVideoIfNeeded(video, false) | ||
120 | |||
121 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
122 | } | ||
123 | |||
124 | async function updateReplaySettings (videoLive: MVideoLive, body: LiveVideoUpdate) { | ||
125 | if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay | ||
126 | |||
127 | // The live replay is not saved anymore, destroy the old model if it existed | ||
128 | if (!videoLive.saveReplay) { | ||
129 | if (videoLive.replaySettingId) { | ||
130 | await VideoLiveReplaySettingModel.removeSettings(videoLive.replaySettingId) | ||
131 | } | ||
132 | |||
133 | return undefined | ||
134 | } | ||
135 | |||
136 | const settingModel = videoLive.replaySettingId | ||
137 | ? await VideoLiveReplaySettingModel.load(videoLive.replaySettingId) | ||
138 | : new VideoLiveReplaySettingModel() | ||
139 | |||
140 | if (exists(body.replaySettings.privacy)) settingModel.privacy = body.replaySettings.privacy | ||
141 | |||
142 | return settingModel.save() | ||
143 | } | ||
144 | |||
145 | async function addLiveVideo (req: express.Request, res: express.Response) { | ||
146 | const videoInfo: LiveVideoCreate = req.body | ||
147 | |||
148 | // Prepare data so we don't block the transaction | ||
149 | let videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id) | ||
150 | videoData = await Hooks.wrapObject(videoData, 'filter:api.video.live.video-attribute.result') | ||
151 | |||
152 | videoData.isLive = true | ||
153 | videoData.state = VideoState.WAITING_FOR_LIVE | ||
154 | videoData.duration = 0 | ||
155 | |||
156 | const video = new VideoModel(videoData) as MVideoDetails | ||
157 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | ||
158 | |||
159 | const videoLive = new VideoLiveModel() | ||
160 | videoLive.saveReplay = videoInfo.saveReplay || false | ||
161 | videoLive.permanentLive = videoInfo.permanentLive || false | ||
162 | videoLive.latencyMode = videoInfo.latencyMode || LiveVideoLatencyMode.DEFAULT | ||
163 | videoLive.streamKey = buildUUID() | ||
164 | |||
165 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
166 | video, | ||
167 | files: req.files, | ||
168 | fallback: type => { | ||
169 | return updateLocalVideoMiniatureFromExisting({ | ||
170 | inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, | ||
171 | video, | ||
172 | type, | ||
173 | automaticallyGenerated: true, | ||
174 | keepOriginal: true | ||
175 | }) | ||
176 | } | ||
177 | }) | ||
178 | |||
179 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
180 | const sequelizeOptions = { transaction: t } | ||
181 | |||
182 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | ||
183 | |||
184 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
185 | if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | ||
186 | |||
187 | // Do not forget to add video channel information to the created video | ||
188 | videoCreated.VideoChannel = res.locals.videoChannel | ||
189 | |||
190 | if (videoLive.saveReplay) { | ||
191 | const replaySettings = new VideoLiveReplaySettingModel({ | ||
192 | privacy: videoInfo.replaySettings.privacy | ||
193 | }) | ||
194 | await replaySettings.save(sequelizeOptions) | ||
195 | |||
196 | videoLive.replaySettingId = replaySettings.id | ||
197 | } | ||
198 | |||
199 | videoLive.videoId = videoCreated.id | ||
200 | videoCreated.VideoLive = await videoLive.save(sequelizeOptions) | ||
201 | |||
202 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) | ||
203 | |||
204 | await federateVideoIfNeeded(videoCreated, true, t) | ||
205 | |||
206 | if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
207 | await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) | ||
208 | } | ||
209 | |||
210 | logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) | ||
211 | |||
212 | return { videoCreated } | ||
213 | }) | ||
214 | |||
215 | Hooks.runAction('action:api.live-video.created', { video: videoCreated, req, res }) | ||
216 | |||
217 | return res.json({ | ||
218 | video: { | ||
219 | id: videoCreated.id, | ||
220 | shortUUID: uuidToShort(videoCreated.uuid), | ||
221 | uuid: videoCreated.uuid | ||
222 | } | ||
223 | }) | ||
224 | } | ||
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts deleted file mode 100644 index 88355b289..000000000 --- a/server/controllers/api/videos/ownership.ts +++ /dev/null | |||
@@ -1,138 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { MVideoFullLight } from '@server/types/models' | ||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | import { getFormattedObjects } from '../../../helpers/utils' | ||
7 | import { sequelizeTypescript } from '../../../initializers/database' | ||
8 | import { sendUpdateVideo } from '../../../lib/activitypub/send' | ||
9 | import { changeVideoChannelShare } from '../../../lib/activitypub/share' | ||
10 | import { | ||
11 | asyncMiddleware, | ||
12 | asyncRetryTransactionMiddleware, | ||
13 | authenticate, | ||
14 | paginationValidator, | ||
15 | setDefaultPagination, | ||
16 | videosAcceptChangeOwnershipValidator, | ||
17 | videosChangeOwnershipValidator, | ||
18 | videosTerminateChangeOwnershipValidator | ||
19 | } from '../../../middlewares' | ||
20 | import { VideoModel } from '../../../models/video/video' | ||
21 | import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' | ||
22 | import { VideoChannelModel } from '../../../models/video/video-channel' | ||
23 | |||
24 | const ownershipVideoRouter = express.Router() | ||
25 | |||
26 | ownershipVideoRouter.post('/:videoId/give-ownership', | ||
27 | authenticate, | ||
28 | asyncMiddleware(videosChangeOwnershipValidator), | ||
29 | asyncRetryTransactionMiddleware(giveVideoOwnership) | ||
30 | ) | ||
31 | |||
32 | ownershipVideoRouter.get('/ownership', | ||
33 | authenticate, | ||
34 | paginationValidator, | ||
35 | setDefaultPagination, | ||
36 | asyncRetryTransactionMiddleware(listVideoOwnership) | ||
37 | ) | ||
38 | |||
39 | ownershipVideoRouter.post('/ownership/:id/accept', | ||
40 | authenticate, | ||
41 | asyncMiddleware(videosTerminateChangeOwnershipValidator), | ||
42 | asyncMiddleware(videosAcceptChangeOwnershipValidator), | ||
43 | asyncRetryTransactionMiddleware(acceptOwnership) | ||
44 | ) | ||
45 | |||
46 | ownershipVideoRouter.post('/ownership/:id/refuse', | ||
47 | authenticate, | ||
48 | asyncMiddleware(videosTerminateChangeOwnershipValidator), | ||
49 | asyncRetryTransactionMiddleware(refuseOwnership) | ||
50 | ) | ||
51 | |||
52 | // --------------------------------------------------------------------------- | ||
53 | |||
54 | export { | ||
55 | ownershipVideoRouter | ||
56 | } | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
59 | |||
60 | async function giveVideoOwnership (req: express.Request, res: express.Response) { | ||
61 | const videoInstance = res.locals.videoAll | ||
62 | const initiatorAccountId = res.locals.oauth.token.User.Account.id | ||
63 | const nextOwner = res.locals.nextOwner | ||
64 | |||
65 | await sequelizeTypescript.transaction(t => { | ||
66 | return VideoChangeOwnershipModel.findOrCreate({ | ||
67 | where: { | ||
68 | initiatorAccountId, | ||
69 | nextOwnerAccountId: nextOwner.id, | ||
70 | videoId: videoInstance.id, | ||
71 | status: VideoChangeOwnershipStatus.WAITING | ||
72 | }, | ||
73 | defaults: { | ||
74 | initiatorAccountId, | ||
75 | nextOwnerAccountId: nextOwner.id, | ||
76 | videoId: videoInstance.id, | ||
77 | status: VideoChangeOwnershipStatus.WAITING | ||
78 | }, | ||
79 | transaction: t | ||
80 | }) | ||
81 | }) | ||
82 | |||
83 | logger.info('Ownership change for video %s created.', videoInstance.name) | ||
84 | return res.type('json') | ||
85 | .status(HttpStatusCode.NO_CONTENT_204) | ||
86 | .end() | ||
87 | } | ||
88 | |||
89 | async function listVideoOwnership (req: express.Request, res: express.Response) { | ||
90 | const currentAccountId = res.locals.oauth.token.User.Account.id | ||
91 | |||
92 | const resultList = await VideoChangeOwnershipModel.listForApi( | ||
93 | currentAccountId, | ||
94 | req.query.start || 0, | ||
95 | req.query.count || 10, | ||
96 | req.query.sort || 'createdAt' | ||
97 | ) | ||
98 | |||
99 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
100 | } | ||
101 | |||
102 | function acceptOwnership (req: express.Request, res: express.Response) { | ||
103 | return sequelizeTypescript.transaction(async t => { | ||
104 | const videoChangeOwnership = res.locals.videoChangeOwnership | ||
105 | const channel = res.locals.videoChannel | ||
106 | |||
107 | // We need more attributes for federation | ||
108 | const targetVideo = await VideoModel.loadFull(videoChangeOwnership.Video.id, t) | ||
109 | |||
110 | const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId, t) | ||
111 | |||
112 | targetVideo.channelId = channel.id | ||
113 | |||
114 | const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight | ||
115 | targetVideoUpdated.VideoChannel = channel | ||
116 | |||
117 | if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) { | ||
118 | await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t) | ||
119 | await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) | ||
120 | } | ||
121 | |||
122 | videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED | ||
123 | await videoChangeOwnership.save({ transaction: t }) | ||
124 | |||
125 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
126 | }) | ||
127 | } | ||
128 | |||
129 | function refuseOwnership (req: express.Request, res: express.Response) { | ||
130 | return sequelizeTypescript.transaction(async t => { | ||
131 | const videoChangeOwnership = res.locals.videoChangeOwnership | ||
132 | |||
133 | videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED | ||
134 | await videoChangeOwnership.save({ transaction: t }) | ||
135 | |||
136 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
137 | }) | ||
138 | } | ||
diff --git a/server/controllers/api/videos/passwords.ts b/server/controllers/api/videos/passwords.ts deleted file mode 100644 index d11cf5bcc..000000000 --- a/server/controllers/api/videos/passwords.ts +++ /dev/null | |||
@@ -1,105 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | |||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | ||
4 | import { getFormattedObjects } from '../../../helpers/utils' | ||
5 | import { | ||
6 | asyncMiddleware, | ||
7 | asyncRetryTransactionMiddleware, | ||
8 | authenticate, | ||
9 | setDefaultPagination, | ||
10 | setDefaultSort | ||
11 | } from '../../../middlewares' | ||
12 | import { | ||
13 | listVideoPasswordValidator, | ||
14 | paginationValidator, | ||
15 | removeVideoPasswordValidator, | ||
16 | updateVideoPasswordListValidator, | ||
17 | videoPasswordsSortValidator | ||
18 | } from '../../../middlewares/validators' | ||
19 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
20 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
21 | import { Transaction } from 'sequelize' | ||
22 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
23 | |||
24 | const lTags = loggerTagsFactory('api', 'video') | ||
25 | const videoPasswordRouter = express.Router() | ||
26 | |||
27 | videoPasswordRouter.get('/:videoId/passwords', | ||
28 | authenticate, | ||
29 | paginationValidator, | ||
30 | videoPasswordsSortValidator, | ||
31 | setDefaultSort, | ||
32 | setDefaultPagination, | ||
33 | asyncMiddleware(listVideoPasswordValidator), | ||
34 | asyncMiddleware(listVideoPasswords) | ||
35 | ) | ||
36 | |||
37 | videoPasswordRouter.put('/:videoId/passwords', | ||
38 | authenticate, | ||
39 | asyncMiddleware(updateVideoPasswordListValidator), | ||
40 | asyncMiddleware(updateVideoPasswordList) | ||
41 | ) | ||
42 | |||
43 | videoPasswordRouter.delete('/:videoId/passwords/:passwordId', | ||
44 | authenticate, | ||
45 | asyncMiddleware(removeVideoPasswordValidator), | ||
46 | asyncRetryTransactionMiddleware(removeVideoPassword) | ||
47 | ) | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export { | ||
52 | videoPasswordRouter | ||
53 | } | ||
54 | |||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | async function listVideoPasswords (req: express.Request, res: express.Response) { | ||
58 | const options = { | ||
59 | videoId: res.locals.videoAll.id, | ||
60 | start: req.query.start, | ||
61 | count: req.query.count, | ||
62 | sort: req.query.sort | ||
63 | } | ||
64 | |||
65 | const resultList = await VideoPasswordModel.listPasswords(options) | ||
66 | |||
67 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
68 | } | ||
69 | |||
70 | async function updateVideoPasswordList (req: express.Request, res: express.Response) { | ||
71 | const videoInstance = getVideoWithAttributes(res) | ||
72 | const videoId = videoInstance.id | ||
73 | |||
74 | const passwordArray = req.body.passwords as string[] | ||
75 | |||
76 | await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => { | ||
77 | await VideoPasswordModel.deleteAllPasswords(videoId, t) | ||
78 | await VideoPasswordModel.addPasswords(passwordArray, videoId, t) | ||
79 | }) | ||
80 | |||
81 | logger.info( | ||
82 | `Video passwords for video with name %s and uuid %s have been updated`, | ||
83 | videoInstance.name, | ||
84 | videoInstance.uuid, | ||
85 | lTags(videoInstance.uuid) | ||
86 | ) | ||
87 | |||
88 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
89 | } | ||
90 | |||
91 | async function removeVideoPassword (req: express.Request, res: express.Response) { | ||
92 | const videoInstance = getVideoWithAttributes(res) | ||
93 | const password = res.locals.videoPassword | ||
94 | |||
95 | await VideoPasswordModel.deletePassword(password.id) | ||
96 | logger.info( | ||
97 | 'Password with id %d of video named %s and uuid %s has been deleted.', | ||
98 | password.id, | ||
99 | videoInstance.name, | ||
100 | videoInstance.uuid, | ||
101 | lTags(videoInstance.uuid) | ||
102 | ) | ||
103 | |||
104 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
105 | } | ||
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts deleted file mode 100644 index 6b26a8eee..000000000 --- a/server/controllers/api/videos/rate.ts +++ /dev/null | |||
@@ -1,87 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode, UserVideoRateUpdate } from '@shared/models' | ||
3 | import { logger } from '../../../helpers/logger' | ||
4 | import { VIDEO_RATE_TYPES } from '../../../initializers/constants' | ||
5 | import { sequelizeTypescript } from '../../../initializers/database' | ||
6 | import { getLocalRateUrl, sendVideoRateChange } from '../../../lib/activitypub/video-rates' | ||
7 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares' | ||
8 | import { AccountModel } from '../../../models/account/account' | ||
9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
10 | |||
11 | const rateVideoRouter = express.Router() | ||
12 | |||
13 | rateVideoRouter.put('/:id/rate', | ||
14 | authenticate, | ||
15 | asyncMiddleware(videoUpdateRateValidator), | ||
16 | asyncRetryTransactionMiddleware(rateVideo) | ||
17 | ) | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | export { | ||
22 | rateVideoRouter | ||
23 | } | ||
24 | |||
25 | // --------------------------------------------------------------------------- | ||
26 | |||
27 | async function rateVideo (req: express.Request, res: express.Response) { | ||
28 | const body: UserVideoRateUpdate = req.body | ||
29 | const rateType = body.rating | ||
30 | const videoInstance = res.locals.videoAll | ||
31 | const userAccount = res.locals.oauth.token.User.Account | ||
32 | |||
33 | await sequelizeTypescript.transaction(async t => { | ||
34 | const sequelizeOptions = { transaction: t } | ||
35 | |||
36 | const accountInstance = await AccountModel.load(userAccount.id, t) | ||
37 | const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) | ||
38 | |||
39 | // Same rate, nothing do to | ||
40 | if (rateType === 'none' && !previousRate || previousRate?.type === rateType) return | ||
41 | |||
42 | let likesToIncrement = 0 | ||
43 | let dislikesToIncrement = 0 | ||
44 | |||
45 | if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++ | ||
46 | else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ | ||
47 | |||
48 | // There was a previous rate, update it | ||
49 | if (previousRate) { | ||
50 | // We will remove the previous rate, so we will need to update the video count attribute | ||
51 | if (previousRate.type === 'like') likesToIncrement-- | ||
52 | else if (previousRate.type === 'dislike') dislikesToIncrement-- | ||
53 | |||
54 | if (rateType === 'none') { // Destroy previous rate | ||
55 | await previousRate.destroy(sequelizeOptions) | ||
56 | } else { // Update previous rate | ||
57 | previousRate.type = rateType | ||
58 | previousRate.url = getLocalRateUrl(rateType, userAccount.Actor, videoInstance) | ||
59 | await previousRate.save(sequelizeOptions) | ||
60 | } | ||
61 | } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate | ||
62 | const query = { | ||
63 | accountId: accountInstance.id, | ||
64 | videoId: videoInstance.id, | ||
65 | type: rateType, | ||
66 | url: getLocalRateUrl(rateType, userAccount.Actor, videoInstance) | ||
67 | } | ||
68 | |||
69 | await AccountVideoRateModel.create(query, sequelizeOptions) | ||
70 | } | ||
71 | |||
72 | const incrementQuery = { | ||
73 | likes: likesToIncrement, | ||
74 | dislikes: dislikesToIncrement | ||
75 | } | ||
76 | |||
77 | await videoInstance.increment(incrementQuery, sequelizeOptions) | ||
78 | |||
79 | await sendVideoRateChange(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t) | ||
80 | |||
81 | logger.info('Account video rate for video %s of account %s updated.', videoInstance.name, accountInstance.name) | ||
82 | }) | ||
83 | |||
84 | return res.type('json') | ||
85 | .status(HttpStatusCode.NO_CONTENT_204) | ||
86 | .end() | ||
87 | } | ||
diff --git a/server/controllers/api/videos/source.ts b/server/controllers/api/videos/source.ts deleted file mode 100644 index 75fe68b6c..000000000 --- a/server/controllers/api/videos/source.ts +++ /dev/null | |||
@@ -1,206 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { move } from 'fs-extra' | ||
3 | import { sequelizeTypescript } from '@server/initializers/database' | ||
4 | import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail' | ||
7 | import { uploadx } from '@server/lib/uploadx' | ||
8 | import { buildMoveToObjectStorageJob } from '@server/lib/video' | ||
9 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | ||
10 | import { buildNewFile } from '@server/lib/video-file' | ||
11 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
12 | import { buildNextVideoState } from '@server/lib/video-state' | ||
13 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
14 | import { VideoModel } from '@server/models/video/video' | ||
15 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
16 | import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | ||
17 | import { VideoState } from '@shared/models' | ||
18 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
19 | import { | ||
20 | asyncMiddleware, | ||
21 | authenticate, | ||
22 | replaceVideoSourceResumableInitValidator, | ||
23 | replaceVideoSourceResumableValidator, | ||
24 | videoSourceGetLatestValidator | ||
25 | } from '../../../middlewares' | ||
26 | |||
27 | const lTags = loggerTagsFactory('api', 'video') | ||
28 | |||
29 | const videoSourceRouter = express.Router() | ||
30 | |||
31 | videoSourceRouter.get('/:id/source', | ||
32 | openapiOperationDoc({ operationId: 'getVideoSource' }), | ||
33 | authenticate, | ||
34 | asyncMiddleware(videoSourceGetLatestValidator), | ||
35 | getVideoLatestSource | ||
36 | ) | ||
37 | |||
38 | videoSourceRouter.post('/:id/source/replace-resumable', | ||
39 | authenticate, | ||
40 | asyncMiddleware(replaceVideoSourceResumableInitValidator), | ||
41 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end | ||
42 | ) | ||
43 | |||
44 | videoSourceRouter.delete('/:id/source/replace-resumable', | ||
45 | authenticate, | ||
46 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end | ||
47 | ) | ||
48 | |||
49 | videoSourceRouter.put('/:id/source/replace-resumable', | ||
50 | authenticate, | ||
51 | uploadx.upload, // uploadx doesn't next() before the file upload completes | ||
52 | asyncMiddleware(replaceVideoSourceResumableValidator), | ||
53 | asyncMiddleware(replaceVideoSourceResumable) | ||
54 | ) | ||
55 | |||
56 | // --------------------------------------------------------------------------- | ||
57 | |||
58 | export { | ||
59 | videoSourceRouter | ||
60 | } | ||
61 | |||
62 | // --------------------------------------------------------------------------- | ||
63 | |||
64 | function getVideoLatestSource (req: express.Request, res: express.Response) { | ||
65 | return res.json(res.locals.videoSource.toFormattedJSON()) | ||
66 | } | ||
67 | |||
68 | async function replaceVideoSourceResumable (req: express.Request, res: express.Response) { | ||
69 | const videoPhysicalFile = res.locals.updateVideoFileResumable | ||
70 | const user = res.locals.oauth.token.User | ||
71 | |||
72 | const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' }) | ||
73 | const originalFilename = videoPhysicalFile.originalname | ||
74 | |||
75 | const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(res.locals.videoAll.uuid) | ||
76 | |||
77 | try { | ||
78 | const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(res.locals.videoAll, videoFile) | ||
79 | await move(videoPhysicalFile.path, destination) | ||
80 | |||
81 | let oldWebVideoFiles: MVideoFile[] = [] | ||
82 | let oldStreamingPlaylists: MStreamingPlaylistFiles[] = [] | ||
83 | |||
84 | const inputFileUpdatedAt = new Date() | ||
85 | |||
86 | const video = await sequelizeTypescript.transaction(async transaction => { | ||
87 | const video = await VideoModel.loadFull(res.locals.videoAll.id, transaction) | ||
88 | |||
89 | oldWebVideoFiles = video.VideoFiles | ||
90 | oldStreamingPlaylists = video.VideoStreamingPlaylists | ||
91 | |||
92 | for (const file of video.VideoFiles) { | ||
93 | await file.destroy({ transaction }) | ||
94 | } | ||
95 | for (const playlist of oldStreamingPlaylists) { | ||
96 | await playlist.destroy({ transaction }) | ||
97 | } | ||
98 | |||
99 | videoFile.videoId = video.id | ||
100 | await videoFile.save({ transaction }) | ||
101 | |||
102 | video.VideoFiles = [ videoFile ] | ||
103 | video.VideoStreamingPlaylists = [] | ||
104 | |||
105 | video.state = buildNextVideoState() | ||
106 | video.duration = videoPhysicalFile.duration | ||
107 | video.inputFileUpdatedAt = inputFileUpdatedAt | ||
108 | await video.save({ transaction }) | ||
109 | |||
110 | await autoBlacklistVideoIfNeeded({ | ||
111 | video, | ||
112 | user, | ||
113 | isRemote: false, | ||
114 | isNew: false, | ||
115 | isNewFile: true, | ||
116 | transaction | ||
117 | }) | ||
118 | |||
119 | return video | ||
120 | }) | ||
121 | |||
122 | await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists }) | ||
123 | |||
124 | const source = await VideoSourceModel.create({ | ||
125 | filename: originalFilename, | ||
126 | videoId: video.id, | ||
127 | createdAt: inputFileUpdatedAt | ||
128 | }) | ||
129 | |||
130 | await regenerateMiniaturesIfNeeded(video) | ||
131 | await video.VideoChannel.setAsUpdated() | ||
132 | await addVideoJobsAfterUpload(video, video.getMaxQualityFile()) | ||
133 | |||
134 | logger.info('Replaced video file of video %s with uuid %s.', video.name, video.uuid, lTags(video.uuid)) | ||
135 | |||
136 | Hooks.runAction('action:api.video.file-updated', { video, req, res }) | ||
137 | |||
138 | return res.json(source.toFormattedJSON()) | ||
139 | } finally { | ||
140 | videoFileMutexReleaser() | ||
141 | } | ||
142 | } | ||
143 | |||
144 | async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) { | ||
145 | const jobs: (CreateJobArgument & CreateJobOptions)[] = [ | ||
146 | { | ||
147 | type: 'manage-video-torrent' as 'manage-video-torrent', | ||
148 | payload: { | ||
149 | videoId: video.id, | ||
150 | videoFileId: videoFile.id, | ||
151 | action: 'create' | ||
152 | } | ||
153 | }, | ||
154 | |||
155 | { | ||
156 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
157 | payload: { | ||
158 | videoUUID: video.uuid, | ||
159 | // No need to federate, we process these jobs sequentially | ||
160 | federate: false | ||
161 | } | ||
162 | }, | ||
163 | |||
164 | { | ||
165 | type: 'federate-video' as 'federate-video', | ||
166 | payload: { | ||
167 | videoUUID: video.uuid, | ||
168 | isNewVideo: false | ||
169 | } | ||
170 | } | ||
171 | ] | ||
172 | |||
173 | if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { | ||
174 | jobs.push(await buildMoveToObjectStorageJob({ video, isNewVideo: false, previousVideoState: undefined })) | ||
175 | } | ||
176 | |||
177 | if (video.state === VideoState.TO_TRANSCODE) { | ||
178 | jobs.push({ | ||
179 | type: 'transcoding-job-builder' as 'transcoding-job-builder', | ||
180 | payload: { | ||
181 | videoUUID: video.uuid, | ||
182 | optimizeJob: { | ||
183 | isNewVideo: false | ||
184 | } | ||
185 | } | ||
186 | }) | ||
187 | } | ||
188 | |||
189 | return JobQueue.Instance.createSequentialJobFlow(...jobs) | ||
190 | } | ||
191 | |||
192 | async function removeOldFiles (options: { | ||
193 | video: MVideo | ||
194 | files: MVideoFile[] | ||
195 | playlists: MStreamingPlaylistFiles[] | ||
196 | }) { | ||
197 | const { video, files, playlists } = options | ||
198 | |||
199 | for (const file of files) { | ||
200 | await video.removeWebVideoFile(file) | ||
201 | } | ||
202 | |||
203 | for (const playlist of playlists) { | ||
204 | await video.removeStreamingPlaylistFiles(playlist) | ||
205 | } | ||
206 | } | ||
diff --git a/server/controllers/api/videos/stats.ts b/server/controllers/api/videos/stats.ts deleted file mode 100644 index e79f01888..000000000 --- a/server/controllers/api/videos/stats.ts +++ /dev/null | |||
@@ -1,75 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' | ||
3 | import { VideoStatsOverallQuery, VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models' | ||
4 | import { | ||
5 | asyncMiddleware, | ||
6 | authenticate, | ||
7 | videoOverallStatsValidator, | ||
8 | videoRetentionStatsValidator, | ||
9 | videoTimeserieStatsValidator | ||
10 | } from '../../../middlewares' | ||
11 | |||
12 | const statsRouter = express.Router() | ||
13 | |||
14 | statsRouter.get('/:videoId/stats/overall', | ||
15 | authenticate, | ||
16 | asyncMiddleware(videoOverallStatsValidator), | ||
17 | asyncMiddleware(getOverallStats) | ||
18 | ) | ||
19 | |||
20 | statsRouter.get('/:videoId/stats/timeseries/:metric', | ||
21 | authenticate, | ||
22 | asyncMiddleware(videoTimeserieStatsValidator), | ||
23 | asyncMiddleware(getTimeserieStats) | ||
24 | ) | ||
25 | |||
26 | statsRouter.get('/:videoId/stats/retention', | ||
27 | authenticate, | ||
28 | asyncMiddleware(videoRetentionStatsValidator), | ||
29 | asyncMiddleware(getRetentionStats) | ||
30 | ) | ||
31 | |||
32 | // --------------------------------------------------------------------------- | ||
33 | |||
34 | export { | ||
35 | statsRouter | ||
36 | } | ||
37 | |||
38 | // --------------------------------------------------------------------------- | ||
39 | |||
40 | async function getOverallStats (req: express.Request, res: express.Response) { | ||
41 | const video = res.locals.videoAll | ||
42 | const query = req.query as VideoStatsOverallQuery | ||
43 | |||
44 | const stats = await LocalVideoViewerModel.getOverallStats({ | ||
45 | video, | ||
46 | startDate: query.startDate, | ||
47 | endDate: query.endDate | ||
48 | }) | ||
49 | |||
50 | return res.json(stats) | ||
51 | } | ||
52 | |||
53 | async function getRetentionStats (req: express.Request, res: express.Response) { | ||
54 | const video = res.locals.videoAll | ||
55 | |||
56 | const stats = await LocalVideoViewerModel.getRetentionStats(video) | ||
57 | |||
58 | return res.json(stats) | ||
59 | } | ||
60 | |||
61 | async function getTimeserieStats (req: express.Request, res: express.Response) { | ||
62 | const video = res.locals.videoAll | ||
63 | const metric = req.params.metric as VideoStatsTimeserieMetric | ||
64 | |||
65 | const query = req.query as VideoStatsTimeserieQuery | ||
66 | |||
67 | const stats = await LocalVideoViewerModel.getTimeserieStats({ | ||
68 | video, | ||
69 | metric, | ||
70 | startDate: query.startDate ?? video.createdAt.toISOString(), | ||
71 | endDate: query.endDate ?? new Date().toISOString() | ||
72 | }) | ||
73 | |||
74 | return res.json(stats) | ||
75 | } | ||
diff --git a/server/controllers/api/videos/storyboard.ts b/server/controllers/api/videos/storyboard.ts deleted file mode 100644 index 47a22011d..000000000 --- a/server/controllers/api/videos/storyboard.ts +++ /dev/null | |||
@@ -1,29 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
3 | import { StoryboardModel } from '@server/models/video/storyboard' | ||
4 | import { asyncMiddleware, videosGetValidator } from '../../../middlewares' | ||
5 | |||
6 | const storyboardRouter = express.Router() | ||
7 | |||
8 | storyboardRouter.get('/:id/storyboards', | ||
9 | asyncMiddleware(videosGetValidator), | ||
10 | asyncMiddleware(listStoryboards) | ||
11 | ) | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | storyboardRouter | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | async function listStoryboards (req: express.Request, res: express.Response) { | ||
22 | const video = getVideoWithAttributes(res) | ||
23 | |||
24 | const storyboards = await StoryboardModel.listStoryboardsOf(video) | ||
25 | |||
26 | return res.json({ | ||
27 | storyboards: storyboards.map(s => s.toFormattedJSON()) | ||
28 | }) | ||
29 | } | ||
diff --git a/server/controllers/api/videos/studio.ts b/server/controllers/api/videos/studio.ts deleted file mode 100644 index 7c31dfd2b..000000000 --- a/server/controllers/api/videos/studio.ts +++ /dev/null | |||
@@ -1,143 +0,0 @@ | |||
1 | import Bluebird from 'bluebird' | ||
2 | import express from 'express' | ||
3 | import { move } from 'fs-extra' | ||
4 | import { basename } from 'path' | ||
5 | import { createAnyReqFiles } from '@server/helpers/express-utils' | ||
6 | import { MIMETYPES, VIDEO_FILTERS } from '@server/initializers/constants' | ||
7 | import { buildTaskFileFieldname, createVideoStudioJob, getStudioTaskFilePath, getTaskFileFromReq } from '@server/lib/video-studio' | ||
8 | import { | ||
9 | HttpStatusCode, | ||
10 | VideoState, | ||
11 | VideoStudioCreateEdition, | ||
12 | VideoStudioTask, | ||
13 | VideoStudioTaskCut, | ||
14 | VideoStudioTaskIntro, | ||
15 | VideoStudioTaskOutro, | ||
16 | VideoStudioTaskPayload, | ||
17 | VideoStudioTaskWatermark | ||
18 | } from '@shared/models' | ||
19 | import { asyncMiddleware, authenticate, videoStudioAddEditionValidator } from '../../../middlewares' | ||
20 | |||
21 | const studioRouter = express.Router() | ||
22 | |||
23 | const tasksFiles = createAnyReqFiles( | ||
24 | MIMETYPES.VIDEO.MIMETYPE_EXT, | ||
25 | (req: express.Request, file: Express.Multer.File, cb: (err: Error, result?: boolean) => void) => { | ||
26 | const body = req.body as VideoStudioCreateEdition | ||
27 | |||
28 | // Fetch array element | ||
29 | const matches = file.fieldname.match(/tasks\[(\d+)\]/) | ||
30 | if (!matches) return cb(new Error('Cannot find array element indice for ' + file.fieldname)) | ||
31 | |||
32 | const indice = parseInt(matches[1]) | ||
33 | const task = body.tasks[indice] | ||
34 | |||
35 | if (!task) return cb(new Error('Cannot find array element of indice ' + indice + ' for ' + file.fieldname)) | ||
36 | |||
37 | if ( | ||
38 | [ 'add-intro', 'add-outro', 'add-watermark' ].includes(task.name) && | ||
39 | file.fieldname === buildTaskFileFieldname(indice) | ||
40 | ) { | ||
41 | return cb(null, true) | ||
42 | } | ||
43 | |||
44 | return cb(null, false) | ||
45 | } | ||
46 | ) | ||
47 | |||
48 | studioRouter.post('/:videoId/studio/edit', | ||
49 | authenticate, | ||
50 | tasksFiles, | ||
51 | asyncMiddleware(videoStudioAddEditionValidator), | ||
52 | asyncMiddleware(createEditionTasks) | ||
53 | ) | ||
54 | |||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | export { | ||
58 | studioRouter | ||
59 | } | ||
60 | |||
61 | // --------------------------------------------------------------------------- | ||
62 | |||
63 | async function createEditionTasks (req: express.Request, res: express.Response) { | ||
64 | const files = req.files as Express.Multer.File[] | ||
65 | const body = req.body as VideoStudioCreateEdition | ||
66 | const video = res.locals.videoAll | ||
67 | |||
68 | video.state = VideoState.TO_EDIT | ||
69 | await video.save() | ||
70 | |||
71 | const payload = { | ||
72 | videoUUID: video.uuid, | ||
73 | tasks: await Bluebird.mapSeries(body.tasks, (t, i) => buildTaskPayload(t, i, files)) | ||
74 | } | ||
75 | |||
76 | await createVideoStudioJob({ | ||
77 | user: res.locals.oauth.token.User, | ||
78 | payload, | ||
79 | video | ||
80 | }) | ||
81 | |||
82 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
83 | } | ||
84 | |||
85 | const taskPayloadBuilders: { | ||
86 | [id in VideoStudioTask['name']]: ( | ||
87 | task: VideoStudioTask, | ||
88 | indice?: number, | ||
89 | files?: Express.Multer.File[] | ||
90 | ) => Promise<VideoStudioTaskPayload> | ||
91 | } = { | ||
92 | 'add-intro': buildIntroOutroTask, | ||
93 | 'add-outro': buildIntroOutroTask, | ||
94 | 'cut': buildCutTask, | ||
95 | 'add-watermark': buildWatermarkTask | ||
96 | } | ||
97 | |||
98 | function buildTaskPayload (task: VideoStudioTask, indice: number, files: Express.Multer.File[]): Promise<VideoStudioTaskPayload> { | ||
99 | return taskPayloadBuilders[task.name](task, indice, files) | ||
100 | } | ||
101 | |||
102 | async function buildIntroOutroTask (task: VideoStudioTaskIntro | VideoStudioTaskOutro, indice: number, files: Express.Multer.File[]) { | ||
103 | const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path) | ||
104 | |||
105 | return { | ||
106 | name: task.name, | ||
107 | options: { | ||
108 | file: destination | ||
109 | } | ||
110 | } | ||
111 | } | ||
112 | |||
113 | function buildCutTask (task: VideoStudioTaskCut) { | ||
114 | return Promise.resolve({ | ||
115 | name: task.name, | ||
116 | options: { | ||
117 | start: task.options.start, | ||
118 | end: task.options.end | ||
119 | } | ||
120 | }) | ||
121 | } | ||
122 | |||
123 | async function buildWatermarkTask (task: VideoStudioTaskWatermark, indice: number, files: Express.Multer.File[]) { | ||
124 | const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path) | ||
125 | |||
126 | return { | ||
127 | name: task.name, | ||
128 | options: { | ||
129 | file: destination, | ||
130 | watermarkSizeRatio: VIDEO_FILTERS.WATERMARK.SIZE_RATIO, | ||
131 | horitonzalMarginRatio: VIDEO_FILTERS.WATERMARK.HORIZONTAL_MARGIN_RATIO, | ||
132 | verticalMarginRatio: VIDEO_FILTERS.WATERMARK.VERTICAL_MARGIN_RATIO | ||
133 | } | ||
134 | } | ||
135 | } | ||
136 | |||
137 | async function moveStudioFileToPersistentTMP (file: string) { | ||
138 | const destination = getStudioTaskFilePath(basename(file)) | ||
139 | |||
140 | await move(file, destination) | ||
141 | |||
142 | return destination | ||
143 | } | ||
diff --git a/server/controllers/api/videos/token.ts b/server/controllers/api/videos/token.ts deleted file mode 100644 index e961ffd9e..000000000 --- a/server/controllers/api/videos/token.ts +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' | ||
3 | import { VideoPrivacy, VideoToken } from '@shared/models' | ||
4 | import { asyncMiddleware, optionalAuthenticate, videoFileTokenValidator, videosCustomGetValidator } from '../../../middlewares' | ||
5 | |||
6 | const tokenRouter = express.Router() | ||
7 | |||
8 | tokenRouter.post('/:id/token', | ||
9 | optionalAuthenticate, | ||
10 | asyncMiddleware(videosCustomGetValidator('only-video')), | ||
11 | videoFileTokenValidator, | ||
12 | generateToken | ||
13 | ) | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | tokenRouter | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | function generateToken (req: express.Request, res: express.Response) { | ||
24 | const video = res.locals.onlyVideo | ||
25 | |||
26 | const files = video.privacy === VideoPrivacy.PASSWORD_PROTECTED | ||
27 | ? VideoTokensManager.Instance.createForPasswordProtectedVideo({ videoUUID: video.uuid }) | ||
28 | : VideoTokensManager.Instance.createForAuthUser({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) | ||
29 | |||
30 | return res.json({ | ||
31 | files | ||
32 | } as VideoToken) | ||
33 | } | ||
diff --git a/server/controllers/api/videos/transcoding.ts b/server/controllers/api/videos/transcoding.ts deleted file mode 100644 index c0b93742f..000000000 --- a/server/controllers/api/videos/transcoding.ts +++ /dev/null | |||
@@ -1,60 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
3 | import { Hooks } from '@server/lib/plugins/hooks' | ||
4 | import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job' | ||
5 | import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions' | ||
6 | import { VideoJobInfoModel } from '@server/models/video/video-job-info' | ||
7 | import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' | ||
8 | import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares' | ||
9 | |||
10 | const lTags = loggerTagsFactory('api', 'video') | ||
11 | const transcodingRouter = express.Router() | ||
12 | |||
13 | transcodingRouter.post('/:videoId/transcoding', | ||
14 | authenticate, | ||
15 | ensureUserHasRight(UserRight.RUN_VIDEO_TRANSCODING), | ||
16 | asyncMiddleware(createTranscodingValidator), | ||
17 | asyncMiddleware(createTranscoding) | ||
18 | ) | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | transcodingRouter | ||
24 | } | ||
25 | |||
26 | // --------------------------------------------------------------------------- | ||
27 | |||
28 | async function createTranscoding (req: express.Request, res: express.Response) { | ||
29 | const video = res.locals.videoAll | ||
30 | logger.info('Creating %s transcoding job for %s.', req.body.transcodingType, video.url, lTags()) | ||
31 | |||
32 | const body: VideoTranscodingCreate = req.body | ||
33 | |||
34 | await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingTranscode') | ||
35 | |||
36 | const { resolution: maxResolution, hasAudio } = await video.probeMaxQualityFile() | ||
37 | |||
38 | const resolutions = await Hooks.wrapObject( | ||
39 | computeResolutionsToTranscode({ input: maxResolution, type: 'vod', includeInput: true, strictLower: false, hasAudio }), | ||
40 | 'filter:transcoding.manual.resolutions-to-transcode.result', | ||
41 | body | ||
42 | ) | ||
43 | |||
44 | if (resolutions.length === 0) { | ||
45 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
46 | } | ||
47 | |||
48 | video.state = VideoState.TO_TRANSCODE | ||
49 | await video.save() | ||
50 | |||
51 | await createTranscodingJobs({ | ||
52 | video, | ||
53 | resolutions, | ||
54 | transcodingType: body.transcodingType, | ||
55 | isNewVideo: false, | ||
56 | user: null // Don't specify priority since these transcoding jobs are fired by the admin | ||
57 | }) | ||
58 | |||
59 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
60 | } | ||
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts deleted file mode 100644 index 1edc509dc..000000000 --- a/server/controllers/api/videos/update.ts +++ /dev/null | |||
@@ -1,210 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { Transaction } from 'sequelize/types' | ||
3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | ||
4 | import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
5 | import { setVideoPrivacy } from '@server/lib/video-privacy' | ||
6 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
7 | import { FilteredModelAttributes } from '@server/types' | ||
8 | import { MVideoFullLight } from '@server/types/models' | ||
9 | import { forceNumber } from '@shared/core-utils' | ||
10 | import { HttpStatusCode, VideoPrivacy, VideoUpdate } from '@shared/models' | ||
11 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
12 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | ||
13 | import { createReqFiles } from '../../../helpers/express-utils' | ||
14 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
15 | import { MIMETYPES } from '../../../initializers/constants' | ||
16 | import { sequelizeTypescript } from '../../../initializers/database' | ||
17 | import { Hooks } from '../../../lib/plugins/hooks' | ||
18 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
19 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' | ||
20 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
21 | import { VideoModel } from '../../../models/video/video' | ||
22 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
25 | |||
26 | const lTags = loggerTagsFactory('api', 'video') | ||
27 | const auditLogger = auditLoggerFactory('videos') | ||
28 | const updateRouter = express.Router() | ||
29 | |||
30 | const reqVideoFileUpdate = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) | ||
31 | |||
32 | updateRouter.put('/:id', | ||
33 | openapiOperationDoc({ operationId: 'putVideo' }), | ||
34 | authenticate, | ||
35 | reqVideoFileUpdate, | ||
36 | asyncMiddleware(videosUpdateValidator), | ||
37 | asyncRetryTransactionMiddleware(updateVideo) | ||
38 | ) | ||
39 | |||
40 | // --------------------------------------------------------------------------- | ||
41 | |||
42 | export { | ||
43 | updateRouter | ||
44 | } | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | async function updateVideo (req: express.Request, res: express.Response) { | ||
49 | const videoFromReq = res.locals.videoAll | ||
50 | const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON()) | ||
51 | const videoInfoToUpdate: VideoUpdate = req.body | ||
52 | |||
53 | const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation() | ||
54 | const oldPrivacy = videoFromReq.privacy | ||
55 | |||
56 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
57 | video: videoFromReq, | ||
58 | files: req.files, | ||
59 | fallback: () => Promise.resolve(undefined), | ||
60 | automaticallyGenerated: false | ||
61 | }) | ||
62 | |||
63 | const videoFileLockReleaser = await VideoPathManager.Instance.lockFiles(videoFromReq.uuid) | ||
64 | |||
65 | try { | ||
66 | const { videoInstanceUpdated, isNewVideo } = await sequelizeTypescript.transaction(async t => { | ||
67 | // Refresh video since thumbnails to prevent concurrent updates | ||
68 | const video = await VideoModel.loadFull(videoFromReq.id, t) | ||
69 | |||
70 | const oldVideoChannel = video.VideoChannel | ||
71 | |||
72 | const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [ | ||
73 | 'name', | ||
74 | 'category', | ||
75 | 'licence', | ||
76 | 'language', | ||
77 | 'nsfw', | ||
78 | 'waitTranscoding', | ||
79 | 'support', | ||
80 | 'description', | ||
81 | 'commentsEnabled', | ||
82 | 'downloadEnabled' | ||
83 | ] | ||
84 | |||
85 | for (const key of keysToUpdate) { | ||
86 | if (videoInfoToUpdate[key] !== undefined) video.set(key, videoInfoToUpdate[key]) | ||
87 | } | ||
88 | |||
89 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { | ||
90 | video.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) | ||
91 | } | ||
92 | |||
93 | // Privacy update? | ||
94 | let isNewVideo = false | ||
95 | if (videoInfoToUpdate.privacy !== undefined) { | ||
96 | isNewVideo = await updateVideoPrivacy({ videoInstance: video, videoInfoToUpdate, hadPrivacyForFederation, transaction: t }) | ||
97 | } | ||
98 | |||
99 | // Force updatedAt attribute change | ||
100 | if (!video.changed()) { | ||
101 | await video.setAsRefreshed(t) | ||
102 | } | ||
103 | |||
104 | const videoInstanceUpdated = await video.save({ transaction: t }) as MVideoFullLight | ||
105 | |||
106 | // Thumbnail & preview updates? | ||
107 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
108 | if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) | ||
109 | |||
110 | // Video tags update? | ||
111 | if (videoInfoToUpdate.tags !== undefined) { | ||
112 | await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t }) | ||
113 | } | ||
114 | |||
115 | // Video channel update? | ||
116 | if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { | ||
117 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | ||
118 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | ||
119 | |||
120 | if (hadPrivacyForFederation === true) { | ||
121 | await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | ||
122 | } | ||
123 | } | ||
124 | |||
125 | // Schedule an update in the future? | ||
126 | await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t) | ||
127 | |||
128 | await autoBlacklistVideoIfNeeded({ | ||
129 | video: videoInstanceUpdated, | ||
130 | user: res.locals.oauth.token.User, | ||
131 | isRemote: false, | ||
132 | isNew: false, | ||
133 | isNewFile: false, | ||
134 | transaction: t | ||
135 | }) | ||
136 | |||
137 | auditLogger.update( | ||
138 | getAuditIdFromRes(res), | ||
139 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | ||
140 | oldVideoAuditView | ||
141 | ) | ||
142 | logger.info('Video with name %s and uuid %s updated.', video.name, video.uuid, lTags(video.uuid)) | ||
143 | |||
144 | return { videoInstanceUpdated, isNewVideo } | ||
145 | }) | ||
146 | |||
147 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res }) | ||
148 | |||
149 | await addVideoJobsAfterUpdate({ | ||
150 | video: videoInstanceUpdated, | ||
151 | nameChanged: !!videoInfoToUpdate.name, | ||
152 | oldPrivacy, | ||
153 | isNewVideo | ||
154 | }) | ||
155 | } catch (err) { | ||
156 | // If the transaction is retried, sequelize will think the object has not changed | ||
157 | // So we need to restore the previous fields | ||
158 | await resetSequelizeInstance(videoFromReq) | ||
159 | |||
160 | throw err | ||
161 | } finally { | ||
162 | videoFileLockReleaser() | ||
163 | } | ||
164 | |||
165 | return res.type('json') | ||
166 | .status(HttpStatusCode.NO_CONTENT_204) | ||
167 | .end() | ||
168 | } | ||
169 | |||
170 | async function updateVideoPrivacy (options: { | ||
171 | videoInstance: MVideoFullLight | ||
172 | videoInfoToUpdate: VideoUpdate | ||
173 | hadPrivacyForFederation: boolean | ||
174 | transaction: Transaction | ||
175 | }) { | ||
176 | const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options | ||
177 | const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) | ||
178 | |||
179 | const newPrivacy = forceNumber(videoInfoToUpdate.privacy) | ||
180 | setVideoPrivacy(videoInstance, newPrivacy) | ||
181 | |||
182 | // Delete passwords if video is not anymore password protected | ||
183 | if (videoInstance.privacy === VideoPrivacy.PASSWORD_PROTECTED && newPrivacy !== VideoPrivacy.PASSWORD_PROTECTED) { | ||
184 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
185 | } | ||
186 | |||
187 | if (newPrivacy === VideoPrivacy.PASSWORD_PROTECTED && exists(videoInfoToUpdate.videoPasswords)) { | ||
188 | await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) | ||
189 | await VideoPasswordModel.addPasswords(videoInfoToUpdate.videoPasswords, videoInstance.id, transaction) | ||
190 | } | ||
191 | |||
192 | // Unfederate the video if the new privacy is not compatible with federation | ||
193 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | ||
194 | await VideoModel.sendDelete(videoInstance, { transaction }) | ||
195 | } | ||
196 | |||
197 | return isNewVideo | ||
198 | } | ||
199 | |||
200 | function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) { | ||
201 | if (videoInfoToUpdate.scheduleUpdate) { | ||
202 | return ScheduleVideoUpdateModel.upsert({ | ||
203 | videoId: videoInstance.id, | ||
204 | updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt), | ||
205 | privacy: videoInfoToUpdate.scheduleUpdate.privacy || null | ||
206 | }, { transaction }) | ||
207 | } else if (videoInfoToUpdate.scheduleUpdate === null) { | ||
208 | return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) | ||
209 | } | ||
210 | } | ||
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts deleted file mode 100644 index e520bf4b5..000000000 --- a/server/controllers/api/videos/upload.ts +++ /dev/null | |||
@@ -1,287 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { move } from 'fs-extra' | ||
3 | import { basename } from 'path' | ||
4 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
6 | import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue' | ||
7 | import { Redis } from '@server/lib/redis' | ||
8 | import { uploadx } from '@server/lib/uploadx' | ||
9 | import { buildLocalVideoFromReq, buildMoveToObjectStorageJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
10 | import { buildNewFile } from '@server/lib/video-file' | ||
11 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
12 | import { buildNextVideoState } from '@server/lib/video-state' | ||
13 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
14 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
15 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
16 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | ||
17 | import { uuidToShort } from '@shared/extra-utils' | ||
18 | import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' | ||
19 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
20 | import { createReqFiles } from '../../../helpers/express-utils' | ||
21 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
22 | import { MIMETYPES } from '../../../initializers/constants' | ||
23 | import { sequelizeTypescript } from '../../../initializers/database' | ||
24 | import { Hooks } from '../../../lib/plugins/hooks' | ||
25 | import { generateLocalVideoMiniature } from '../../../lib/thumbnail' | ||
26 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
27 | import { | ||
28 | asyncMiddleware, | ||
29 | asyncRetryTransactionMiddleware, | ||
30 | authenticate, | ||
31 | videosAddLegacyValidator, | ||
32 | videosAddResumableInitValidator, | ||
33 | videosAddResumableValidator | ||
34 | } from '../../../middlewares' | ||
35 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
36 | import { VideoModel } from '../../../models/video/video' | ||
37 | |||
38 | const lTags = loggerTagsFactory('api', 'video') | ||
39 | const auditLogger = auditLoggerFactory('videos') | ||
40 | const uploadRouter = express.Router() | ||
41 | |||
42 | const reqVideoFileAdd = createReqFiles( | ||
43 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | ||
44 | { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT } | ||
45 | ) | ||
46 | |||
47 | const reqVideoFileAddResumable = createReqFiles( | ||
48 | [ 'thumbnailfile', 'previewfile' ], | ||
49 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
50 | getResumableUploadPath() | ||
51 | ) | ||
52 | |||
53 | uploadRouter.post('/upload', | ||
54 | openapiOperationDoc({ operationId: 'uploadLegacy' }), | ||
55 | authenticate, | ||
56 | reqVideoFileAdd, | ||
57 | asyncMiddleware(videosAddLegacyValidator), | ||
58 | asyncRetryTransactionMiddleware(addVideoLegacy) | ||
59 | ) | ||
60 | |||
61 | uploadRouter.post('/upload-resumable', | ||
62 | openapiOperationDoc({ operationId: 'uploadResumableInit' }), | ||
63 | authenticate, | ||
64 | reqVideoFileAddResumable, | ||
65 | asyncMiddleware(videosAddResumableInitValidator), | ||
66 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end | ||
67 | ) | ||
68 | |||
69 | uploadRouter.delete('/upload-resumable', | ||
70 | authenticate, | ||
71 | asyncMiddleware(deleteUploadResumableCache), | ||
72 | (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end | ||
73 | ) | ||
74 | |||
75 | uploadRouter.put('/upload-resumable', | ||
76 | openapiOperationDoc({ operationId: 'uploadResumable' }), | ||
77 | authenticate, | ||
78 | uploadx.upload, // uploadx doesn't next() before the file upload completes | ||
79 | asyncMiddleware(videosAddResumableValidator), | ||
80 | asyncMiddleware(addVideoResumable) | ||
81 | ) | ||
82 | |||
83 | // --------------------------------------------------------------------------- | ||
84 | |||
85 | export { | ||
86 | uploadRouter | ||
87 | } | ||
88 | |||
89 | // --------------------------------------------------------------------------- | ||
90 | |||
91 | async function addVideoLegacy (req: express.Request, res: express.Response) { | ||
92 | // Uploading the video could be long | ||
93 | // Set timeout to 10 minutes, as Express's default is 2 minutes | ||
94 | req.setTimeout(1000 * 60 * 10, () => { | ||
95 | logger.error('Video upload has timed out.') | ||
96 | return res.fail({ | ||
97 | status: HttpStatusCode.REQUEST_TIMEOUT_408, | ||
98 | message: 'Video upload has timed out.' | ||
99 | }) | ||
100 | }) | ||
101 | |||
102 | const videoPhysicalFile = req.files['videofile'][0] | ||
103 | const videoInfo: VideoCreate = req.body | ||
104 | const files = req.files | ||
105 | |||
106 | const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) | ||
107 | |||
108 | return res.json(response) | ||
109 | } | ||
110 | |||
111 | async function addVideoResumable (req: express.Request, res: express.Response) { | ||
112 | const videoPhysicalFile = res.locals.uploadVideoFileResumable | ||
113 | const videoInfo = videoPhysicalFile.metadata | ||
114 | const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile } | ||
115 | |||
116 | const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) | ||
117 | await Redis.Instance.setUploadSession(req.query.upload_id, response) | ||
118 | |||
119 | return res.json(response) | ||
120 | } | ||
121 | |||
122 | async function addVideo (options: { | ||
123 | req: express.Request | ||
124 | res: express.Response | ||
125 | videoPhysicalFile: express.VideoUploadFile | ||
126 | videoInfo: VideoCreate | ||
127 | files: express.UploadFiles | ||
128 | }) { | ||
129 | const { req, res, videoPhysicalFile, videoInfo, files } = options | ||
130 | const videoChannel = res.locals.videoChannel | ||
131 | const user = res.locals.oauth.token.User | ||
132 | |||
133 | let videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) | ||
134 | videoData = await Hooks.wrapObject(videoData, 'filter:api.video.upload.video-attribute.result') | ||
135 | |||
136 | videoData.state = buildNextVideoState() | ||
137 | videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware | ||
138 | |||
139 | const video = new VideoModel(videoData) as MVideoFullLight | ||
140 | video.VideoChannel = videoChannel | ||
141 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | ||
142 | |||
143 | const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' }) | ||
144 | const originalFilename = videoPhysicalFile.originalname | ||
145 | |||
146 | // Move physical file | ||
147 | const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) | ||
148 | await move(videoPhysicalFile.path, destination) | ||
149 | // This is important in case if there is another attempt in the retry process | ||
150 | videoPhysicalFile.filename = basename(destination) | ||
151 | videoPhysicalFile.path = destination | ||
152 | |||
153 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
154 | video, | ||
155 | files, | ||
156 | fallback: type => generateLocalVideoMiniature({ video, videoFile, type }) | ||
157 | }) | ||
158 | |||
159 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
160 | const sequelizeOptions = { transaction: t } | ||
161 | |||
162 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | ||
163 | |||
164 | await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
165 | await videoCreated.addAndSaveThumbnail(previewModel, t) | ||
166 | |||
167 | // Do not forget to add video channel information to the created video | ||
168 | videoCreated.VideoChannel = res.locals.videoChannel | ||
169 | |||
170 | videoFile.videoId = video.id | ||
171 | await videoFile.save(sequelizeOptions) | ||
172 | |||
173 | video.VideoFiles = [ videoFile ] | ||
174 | |||
175 | await VideoSourceModel.create({ | ||
176 | filename: originalFilename, | ||
177 | videoId: video.id | ||
178 | }, { transaction: t }) | ||
179 | |||
180 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) | ||
181 | |||
182 | // Schedule an update in the future? | ||
183 | if (videoInfo.scheduleUpdate) { | ||
184 | await ScheduleVideoUpdateModel.create({ | ||
185 | videoId: video.id, | ||
186 | updateAt: new Date(videoInfo.scheduleUpdate.updateAt), | ||
187 | privacy: videoInfo.scheduleUpdate.privacy || null | ||
188 | }, sequelizeOptions) | ||
189 | } | ||
190 | |||
191 | await autoBlacklistVideoIfNeeded({ | ||
192 | video, | ||
193 | user, | ||
194 | isRemote: false, | ||
195 | isNew: true, | ||
196 | isNewFile: true, | ||
197 | transaction: t | ||
198 | }) | ||
199 | |||
200 | if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
201 | await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) | ||
202 | } | ||
203 | |||
204 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | ||
205 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) | ||
206 | |||
207 | return { videoCreated } | ||
208 | }) | ||
209 | |||
210 | // Channel has a new content, set as updated | ||
211 | await videoCreated.VideoChannel.setAsUpdated() | ||
212 | |||
213 | addVideoJobsAfterUpload(videoCreated, videoFile) | ||
214 | .catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })) | ||
215 | |||
216 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res }) | ||
217 | |||
218 | return { | ||
219 | video: { | ||
220 | id: videoCreated.id, | ||
221 | shortUUID: uuidToShort(videoCreated.uuid), | ||
222 | uuid: videoCreated.uuid | ||
223 | } | ||
224 | } | ||
225 | } | ||
226 | |||
227 | async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) { | ||
228 | const jobs: (CreateJobArgument & CreateJobOptions)[] = [ | ||
229 | { | ||
230 | type: 'manage-video-torrent' as 'manage-video-torrent', | ||
231 | payload: { | ||
232 | videoId: video.id, | ||
233 | videoFileId: videoFile.id, | ||
234 | action: 'create' | ||
235 | } | ||
236 | }, | ||
237 | |||
238 | { | ||
239 | type: 'generate-video-storyboard' as 'generate-video-storyboard', | ||
240 | payload: { | ||
241 | videoUUID: video.uuid, | ||
242 | // No need to federate, we process these jobs sequentially | ||
243 | federate: false | ||
244 | } | ||
245 | }, | ||
246 | |||
247 | { | ||
248 | type: 'notify', | ||
249 | payload: { | ||
250 | action: 'new-video', | ||
251 | videoUUID: video.uuid | ||
252 | } | ||
253 | }, | ||
254 | |||
255 | { | ||
256 | type: 'federate-video' as 'federate-video', | ||
257 | payload: { | ||
258 | videoUUID: video.uuid, | ||
259 | isNewVideo: true | ||
260 | } | ||
261 | } | ||
262 | ] | ||
263 | |||
264 | if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { | ||
265 | jobs.push(await buildMoveToObjectStorageJob({ video, previousVideoState: undefined })) | ||
266 | } | ||
267 | |||
268 | if (video.state === VideoState.TO_TRANSCODE) { | ||
269 | jobs.push({ | ||
270 | type: 'transcoding-job-builder' as 'transcoding-job-builder', | ||
271 | payload: { | ||
272 | videoUUID: video.uuid, | ||
273 | optimizeJob: { | ||
274 | isNewVideo: true | ||
275 | } | ||
276 | } | ||
277 | }) | ||
278 | } | ||
279 | |||
280 | return JobQueue.Instance.createSequentialJobFlow(...jobs) | ||
281 | } | ||
282 | |||
283 | async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
284 | await Redis.Instance.deleteUploadSession(req.query.upload_id) | ||
285 | |||
286 | return next() | ||
287 | } | ||
diff --git a/server/controllers/api/videos/view.ts b/server/controllers/api/videos/view.ts deleted file mode 100644 index a747fa334..000000000 --- a/server/controllers/api/videos/view.ts +++ /dev/null | |||
@@ -1,60 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { Hooks } from '@server/lib/plugins/hooks' | ||
3 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
4 | import { MVideoId } from '@server/types/models' | ||
5 | import { HttpStatusCode, VideoView } from '@shared/models' | ||
6 | import { asyncMiddleware, methodsValidator, openapiOperationDoc, optionalAuthenticate, videoViewValidator } from '../../../middlewares' | ||
7 | import { UserVideoHistoryModel } from '../../../models/user/user-video-history' | ||
8 | |||
9 | const viewRouter = express.Router() | ||
10 | |||
11 | viewRouter.all( | ||
12 | [ '/:videoId/views', '/:videoId/watching' ], | ||
13 | openapiOperationDoc({ operationId: 'addView' }), | ||
14 | methodsValidator([ 'PUT', 'POST' ]), | ||
15 | optionalAuthenticate, | ||
16 | asyncMiddleware(videoViewValidator), | ||
17 | asyncMiddleware(viewVideo) | ||
18 | ) | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | viewRouter | ||
24 | } | ||
25 | |||
26 | // --------------------------------------------------------------------------- | ||
27 | |||
28 | async function viewVideo (req: express.Request, res: express.Response) { | ||
29 | const video = res.locals.onlyImmutableVideo | ||
30 | |||
31 | const body = req.body as VideoView | ||
32 | |||
33 | const ip = req.ip | ||
34 | const { successView } = await VideoViewsManager.Instance.processLocalView({ | ||
35 | video, | ||
36 | ip, | ||
37 | currentTime: body.currentTime, | ||
38 | viewEvent: body.viewEvent | ||
39 | }) | ||
40 | |||
41 | if (successView) { | ||
42 | Hooks.runAction('action:api.video.viewed', { video, ip, req, res }) | ||
43 | } | ||
44 | |||
45 | await updateUserHistoryIfNeeded(body, video, res) | ||
46 | |||
47 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
48 | } | ||
49 | |||
50 | async function updateUserHistoryIfNeeded (body: VideoView, video: MVideoId, res: express.Response) { | ||
51 | const user = res.locals.oauth?.token.User | ||
52 | if (!user) return | ||
53 | if (user.videosHistoryEnabled !== true) return | ||
54 | |||
55 | await UserVideoHistoryModel.upsert({ | ||
56 | videoId: video.id, | ||
57 | userId: user.id, | ||
58 | currentTime: body.currentTime | ||
59 | }) | ||
60 | } | ||