aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers/api/videos
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers/api/videos')
-rw-r--r--server/controllers/api/videos/index.ts16
-rw-r--r--server/controllers/api/videos/source.ts206
-rw-r--r--server/controllers/api/videos/update.ts1
-rw-r--r--server/controllers/api/videos/upload.ts11
4 files changed, 216 insertions, 18 deletions
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 520d8cbbb..3cdd42289 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -26,7 +26,6 @@ import {
26 setDefaultVideosSort, 26 setDefaultVideosSort,
27 videosCustomGetValidator, 27 videosCustomGetValidator,
28 videosGetValidator, 28 videosGetValidator,
29 videoSourceGetValidator,
30 videosRemoveValidator, 29 videosRemoveValidator,
31 videosSortValidator 30 videosSortValidator
32} from '../../../middlewares' 31} from '../../../middlewares'
@@ -39,7 +38,9 @@ import { filesRouter } from './files'
39import { videoImportsRouter } from './import' 38import { videoImportsRouter } from './import'
40import { liveRouter } from './live' 39import { liveRouter } from './live'
41import { ownershipVideoRouter } from './ownership' 40import { ownershipVideoRouter } from './ownership'
41import { videoPasswordRouter } from './passwords'
42import { rateVideoRouter } from './rate' 42import { rateVideoRouter } from './rate'
43import { videoSourceRouter } from './source'
43import { statsRouter } from './stats' 44import { statsRouter } from './stats'
44import { storyboardRouter } from './storyboard' 45import { storyboardRouter } from './storyboard'
45import { studioRouter } from './studio' 46import { studioRouter } from './studio'
@@ -48,7 +49,6 @@ import { transcodingRouter } from './transcoding'
48import { updateRouter } from './update' 49import { updateRouter } from './update'
49import { uploadRouter } from './upload' 50import { uploadRouter } from './upload'
50import { viewRouter } from './view' 51import { viewRouter } from './view'
51import { videoPasswordRouter } from './passwords'
52 52
53const auditLogger = auditLoggerFactory('videos') 53const auditLogger = auditLoggerFactory('videos')
54const videosRouter = express.Router() 54const videosRouter = express.Router()
@@ -72,6 +72,7 @@ videosRouter.use('/', transcodingRouter)
72videosRouter.use('/', tokenRouter) 72videosRouter.use('/', tokenRouter)
73videosRouter.use('/', videoPasswordRouter) 73videosRouter.use('/', videoPasswordRouter)
74videosRouter.use('/', storyboardRouter) 74videosRouter.use('/', storyboardRouter)
75videosRouter.use('/', videoSourceRouter)
75 76
76videosRouter.get('/categories', 77videosRouter.get('/categories',
77 openapiOperationDoc({ operationId: 'getCategories' }), 78 openapiOperationDoc({ operationId: 'getCategories' }),
@@ -108,13 +109,6 @@ videosRouter.get('/:id/description',
108 asyncMiddleware(getVideoDescription) 109 asyncMiddleware(getVideoDescription)
109) 110)
110 111
111videosRouter.get('/:id/source',
112 openapiOperationDoc({ operationId: 'getVideoSource' }),
113 authenticate,
114 asyncMiddleware(videoSourceGetValidator),
115 getVideoSource
116)
117
118videosRouter.get('/:id', 112videosRouter.get('/:id',
119 openapiOperationDoc({ operationId: 'getVideo' }), 113 openapiOperationDoc({ operationId: 'getVideo' }),
120 optionalAuthenticate, 114 optionalAuthenticate,
@@ -177,10 +171,6 @@ async function getVideoDescription (req: express.Request, res: express.Response)
177 return res.json({ description }) 171 return res.json({ description })
178} 172}
179 173
180function getVideoSource (req: express.Request, res: express.Response) {
181 return res.json(res.locals.videoSource.toFormattedJSON())
182}
183
184async function listVideos (req: express.Request, res: express.Response) { 174async function listVideos (req: express.Request, res: express.Response) {
185 const serverActor = await getServerActor() 175 const serverActor = await getServerActor()
186 176
diff --git a/server/controllers/api/videos/source.ts b/server/controllers/api/videos/source.ts
new file mode 100644
index 000000000..b20c4af0e
--- /dev/null
+++ b/server/controllers/api/videos/source.ts
@@ -0,0 +1,206 @@
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 { HttpStatusCode, 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 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.sendStatus(HttpStatusCode.NO_CONTENT_204)
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/update.ts b/server/controllers/api/videos/update.ts
index 28ec2cf37..1edc509dc 100644
--- a/server/controllers/api/videos/update.ts
+++ b/server/controllers/api/videos/update.ts
@@ -130,6 +130,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
130 user: res.locals.oauth.token.User, 130 user: res.locals.oauth.token.User,
131 isRemote: false, 131 isRemote: false,
132 isNew: false, 132 isNew: false,
133 isNewFile: false,
133 transaction: t 134 transaction: t
134 }) 135 })
135 136
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 27fef0b1a..e520bf4b5 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -11,8 +11,9 @@ import { buildNewFile } from '@server/lib/video-file'
11import { VideoPathManager } from '@server/lib/video-path-manager' 11import { VideoPathManager } from '@server/lib/video-path-manager'
12import { buildNextVideoState } from '@server/lib/video-state' 12import { buildNextVideoState } from '@server/lib/video-state'
13import { openapiOperationDoc } from '@server/middlewares/doc' 13import { openapiOperationDoc } from '@server/middlewares/doc'
14import { VideoPasswordModel } from '@server/models/video/video-password'
14import { VideoSourceModel } from '@server/models/video/video-source' 15import { VideoSourceModel } from '@server/models/video/video-source'
15import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models' 16import { MVideoFile, MVideoFullLight } from '@server/types/models'
16import { uuidToShort } from '@shared/extra-utils' 17import { uuidToShort } from '@shared/extra-utils'
17import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' 18import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models'
18import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 19import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
@@ -33,7 +34,6 @@ import {
33} from '../../../middlewares' 34} from '../../../middlewares'
34import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 35import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
35import { VideoModel } from '../../../models/video/video' 36import { VideoModel } from '../../../models/video/video'
36import { VideoPasswordModel } from '@server/models/video/video-password'
37 37
38const lTags = loggerTagsFactory('api', 'video') 38const lTags = loggerTagsFactory('api', 'video')
39const auditLogger = auditLoggerFactory('videos') 39const auditLogger = auditLoggerFactory('videos')
@@ -109,7 +109,7 @@ async function addVideoLegacy (req: express.Request, res: express.Response) {
109} 109}
110 110
111async function addVideoResumable (req: express.Request, res: express.Response) { 111async function addVideoResumable (req: express.Request, res: express.Response) {
112 const videoPhysicalFile = res.locals.videoFileResumable 112 const videoPhysicalFile = res.locals.uploadVideoFileResumable
113 const videoInfo = videoPhysicalFile.metadata 113 const videoInfo = videoPhysicalFile.metadata
114 const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile } 114 const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile }
115 115
@@ -193,6 +193,7 @@ async function addVideo (options: {
193 user, 193 user,
194 isRemote: false, 194 isRemote: false,
195 isNew: true, 195 isNew: true,
196 isNewFile: true,
196 transaction: t 197 transaction: t
197 }) 198 })
198 199
@@ -209,7 +210,7 @@ async function addVideo (options: {
209 // Channel has a new content, set as updated 210 // Channel has a new content, set as updated
210 await videoCreated.VideoChannel.setAsUpdated() 211 await videoCreated.VideoChannel.setAsUpdated()
211 212
212 addVideoJobsAfterUpload(videoCreated, videoFile, user) 213 addVideoJobsAfterUpload(videoCreated, videoFile)
213 .catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })) 214 .catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
214 215
215 Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res }) 216 Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res })
@@ -223,7 +224,7 @@ async function addVideo (options: {
223 } 224 }
224} 225}
225 226
226async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile, user: MUserId) { 227async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) {
227 const jobs: (CreateJobArgument & CreateJobOptions)[] = [ 228 const jobs: (CreateJobArgument & CreateJobOptions)[] = [
228 { 229 {
229 type: 'manage-video-torrent' as 'manage-video-torrent', 230 type: 'manage-video-torrent' as 'manage-video-torrent',