aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers/api/videos
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/controllers/api/videos
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-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')
-rw-r--r--server/controllers/api/videos/blacklist.ts112
-rw-r--r--server/controllers/api/videos/captions.ts93
-rw-r--r--server/controllers/api/videos/comment.ts234
-rw-r--r--server/controllers/api/videos/files.ts122
-rw-r--r--server/controllers/api/videos/import.ts262
-rw-r--r--server/controllers/api/videos/index.ts228
-rw-r--r--server/controllers/api/videos/live.ts224
-rw-r--r--server/controllers/api/videos/ownership.ts138
-rw-r--r--server/controllers/api/videos/passwords.ts105
-rw-r--r--server/controllers/api/videos/rate.ts87
-rw-r--r--server/controllers/api/videos/source.ts206
-rw-r--r--server/controllers/api/videos/stats.ts75
-rw-r--r--server/controllers/api/videos/storyboard.ts29
-rw-r--r--server/controllers/api/videos/studio.ts143
-rw-r--r--server/controllers/api/videos/token.ts33
-rw-r--r--server/controllers/api/videos/transcoding.ts60
-rw-r--r--server/controllers/api/videos/update.ts210
-rw-r--r--server/controllers/api/videos/upload.ts287
-rw-r--r--server/controllers/api/videos/view.ts60
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 @@
1import express from 'express'
2import { blacklistVideo, unblacklistVideo } from '@server/lib/video-blacklist'
3import { HttpStatusCode, UserRight, VideoBlacklistCreate } from '@shared/models'
4import { logger } from '../../../helpers/logger'
5import { getFormattedObjects } from '../../../helpers/utils'
6import { sequelizeTypescript } from '../../../initializers/database'
7import {
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'
21import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
22
23const blacklistRouter = express.Router()
24
25blacklistRouter.post('/:videoId/blacklist',
26 openapiOperationDoc({ operationId: 'addVideoBlock' }),
27 authenticate,
28 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
29 asyncMiddleware(videosBlacklistAddValidator),
30 asyncMiddleware(addVideoToBlacklistController)
31)
32
33blacklistRouter.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
45blacklistRouter.put('/:videoId/blacklist',
46 authenticate,
47 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
48 asyncMiddleware(videosBlacklistUpdateValidator),
49 asyncMiddleware(updateVideoBlacklistController)
50)
51
52blacklistRouter.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
62export {
63 blacklistRouter
64}
65
66// ---------------------------------------------------------------------------
67
68async 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
79async 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
91async 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
103async 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 @@
1import express from 'express'
2import { Hooks } from '@server/lib/plugins/hooks'
3import { MVideoCaption } from '@server/types/models'
4import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
5import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
6import { createReqFiles } from '../../../helpers/express-utils'
7import { logger } from '../../../helpers/logger'
8import { getFormattedObjects } from '../../../helpers/utils'
9import { MIMETYPES } from '../../../initializers/constants'
10import { sequelizeTypescript } from '../../../initializers/database'
11import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
12import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
13import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
14import { VideoCaptionModel } from '../../../models/video/video-caption'
15
16const reqVideoCaptionAdd = createReqFiles([ 'captionfile' ], MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
17
18const videoCaptionsRouter = express.Router()
19
20videoCaptionsRouter.get('/:videoId/captions',
21 asyncMiddleware(listVideoCaptionsValidator),
22 asyncMiddleware(listVideoCaptions)
23)
24videoCaptionsRouter.put('/:videoId/captions/:captionLanguage',
25 authenticate,
26 reqVideoCaptionAdd,
27 asyncMiddleware(addVideoCaptionValidator),
28 asyncRetryTransactionMiddleware(addVideoCaption)
29)
30videoCaptionsRouter.delete('/:videoId/captions/:captionLanguage',
31 authenticate,
32 asyncMiddleware(deleteVideoCaptionValidator),
33 asyncRetryTransactionMiddleware(deleteVideoCaption)
34)
35
36// ---------------------------------------------------------------------------
37
38export {
39 videoCaptionsRouter
40}
41
42// ---------------------------------------------------------------------------
43
44async 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
50async 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
77async 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 @@
1import { MCommentFormattable } from '@server/types/models'
2import express from 'express'
3
4import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models'
5import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
6import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model'
7import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
8import { getFormattedObjects } from '../../../helpers/utils'
9import { sequelizeTypescript } from '../../../initializers/database'
10import { Notifier } from '../../../lib/notifier'
11import { Hooks } from '../../../lib/plugins/hooks'
12import { buildFormattedCommentTree, createVideoComment, removeComment } from '../../../lib/video-comment'
13import {
14 asyncMiddleware,
15 asyncRetryTransactionMiddleware,
16 authenticate,
17 ensureUserHasRight,
18 optionalAuthenticate,
19 paginationValidator,
20 setDefaultPagination,
21 setDefaultSort
22} from '../../../middlewares'
23import {
24 addVideoCommentReplyValidator,
25 addVideoCommentThreadValidator,
26 listVideoCommentsValidator,
27 listVideoCommentThreadsValidator,
28 listVideoThreadCommentsValidator,
29 removeVideoCommentValidator,
30 videoCommentsValidator,
31 videoCommentThreadsSortValidator
32} from '../../../middlewares/validators'
33import { AccountModel } from '../../../models/account/account'
34import { VideoCommentModel } from '../../../models/video/video-comment'
35
36const auditLogger = auditLoggerFactory('comments')
37const videoCommentRouter = express.Router()
38
39videoCommentRouter.get('/:videoId/comment-threads',
40 paginationValidator,
41 videoCommentThreadsSortValidator,
42 setDefaultSort,
43 setDefaultPagination,
44 asyncMiddleware(listVideoCommentThreadsValidator),
45 optionalAuthenticate,
46 asyncMiddleware(listVideoThreads)
47)
48videoCommentRouter.get('/:videoId/comment-threads/:threadId',
49 asyncMiddleware(listVideoThreadCommentsValidator),
50 optionalAuthenticate,
51 asyncMiddleware(listVideoThreadComments)
52)
53
54videoCommentRouter.post('/:videoId/comment-threads',
55 authenticate,
56 asyncMiddleware(addVideoCommentThreadValidator),
57 asyncRetryTransactionMiddleware(addVideoCommentThread)
58)
59videoCommentRouter.post('/:videoId/comments/:commentId',
60 authenticate,
61 asyncMiddleware(addVideoCommentReplyValidator),
62 asyncRetryTransactionMiddleware(addVideoCommentReply)
63)
64videoCommentRouter.delete('/:videoId/comments/:commentId',
65 authenticate,
66 asyncMiddleware(removeVideoCommentValidator),
67 asyncRetryTransactionMiddleware(removeVideoComment)
68)
69
70videoCommentRouter.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
83export {
84 videoCommentRouter
85}
86
87// ---------------------------------------------------------------------------
88
89async 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
110async 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
145async 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
180async 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
202async 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
224async 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 @@
1import express from 'express'
2import toInt from 'validator/lib/toInt'
3import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
5import { updatePlaylistAfterFileChange } from '@server/lib/hls'
6import { removeAllWebVideoFiles, removeHLSFile, removeHLSPlaylist, removeWebVideoFile } from '@server/lib/video-file'
7import { VideoFileModel } from '@server/models/video/video-file'
8import { HttpStatusCode, UserRight } from '@shared/models'
9import {
10 asyncMiddleware,
11 authenticate,
12 ensureUserHasRight,
13 videoFileMetadataGetValidator,
14 videoFilesDeleteHLSFileValidator,
15 videoFilesDeleteHLSValidator,
16 videoFilesDeleteWebVideoFileValidator,
17 videoFilesDeleteWebVideoValidator,
18 videosGetValidator
19} from '../../../middlewares'
20
21const lTags = loggerTagsFactory('api', 'video')
22const filesRouter = express.Router()
23
24filesRouter.get('/:id/metadata/:videoFileId',
25 asyncMiddleware(videosGetValidator),
26 asyncMiddleware(videoFileMetadataGetValidator),
27 asyncMiddleware(getVideoFileMetadata)
28)
29
30filesRouter.delete('/:id/hls',
31 authenticate,
32 ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
33 asyncMiddleware(videoFilesDeleteHLSValidator),
34 asyncMiddleware(removeHLSPlaylistController)
35)
36filesRouter.delete('/:id/hls/:videoFileId',
37 authenticate,
38 ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES),
39 asyncMiddleware(videoFilesDeleteHLSFileValidator),
40 asyncMiddleware(removeHLSFileController)
41)
42
43filesRouter.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)
50filesRouter.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
60export {
61 filesRouter
62}
63
64// ---------------------------------------------------------------------------
65
66async 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
74async 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
85async 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
101async 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
112async 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 @@
1import express from 'express'
2import { move, readFile } from 'fs-extra'
3import { decode } from 'magnet-uri'
4import parseTorrent, { Instance } from 'parse-torrent'
5import { join } from 'path'
6import { buildYoutubeDLImport, buildVideoFromImport, insertFromImportIntoDB, YoutubeDlImportError } from '@server/lib/video-pre-import'
7import { MThumbnail, MVideoThumbnail } from '@server/types/models'
8import { HttpStatusCode, ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState } from '@shared/models'
9import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
10import { isArray } from '../../../helpers/custom-validators/misc'
11import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
12import { logger } from '../../../helpers/logger'
13import { getSecureTorrentName } from '../../../helpers/utils'
14import { CONFIG } from '../../../initializers/config'
15import { MIMETYPES } from '../../../initializers/constants'
16import { JobQueue } from '../../../lib/job-queue/job-queue'
17import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail'
18import {
19 asyncMiddleware,
20 asyncRetryTransactionMiddleware,
21 authenticate,
22 videoImportAddValidator,
23 videoImportCancelValidator,
24 videoImportDeleteValidator
25} from '../../../middlewares'
26
27const auditLogger = auditLoggerFactory('video-imports')
28const videoImportsRouter = express.Router()
29
30const reqVideoFileImport = createReqFiles(
31 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
32 { ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
33)
34
35videoImportsRouter.post('/imports',
36 authenticate,
37 reqVideoFileImport,
38 asyncMiddleware(videoImportAddValidator),
39 asyncRetryTransactionMiddleware(handleVideoImport)
40)
41
42videoImportsRouter.post('/imports/:id/cancel',
43 authenticate,
44 asyncMiddleware(videoImportCancelValidator),
45 asyncRetryTransactionMiddleware(cancelVideoImport)
46)
47
48videoImportsRouter.delete('/imports/:id',
49 authenticate,
50 asyncMiddleware(videoImportDeleteValidator),
51 asyncRetryTransactionMiddleware(deleteVideoImport)
52)
53
54// ---------------------------------------------------------------------------
55
56export {
57 videoImportsRouter
58}
59
60// ---------------------------------------------------------------------------
61
62async 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
70async 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
79function 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
86async 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
146function 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
159async 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
191async 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
207async 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
223async 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
250function 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
260function 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 @@
1import express from 'express'
2import { pickCommonVideoQuery } from '@server/helpers/query'
3import { doJSONRequest } from '@server/helpers/requests'
4import { openapiOperationDoc } from '@server/middlewares/doc'
5import { getServerActor } from '@server/models/application/application'
6import { MVideoAccountLight } from '@server/types/models'
7import { HttpStatusCode } from '../../../../shared/models'
8import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
9import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
10import { logger } from '../../../helpers/logger'
11import { getFormattedObjects } from '../../../helpers/utils'
12import { REMOTE_SCHEME, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
13import { sequelizeTypescript } from '../../../initializers/database'
14import { JobQueue } from '../../../lib/job-queue'
15import { Hooks } from '../../../lib/plugins/hooks'
16import {
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'
32import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter'
33import { VideoModel } from '../../../models/video/video'
34import { blacklistRouter } from './blacklist'
35import { videoCaptionsRouter } from './captions'
36import { videoCommentRouter } from './comment'
37import { filesRouter } from './files'
38import { videoImportsRouter } from './import'
39import { liveRouter } from './live'
40import { ownershipVideoRouter } from './ownership'
41import { videoPasswordRouter } from './passwords'
42import { rateVideoRouter } from './rate'
43import { videoSourceRouter } from './source'
44import { statsRouter } from './stats'
45import { storyboardRouter } from './storyboard'
46import { studioRouter } from './studio'
47import { tokenRouter } from './token'
48import { transcodingRouter } from './transcoding'
49import { updateRouter } from './update'
50import { uploadRouter } from './upload'
51import { viewRouter } from './view'
52
53const auditLogger = auditLoggerFactory('videos')
54const videosRouter = express.Router()
55
56videosRouter.use(apiRateLimiter)
57
58videosRouter.use('/', blacklistRouter)
59videosRouter.use('/', statsRouter)
60videosRouter.use('/', rateVideoRouter)
61videosRouter.use('/', videoCommentRouter)
62videosRouter.use('/', studioRouter)
63videosRouter.use('/', videoCaptionsRouter)
64videosRouter.use('/', videoImportsRouter)
65videosRouter.use('/', ownershipVideoRouter)
66videosRouter.use('/', viewRouter)
67videosRouter.use('/', liveRouter)
68videosRouter.use('/', uploadRouter)
69videosRouter.use('/', updateRouter)
70videosRouter.use('/', filesRouter)
71videosRouter.use('/', transcodingRouter)
72videosRouter.use('/', tokenRouter)
73videosRouter.use('/', videoPasswordRouter)
74videosRouter.use('/', storyboardRouter)
75videosRouter.use('/', videoSourceRouter)
76
77videosRouter.get('/categories',
78 openapiOperationDoc({ operationId: 'getCategories' }),
79 listVideoCategories
80)
81videosRouter.get('/licences',
82 openapiOperationDoc({ operationId: 'getLicences' }),
83 listVideoLicences
84)
85videosRouter.get('/languages',
86 openapiOperationDoc({ operationId: 'getLanguages' }),
87 listVideoLanguages
88)
89videosRouter.get('/privacies',
90 openapiOperationDoc({ operationId: 'getPrivacies' }),
91 listVideoPrivacies
92)
93
94videosRouter.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
106videosRouter.get('/:id/description',
107 openapiOperationDoc({ operationId: 'getVideoDesc' }),
108 asyncMiddleware(videosGetValidator),
109 asyncMiddleware(getVideoDescription)
110)
111
112videosRouter.get('/:id',
113 openapiOperationDoc({ operationId: 'getVideo' }),
114 optionalAuthenticate,
115 asyncMiddleware(videosCustomGetValidator('for-api')),
116 asyncMiddleware(checkVideoFollowConstraints),
117 asyncMiddleware(getVideo)
118)
119
120videosRouter.delete('/:id',
121 openapiOperationDoc({ operationId: 'delVideo' }),
122 authenticate,
123 asyncMiddleware(videosRemoveValidator),
124 asyncRetryTransactionMiddleware(removeVideo)
125)
126
127// ---------------------------------------------------------------------------
128
129export {
130 videosRouter
131}
132
133// ---------------------------------------------------------------------------
134
135function listVideoCategories (_req: express.Request, res: express.Response) {
136 res.json(VIDEO_CATEGORIES)
137}
138
139function listVideoLicences (_req: express.Request, res: express.Response) {
140 res.json(VIDEO_LICENCES)
141}
142
143function listVideoLanguages (_req: express.Request, res: express.Response) {
144 res.json(VIDEO_LANGUAGES)
145}
146
147function listVideoPrivacies (_req: express.Request, res: express.Response) {
148 res.json(VIDEO_PRIVACIES)
149}
150
151async 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
164async 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
174async 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
201async 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
221async 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 @@
1import express from 'express'
2import { exists } from '@server/helpers/custom-validators/misc'
3import { createReqFiles } from '@server/helpers/express-utils'
4import { getFormattedObjects } from '@server/helpers/utils'
5import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants'
6import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
7import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
8import { Hooks } from '@server/lib/plugins/hooks'
9import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
10import {
11 videoLiveAddValidator,
12 videoLiveFindReplaySessionValidator,
13 videoLiveGetValidator,
14 videoLiveListSessionsValidator,
15 videoLiveUpdateValidator
16} from '@server/middlewares/validators/videos/video-live'
17import { VideoLiveModel } from '@server/models/video/video-live'
18import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
19import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models'
20import { buildUUID, uuidToShort } from '@shared/extra-utils'
21import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoPrivacy, VideoState } from '@shared/models'
22import { logger } from '../../../helpers/logger'
23import { sequelizeTypescript } from '../../../initializers/database'
24import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail'
25import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares'
26import { VideoModel } from '../../../models/video/video'
27import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
28import { VideoPasswordModel } from '@server/models/video/video-password'
29
30const liveRouter = express.Router()
31
32const reqVideoFileLive = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
33
34liveRouter.post('/live',
35 authenticate,
36 reqVideoFileLive,
37 asyncMiddleware(videoLiveAddValidator),
38 asyncRetryTransactionMiddleware(addLiveVideo)
39)
40
41liveRouter.get('/live/:videoId/sessions',
42 authenticate,
43 asyncMiddleware(videoLiveGetValidator),
44 videoLiveListSessionsValidator,
45 asyncMiddleware(getLiveVideoSessions)
46)
47
48liveRouter.get('/live/:videoId',
49 optionalAuthenticate,
50 asyncMiddleware(videoLiveGetValidator),
51 getLiveVideo
52)
53
54liveRouter.put('/live/:videoId',
55 authenticate,
56 asyncMiddleware(videoLiveGetValidator),
57 videoLiveUpdateValidator,
58 asyncRetryTransactionMiddleware(updateLiveVideo)
59)
60
61liveRouter.get('/:videoId/live-session',
62 asyncMiddleware(videoLiveFindReplaySessionValidator),
63 getLiveReplaySession
64)
65
66// ---------------------------------------------------------------------------
67
68export {
69 liveRouter
70}
71
72// ---------------------------------------------------------------------------
73
74function getLiveVideo (req: express.Request, res: express.Response) {
75 const videoLive = res.locals.videoLive
76
77 return res.json(videoLive.toFormattedJSON(canSeePrivateLiveInformation(res)))
78}
79
80function getLiveReplaySession (req: express.Request, res: express.Response) {
81 const session = res.locals.videoLiveSession
82
83 return res.json(session.toFormattedJSON())
84}
85
86async 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
94function 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
104async 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
124async 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
145async 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 @@
1import express from 'express'
2import { MVideoFullLight } from '@server/types/models'
3import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
4import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos'
5import { logger } from '../../../helpers/logger'
6import { getFormattedObjects } from '../../../helpers/utils'
7import { sequelizeTypescript } from '../../../initializers/database'
8import { sendUpdateVideo } from '../../../lib/activitypub/send'
9import { changeVideoChannelShare } from '../../../lib/activitypub/share'
10import {
11 asyncMiddleware,
12 asyncRetryTransactionMiddleware,
13 authenticate,
14 paginationValidator,
15 setDefaultPagination,
16 videosAcceptChangeOwnershipValidator,
17 videosChangeOwnershipValidator,
18 videosTerminateChangeOwnershipValidator
19} from '../../../middlewares'
20import { VideoModel } from '../../../models/video/video'
21import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
22import { VideoChannelModel } from '../../../models/video/video-channel'
23
24const ownershipVideoRouter = express.Router()
25
26ownershipVideoRouter.post('/:videoId/give-ownership',
27 authenticate,
28 asyncMiddleware(videosChangeOwnershipValidator),
29 asyncRetryTransactionMiddleware(giveVideoOwnership)
30)
31
32ownershipVideoRouter.get('/ownership',
33 authenticate,
34 paginationValidator,
35 setDefaultPagination,
36 asyncRetryTransactionMiddleware(listVideoOwnership)
37)
38
39ownershipVideoRouter.post('/ownership/:id/accept',
40 authenticate,
41 asyncMiddleware(videosTerminateChangeOwnershipValidator),
42 asyncMiddleware(videosAcceptChangeOwnershipValidator),
43 asyncRetryTransactionMiddleware(acceptOwnership)
44)
45
46ownershipVideoRouter.post('/ownership/:id/refuse',
47 authenticate,
48 asyncMiddleware(videosTerminateChangeOwnershipValidator),
49 asyncRetryTransactionMiddleware(refuseOwnership)
50)
51
52// ---------------------------------------------------------------------------
53
54export {
55 ownershipVideoRouter
56}
57
58// ---------------------------------------------------------------------------
59
60async 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
89async 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
102function 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
129function 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 @@
1import express from 'express'
2
3import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
4import { getFormattedObjects } from '../../../helpers/utils'
5import {
6 asyncMiddleware,
7 asyncRetryTransactionMiddleware,
8 authenticate,
9 setDefaultPagination,
10 setDefaultSort
11} from '../../../middlewares'
12import {
13 listVideoPasswordValidator,
14 paginationValidator,
15 removeVideoPasswordValidator,
16 updateVideoPasswordListValidator,
17 videoPasswordsSortValidator
18} from '../../../middlewares/validators'
19import { VideoPasswordModel } from '@server/models/video/video-password'
20import { logger, loggerTagsFactory } from '@server/helpers/logger'
21import { Transaction } from 'sequelize'
22import { getVideoWithAttributes } from '@server/helpers/video'
23
24const lTags = loggerTagsFactory('api', 'video')
25const videoPasswordRouter = express.Router()
26
27videoPasswordRouter.get('/:videoId/passwords',
28 authenticate,
29 paginationValidator,
30 videoPasswordsSortValidator,
31 setDefaultSort,
32 setDefaultPagination,
33 asyncMiddleware(listVideoPasswordValidator),
34 asyncMiddleware(listVideoPasswords)
35)
36
37videoPasswordRouter.put('/:videoId/passwords',
38 authenticate,
39 asyncMiddleware(updateVideoPasswordListValidator),
40 asyncMiddleware(updateVideoPasswordList)
41)
42
43videoPasswordRouter.delete('/:videoId/passwords/:passwordId',
44 authenticate,
45 asyncMiddleware(removeVideoPasswordValidator),
46 asyncRetryTransactionMiddleware(removeVideoPassword)
47)
48
49// ---------------------------------------------------------------------------
50
51export {
52 videoPasswordRouter
53}
54
55// ---------------------------------------------------------------------------
56
57async 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
70async 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
91async 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 @@
1import express from 'express'
2import { HttpStatusCode, UserVideoRateUpdate } from '@shared/models'
3import { logger } from '../../../helpers/logger'
4import { VIDEO_RATE_TYPES } from '../../../initializers/constants'
5import { sequelizeTypescript } from '../../../initializers/database'
6import { getLocalRateUrl, sendVideoRateChange } from '../../../lib/activitypub/video-rates'
7import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
8import { AccountModel } from '../../../models/account/account'
9import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
10
11const rateVideoRouter = express.Router()
12
13rateVideoRouter.put('/:id/rate',
14 authenticate,
15 asyncMiddleware(videoUpdateRateValidator),
16 asyncRetryTransactionMiddleware(rateVideo)
17)
18
19// ---------------------------------------------------------------------------
20
21export {
22 rateVideoRouter
23}
24
25// ---------------------------------------------------------------------------
26
27async 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 @@
1import express from 'express'
2import { move } from 'fs-extra'
3import { sequelizeTypescript } from '@server/initializers/database'
4import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue'
5import { Hooks } from '@server/lib/plugins/hooks'
6import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail'
7import { uploadx } from '@server/lib/uploadx'
8import { buildMoveToObjectStorageJob } from '@server/lib/video'
9import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
10import { buildNewFile } from '@server/lib/video-file'
11import { VideoPathManager } from '@server/lib/video-path-manager'
12import { buildNextVideoState } from '@server/lib/video-state'
13import { openapiOperationDoc } from '@server/middlewares/doc'
14import { VideoModel } from '@server/models/video/video'
15import { VideoSourceModel } from '@server/models/video/video-source'
16import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
17import { VideoState } from '@shared/models'
18import { logger, loggerTagsFactory } from '../../../helpers/logger'
19import {
20 asyncMiddleware,
21 authenticate,
22 replaceVideoSourceResumableInitValidator,
23 replaceVideoSourceResumableValidator,
24 videoSourceGetLatestValidator
25} from '../../../middlewares'
26
27const lTags = loggerTagsFactory('api', 'video')
28
29const videoSourceRouter = express.Router()
30
31videoSourceRouter.get('/:id/source',
32 openapiOperationDoc({ operationId: 'getVideoSource' }),
33 authenticate,
34 asyncMiddleware(videoSourceGetLatestValidator),
35 getVideoLatestSource
36)
37
38videoSourceRouter.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
44videoSourceRouter.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
49videoSourceRouter.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
58export {
59 videoSourceRouter
60}
61
62// ---------------------------------------------------------------------------
63
64function getVideoLatestSource (req: express.Request, res: express.Response) {
65 return res.json(res.locals.videoSource.toFormattedJSON())
66}
67
68async 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
144async 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
192async 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 @@
1import express from 'express'
2import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
3import { VideoStatsOverallQuery, VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models'
4import {
5 asyncMiddleware,
6 authenticate,
7 videoOverallStatsValidator,
8 videoRetentionStatsValidator,
9 videoTimeserieStatsValidator
10} from '../../../middlewares'
11
12const statsRouter = express.Router()
13
14statsRouter.get('/:videoId/stats/overall',
15 authenticate,
16 asyncMiddleware(videoOverallStatsValidator),
17 asyncMiddleware(getOverallStats)
18)
19
20statsRouter.get('/:videoId/stats/timeseries/:metric',
21 authenticate,
22 asyncMiddleware(videoTimeserieStatsValidator),
23 asyncMiddleware(getTimeserieStats)
24)
25
26statsRouter.get('/:videoId/stats/retention',
27 authenticate,
28 asyncMiddleware(videoRetentionStatsValidator),
29 asyncMiddleware(getRetentionStats)
30)
31
32// ---------------------------------------------------------------------------
33
34export {
35 statsRouter
36}
37
38// ---------------------------------------------------------------------------
39
40async 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
53async 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
61async 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 @@
1import express from 'express'
2import { getVideoWithAttributes } from '@server/helpers/video'
3import { StoryboardModel } from '@server/models/video/storyboard'
4import { asyncMiddleware, videosGetValidator } from '../../../middlewares'
5
6const storyboardRouter = express.Router()
7
8storyboardRouter.get('/:id/storyboards',
9 asyncMiddleware(videosGetValidator),
10 asyncMiddleware(listStoryboards)
11)
12
13// ---------------------------------------------------------------------------
14
15export {
16 storyboardRouter
17}
18
19// ---------------------------------------------------------------------------
20
21async 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 @@
1import Bluebird from 'bluebird'
2import express from 'express'
3import { move } from 'fs-extra'
4import { basename } from 'path'
5import { createAnyReqFiles } from '@server/helpers/express-utils'
6import { MIMETYPES, VIDEO_FILTERS } from '@server/initializers/constants'
7import { buildTaskFileFieldname, createVideoStudioJob, getStudioTaskFilePath, getTaskFileFromReq } from '@server/lib/video-studio'
8import {
9 HttpStatusCode,
10 VideoState,
11 VideoStudioCreateEdition,
12 VideoStudioTask,
13 VideoStudioTaskCut,
14 VideoStudioTaskIntro,
15 VideoStudioTaskOutro,
16 VideoStudioTaskPayload,
17 VideoStudioTaskWatermark
18} from '@shared/models'
19import { asyncMiddleware, authenticate, videoStudioAddEditionValidator } from '../../../middlewares'
20
21const studioRouter = express.Router()
22
23const 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
48studioRouter.post('/:videoId/studio/edit',
49 authenticate,
50 tasksFiles,
51 asyncMiddleware(videoStudioAddEditionValidator),
52 asyncMiddleware(createEditionTasks)
53)
54
55// ---------------------------------------------------------------------------
56
57export {
58 studioRouter
59}
60
61// ---------------------------------------------------------------------------
62
63async 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
85const 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
98function buildTaskPayload (task: VideoStudioTask, indice: number, files: Express.Multer.File[]): Promise<VideoStudioTaskPayload> {
99 return taskPayloadBuilders[task.name](task, indice, files)
100}
101
102async 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
113function 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
123async 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
137async 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 @@
1import express from 'express'
2import { VideoTokensManager } from '@server/lib/video-tokens-manager'
3import { VideoPrivacy, VideoToken } from '@shared/models'
4import { asyncMiddleware, optionalAuthenticate, videoFileTokenValidator, videosCustomGetValidator } from '../../../middlewares'
5
6const tokenRouter = express.Router()
7
8tokenRouter.post('/:id/token',
9 optionalAuthenticate,
10 asyncMiddleware(videosCustomGetValidator('only-video')),
11 videoFileTokenValidator,
12 generateToken
13)
14
15// ---------------------------------------------------------------------------
16
17export {
18 tokenRouter
19}
20
21// ---------------------------------------------------------------------------
22
23function 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 @@
1import express from 'express'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { Hooks } from '@server/lib/plugins/hooks'
4import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job'
5import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions'
6import { VideoJobInfoModel } from '@server/models/video/video-job-info'
7import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models'
8import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares'
9
10const lTags = loggerTagsFactory('api', 'video')
11const transcodingRouter = express.Router()
12
13transcodingRouter.post('/:videoId/transcoding',
14 authenticate,
15 ensureUserHasRight(UserRight.RUN_VIDEO_TRANSCODING),
16 asyncMiddleware(createTranscodingValidator),
17 asyncMiddleware(createTranscoding)
18)
19
20// ---------------------------------------------------------------------------
21
22export {
23 transcodingRouter
24}
25
26// ---------------------------------------------------------------------------
27
28async 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 @@
1import express from 'express'
2import { Transaction } from 'sequelize/types'
3import { changeVideoChannelShare } from '@server/lib/activitypub/share'
4import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
5import { setVideoPrivacy } from '@server/lib/video-privacy'
6import { openapiOperationDoc } from '@server/middlewares/doc'
7import { FilteredModelAttributes } from '@server/types'
8import { MVideoFullLight } from '@server/types/models'
9import { forceNumber } from '@shared/core-utils'
10import { HttpStatusCode, VideoPrivacy, VideoUpdate } from '@shared/models'
11import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
12import { resetSequelizeInstance } from '../../../helpers/database-utils'
13import { createReqFiles } from '../../../helpers/express-utils'
14import { logger, loggerTagsFactory } from '../../../helpers/logger'
15import { MIMETYPES } from '../../../initializers/constants'
16import { sequelizeTypescript } from '../../../initializers/database'
17import { Hooks } from '../../../lib/plugins/hooks'
18import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
19import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
20import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
21import { VideoModel } from '../../../models/video/video'
22import { VideoPathManager } from '@server/lib/video-path-manager'
23import { VideoPasswordModel } from '@server/models/video/video-password'
24import { exists } from '@server/helpers/custom-validators/misc'
25
26const lTags = loggerTagsFactory('api', 'video')
27const auditLogger = auditLoggerFactory('videos')
28const updateRouter = express.Router()
29
30const reqVideoFileUpdate = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
31
32updateRouter.put('/:id',
33 openapiOperationDoc({ operationId: 'putVideo' }),
34 authenticate,
35 reqVideoFileUpdate,
36 asyncMiddleware(videosUpdateValidator),
37 asyncRetryTransactionMiddleware(updateVideo)
38)
39
40// ---------------------------------------------------------------------------
41
42export {
43 updateRouter
44}
45
46// ---------------------------------------------------------------------------
47
48async 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
170async 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
200function 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 @@
1import express from 'express'
2import { move } from 'fs-extra'
3import { basename } from 'path'
4import { getResumableUploadPath } from '@server/helpers/upload'
5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
6import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue'
7import { Redis } from '@server/lib/redis'
8import { uploadx } from '@server/lib/uploadx'
9import { buildLocalVideoFromReq, buildMoveToObjectStorageJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
10import { buildNewFile } from '@server/lib/video-file'
11import { VideoPathManager } from '@server/lib/video-path-manager'
12import { buildNextVideoState } from '@server/lib/video-state'
13import { openapiOperationDoc } from '@server/middlewares/doc'
14import { VideoPasswordModel } from '@server/models/video/video-password'
15import { VideoSourceModel } from '@server/models/video/video-source'
16import { MVideoFile, MVideoFullLight } from '@server/types/models'
17import { uuidToShort } from '@shared/extra-utils'
18import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models'
19import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
20import { createReqFiles } from '../../../helpers/express-utils'
21import { logger, loggerTagsFactory } from '../../../helpers/logger'
22import { MIMETYPES } from '../../../initializers/constants'
23import { sequelizeTypescript } from '../../../initializers/database'
24import { Hooks } from '../../../lib/plugins/hooks'
25import { generateLocalVideoMiniature } from '../../../lib/thumbnail'
26import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
27import {
28 asyncMiddleware,
29 asyncRetryTransactionMiddleware,
30 authenticate,
31 videosAddLegacyValidator,
32 videosAddResumableInitValidator,
33 videosAddResumableValidator
34} from '../../../middlewares'
35import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
36import { VideoModel } from '../../../models/video/video'
37
38const lTags = loggerTagsFactory('api', 'video')
39const auditLogger = auditLoggerFactory('videos')
40const uploadRouter = express.Router()
41
42const reqVideoFileAdd = createReqFiles(
43 [ 'videofile', 'thumbnailfile', 'previewfile' ],
44 { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
45)
46
47const reqVideoFileAddResumable = createReqFiles(
48 [ 'thumbnailfile', 'previewfile' ],
49 MIMETYPES.IMAGE.MIMETYPE_EXT,
50 getResumableUploadPath()
51)
52
53uploadRouter.post('/upload',
54 openapiOperationDoc({ operationId: 'uploadLegacy' }),
55 authenticate,
56 reqVideoFileAdd,
57 asyncMiddleware(videosAddLegacyValidator),
58 asyncRetryTransactionMiddleware(addVideoLegacy)
59)
60
61uploadRouter.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
69uploadRouter.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
75uploadRouter.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
85export {
86 uploadRouter
87}
88
89// ---------------------------------------------------------------------------
90
91async 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
111async 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
122async 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
227async 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
283async 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 @@
1import express from 'express'
2import { Hooks } from '@server/lib/plugins/hooks'
3import { VideoViewsManager } from '@server/lib/views/video-views-manager'
4import { MVideoId } from '@server/types/models'
5import { HttpStatusCode, VideoView } from '@shared/models'
6import { asyncMiddleware, methodsValidator, openapiOperationDoc, optionalAuthenticate, videoViewValidator } from '../../../middlewares'
7import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
8
9const viewRouter = express.Router()
10
11viewRouter.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
22export {
23 viewRouter
24}
25
26// ---------------------------------------------------------------------------
27
28async 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
50async 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}