aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/update.ts34
-rw-r--r--server/controllers/api/videos/upload.ts64
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/lib/job-queue/handlers/manage-video-torrent.ts88
-rw-r--r--server/lib/job-queue/job-queue.ts7
-rw-r--r--server/models/video/video.ts18
6 files changed, 162 insertions, 52 deletions
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts
index 15899307d..2cf8a5883 100644
--- a/server/controllers/api/videos/update.ts
+++ b/server/controllers/api/videos/update.ts
@@ -1,12 +1,12 @@
1import express from 'express' 1import express from 'express'
2import { Transaction } from 'sequelize/types' 2import { Transaction } from 'sequelize/types'
3import { updateTorrentMetadata } from '@server/helpers/webtorrent'
4import { changeVideoChannelShare } from '@server/lib/activitypub/share' 3import { changeVideoChannelShare } from '@server/lib/activitypub/share'
4import { JobQueue } from '@server/lib/job-queue'
5import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' 5import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
6import { openapiOperationDoc } from '@server/middlewares/doc' 6import { openapiOperationDoc } from '@server/middlewares/doc'
7import { FilteredModelAttributes } from '@server/types' 7import { FilteredModelAttributes } from '@server/types'
8import { MVideoFullLight } from '@server/types/models' 8import { MVideoFullLight } from '@server/types/models'
9import { HttpStatusCode, VideoUpdate } from '@shared/models' 9import { HttpStatusCode, ManageVideoTorrentPayload, VideoUpdate } from '@shared/models'
10import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 10import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
11import { resetSequelizeInstance } from '../../../helpers/database-utils' 11import { resetSequelizeInstance } from '../../../helpers/database-utils'
12import { createReqFiles } from '../../../helpers/express-utils' 12import { createReqFiles } from '../../../helpers/express-utils'
@@ -139,15 +139,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
139 return { videoInstanceUpdated, isNewVideo } 139 return { videoInstanceUpdated, isNewVideo }
140 }) 140 })
141 141
142 if (videoInstanceUpdated.isLive !== true && videoInfoToUpdate.name) { 142 const refreshedVideo = await updateTorrentsMetadataIfNeeded(videoInstanceUpdated, videoInfoToUpdate)
143 await updateTorrentsMetadata(videoInstanceUpdated)
144 }
145 143
146 await sequelizeTypescript.transaction(t => federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)) 144 await sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, isNewVideo, t))
147 145
148 if (wasConfidentialVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) 146 if (wasConfidentialVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
149 147
150 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res }) 148 Hooks.runAction('action:api.video.updated', { video: refreshedVideo, body: req.body, req, res })
151 } catch (err) { 149 } catch (err) {
152 // Force fields we want to update 150 // Force fields we want to update
153 // If the transaction is retried, sequelize will think the object has not changed 151 // If the transaction is retried, sequelize will think the object has not changed
@@ -194,19 +192,25 @@ function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: Vide
194 } 192 }
195} 193}
196 194
197async function updateTorrentsMetadata (video: MVideoFullLight) { 195async function updateTorrentsMetadataIfNeeded (video: MVideoFullLight, videoInfoToUpdate: VideoUpdate) {
196 if (video.isLive || !videoInfoToUpdate.name) return video
197
198 for (const file of (video.VideoFiles || [])) { 198 for (const file of (video.VideoFiles || [])) {
199 await updateTorrentMetadata(video, file) 199 const payload: ManageVideoTorrentPayload = { action: 'update-metadata', videoId: video.id, videoFileId: file.id }
200 200
201 await file.save() 201 const job = await JobQueue.Instance.createJobWithPromise({ type: 'manage-video-torrent', payload })
202 await job.finished()
202 } 203 }
203 204
204 const hls = video.getHLSPlaylist() 205 const hls = video.getHLSPlaylist()
205 if (!hls) return
206 206
207 for (const file of (hls.VideoFiles || [])) { 207 for (const file of (hls?.VideoFiles || [])) {
208 await updateTorrentMetadata(hls, file) 208 const payload: ManageVideoTorrentPayload = { action: 'update-metadata', streamingPlaylistId: hls.id, videoFileId: file.id }
209 209
210 await file.save() 210 const job = await JobQueue.Instance.createJobWithPromise({ type: 'manage-video-torrent', payload })
211 await job.finished()
211 } 212 }
213
214 // Refresh video since files have changed
215 return VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
212} 216}
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index dd69cf238..14ae9d920 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -2,8 +2,8 @@ import express from 'express'
2import { move } from 'fs-extra' 2import { move } from 'fs-extra'
3import { basename } from 'path' 3import { basename } from 'path'
4import { getResumableUploadPath } from '@server/helpers/upload' 4import { getResumableUploadPath } from '@server/helpers/upload'
5import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
6import { JobQueue } from '@server/lib/job-queue'
7import { generateWebTorrentVideoFilename } from '@server/lib/paths' 7import { generateWebTorrentVideoFilename } from '@server/lib/paths'
8import { Redis } from '@server/lib/redis' 8import { Redis } from '@server/lib/redis'
9import { uploadx } from '@server/lib/uploadx' 9import { uploadx } from '@server/lib/uploadx'
@@ -17,10 +17,10 @@ import {
17import { VideoPathManager } from '@server/lib/video-path-manager' 17import { VideoPathManager } from '@server/lib/video-path-manager'
18import { buildNextVideoState } from '@server/lib/video-state' 18import { buildNextVideoState } from '@server/lib/video-state'
19import { openapiOperationDoc } from '@server/middlewares/doc' 19import { openapiOperationDoc } from '@server/middlewares/doc'
20import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 20import { MVideoFile, MVideoFullLight } from '@server/types/models'
21import { getLowercaseExtension } from '@shared/core-utils' 21import { getLowercaseExtension } from '@shared/core-utils'
22import { isAudioFile, uuidToShort } from '@shared/extra-utils' 22import { isAudioFile, uuidToShort } from '@shared/extra-utils'
23import { HttpStatusCode, VideoCreate, VideoResolution, VideoState } from '@shared/models' 23import { HttpStatusCode, ManageVideoTorrentPayload, VideoCreate, VideoResolution, VideoState } from '@shared/models'
24import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 24import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
25import { retryTransactionWrapper } from '../../../helpers/database-utils' 25import { retryTransactionWrapper } from '../../../helpers/database-utils'
26import { createReqFiles } from '../../../helpers/express-utils' 26import { createReqFiles } from '../../../helpers/express-utils'
@@ -209,17 +209,22 @@ async function addVideo (options: {
209 // Channel has a new content, set as updated 209 // Channel has a new content, set as updated
210 await videoCreated.VideoChannel.setAsUpdated() 210 await videoCreated.VideoChannel.setAsUpdated()
211 211
212 createTorrentFederate(video, videoFile) 212 createTorrentFederate(videoCreated, videoFile)
213 .then(() => { 213 .catch(err => {
214 if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { 214 logger.error('Cannot create torrent or federate video for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })
215 return addMoveToObjectStorageJob(video) 215
216 return videoCreated
217 }).then(refreshedVideo => {
218 if (!refreshedVideo) return
219
220 if (refreshedVideo.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
221 return addMoveToObjectStorageJob(refreshedVideo)
216 } 222 }
217 223
218 if (video.state === VideoState.TO_TRANSCODE) { 224 if (refreshedVideo.state === VideoState.TO_TRANSCODE) {
219 return addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) 225 return addOptimizeOrMergeAudioJob(refreshedVideo, videoFile, user)
220 } 226 }
221 }) 227 }).catch(err => logger.error('Cannot add optimize/merge audio job for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
222 .catch(err => logger.error('Cannot add optimize/merge audio job for %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
223 228
224 Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res }) 229 Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res })
225 230
@@ -254,36 +259,23 @@ async function buildNewFile (videoPhysicalFile: express.VideoUploadFile) {
254 return videoFile 259 return videoFile
255} 260}
256 261
257async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) { 262async function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile) {
258 await createTorrentAndSetInfoHash(video, fileArg) 263 const payload: ManageVideoTorrentPayload = { videoId: video.id, videoFileId: videoFile.id, action: 'create' }
259 264
260 // Refresh videoFile because the createTorrentAndSetInfoHash could be long 265 const job = await JobQueue.Instance.createJobWithPromise({ type: 'manage-video-torrent', payload })
261 const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id) 266 await job.finished()
262 // File does not exist anymore, remove the generated torrent
263 if (!refreshedFile) return fileArg.removeTorrent()
264 267
265 refreshedFile.infoHash = fileArg.infoHash 268 const refreshedVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)
266 refreshedFile.torrentFilename = fileArg.torrentFilename 269 if (!refreshedVideo) return
267 270
268 return refreshedFile.save() 271 // Only federate and notify after the torrent creation
269} 272 Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
270 273
271function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile) { 274 await retryTransactionWrapper(() => {
272 // Create the torrent file in async way because it could be long 275 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
273 return createTorrentAndSetInfoHashAsync(video, videoFile) 276 })
274 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
275 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
276 .then(refreshedVideo => {
277 if (!refreshedVideo) return
278
279 // Only federate and notify after the torrent creation
280 Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
281 277
282 return retryTransactionWrapper(() => { 278 return refreshedVideo
283 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
284 })
285 })
286 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
287} 279}
288 280
289async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) { 281async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 1c849b561..e0f6f2bd2 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -153,6 +153,7 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = {
153 'video-redundancy': 1, 153 'video-redundancy': 1,
154 'video-live-ending': 1, 154 'video-live-ending': 1,
155 'video-edition': 1, 155 'video-edition': 1,
156 'manage-video-torrent': 1,
156 'move-to-object-storage': 3 157 'move-to-object-storage': 3
157} 158}
158// Excluded keys are jobs that can be configured by admins 159// Excluded keys are jobs that can be configured by admins
@@ -170,6 +171,7 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im
170 'video-redundancy': 1, 171 'video-redundancy': 1,
171 'video-live-ending': 10, 172 'video-live-ending': 10,
172 'video-edition': 1, 173 'video-edition': 1,
174 'manage-video-torrent': 1,
173 'move-to-object-storage': 1 175 'move-to-object-storage': 1
174} 176}
175const JOB_TTL: { [id in JobType]: number } = { 177const JOB_TTL: { [id in JobType]: number } = {
@@ -188,6 +190,7 @@ const JOB_TTL: { [id in JobType]: number } = {
188 'activitypub-refresher': 60000 * 10, // 10 minutes 190 'activitypub-refresher': 60000 * 10, // 10 minutes
189 'video-redundancy': 1000 * 3600 * 3, // 3 hours 191 'video-redundancy': 1000 * 3600 * 3, // 3 hours
190 'video-live-ending': 1000 * 60 * 10, // 10 minutes 192 'video-live-ending': 1000 * 60 * 10, // 10 minutes
193 'manage-video-torrent': 1000 * 3600 * 3, // 3 hours
191 'move-to-object-storage': 1000 * 60 * 60 * 3 // 3 hours 194 'move-to-object-storage': 1000 * 60 * 60 * 3 // 3 hours
192} 195}
193const REPEAT_JOBS: { [ id in JobType ]?: EveryRepeatOptions | CronRepeatOptions } = { 196const REPEAT_JOBS: { [ id in JobType ]?: EveryRepeatOptions | CronRepeatOptions } = {
diff --git a/server/lib/job-queue/handlers/manage-video-torrent.ts b/server/lib/job-queue/handlers/manage-video-torrent.ts
new file mode 100644
index 000000000..5cb4287e1
--- /dev/null
+++ b/server/lib/job-queue/handlers/manage-video-torrent.ts
@@ -0,0 +1,88 @@
1import { Job } from 'bull'
2import { createTorrentAndSetInfoHash, updateTorrentMetadata } from '@server/helpers/webtorrent'
3import { VideoModel } from '@server/models/video/video'
4import { VideoFileModel } from '@server/models/video/video-file'
5import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
6import { ManageVideoTorrentPayload } from '@shared/models'
7import { logger } from '../../../helpers/logger'
8
9async function processManageVideoTorrent (job: Job) {
10 const payload = job.data as ManageVideoTorrentPayload
11 logger.info('Processing torrent in job %d.', job.id)
12
13 if (payload.action === 'create') return doCreateAction(payload)
14 if (payload.action === 'update-metadata') return doUpdateMetadataAction(payload)
15}
16
17// ---------------------------------------------------------------------------
18
19export {
20 processManageVideoTorrent
21}
22
23// ---------------------------------------------------------------------------
24
25async function doCreateAction (payload: ManageVideoTorrentPayload & { action: 'create' }) {
26 const [ video, file ] = await Promise.all([
27 loadVideoOrLog(payload.videoId),
28 loadFileOrLog(payload.videoFileId)
29 ])
30
31 await createTorrentAndSetInfoHash(video, file)
32
33 // Refresh videoFile because the createTorrentAndSetInfoHash could be long
34 const refreshedFile = await VideoFileModel.loadWithVideo(file.id)
35 // File does not exist anymore, remove the generated torrent
36 if (!refreshedFile) return file.removeTorrent()
37
38 refreshedFile.infoHash = file.infoHash
39 refreshedFile.torrentFilename = file.torrentFilename
40
41 return refreshedFile.save()
42}
43
44async function doUpdateMetadataAction (payload: ManageVideoTorrentPayload & { action: 'update-metadata' }) {
45 const [ video, streamingPlaylist, file ] = await Promise.all([
46 loadVideoOrLog(payload.videoId),
47 loadStreamingPlaylistOrLog(payload.streamingPlaylistId),
48 loadFileOrLog(payload.videoFileId)
49 ])
50
51 await updateTorrentMetadata(video || streamingPlaylist, file)
52
53 await file.save()
54}
55
56async function loadVideoOrLog (videoId: number) {
57 if (!videoId) return undefined
58
59 const video = await VideoModel.load(videoId)
60 if (!video) {
61 logger.debug('Do not process torrent for video %d: does not exist anymore.', videoId)
62 }
63
64 return video
65}
66
67async function loadStreamingPlaylistOrLog (streamingPlaylistId: number) {
68 if (!streamingPlaylistId) return undefined
69
70 const streamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(streamingPlaylistId)
71 if (!streamingPlaylist) {
72 logger.debug('Do not process torrent for streaming playlist %d: does not exist anymore.', streamingPlaylistId)
73 }
74
75 return streamingPlaylist
76}
77
78async function loadFileOrLog (videoFileId: number) {
79 if (!videoFileId) return undefined
80
81 const file = await VideoFileModel.loadWithVideo(videoFileId)
82
83 if (!file) {
84 logger.debug('Do not process torrent for file %d: does not exist anymore.', videoFileId)
85 }
86
87 return file
88}
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index e10a3bab5..3224abcc3 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -12,6 +12,7 @@ import {
12 EmailPayload, 12 EmailPayload,
13 JobState, 13 JobState,
14 JobType, 14 JobType,
15 ManageVideoTorrentPayload,
15 MoveObjectStoragePayload, 16 MoveObjectStoragePayload,
16 RefreshPayload, 17 RefreshPayload,
17 VideoEditionPayload, 18 VideoEditionPayload,
@@ -31,6 +32,7 @@ import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unica
31import { refreshAPObject } from './handlers/activitypub-refresher' 32import { refreshAPObject } from './handlers/activitypub-refresher'
32import { processActorKeys } from './handlers/actor-keys' 33import { processActorKeys } from './handlers/actor-keys'
33import { processEmail } from './handlers/email' 34import { processEmail } from './handlers/email'
35import { processManageVideoTorrent } from './handlers/manage-video-torrent'
34import { processMoveToObjectStorage } from './handlers/move-to-object-storage' 36import { processMoveToObjectStorage } from './handlers/move-to-object-storage'
35import { processVideoEdition } from './handlers/video-edition' 37import { processVideoEdition } from './handlers/video-edition'
36import { processVideoFileImport } from './handlers/video-file-import' 38import { processVideoFileImport } from './handlers/video-file-import'
@@ -56,6 +58,7 @@ type CreateJobArgument =
56 { type: 'video-redundancy', payload: VideoRedundancyPayload } | 58 { type: 'video-redundancy', payload: VideoRedundancyPayload } |
57 { type: 'delete-resumable-upload-meta-file', payload: DeleteResumableUploadMetaFilePayload } | 59 { type: 'delete-resumable-upload-meta-file', payload: DeleteResumableUploadMetaFilePayload } |
58 { type: 'video-edition', payload: VideoEditionPayload } | 60 { type: 'video-edition', payload: VideoEditionPayload } |
61 { type: 'manage-video-torrent', payload: ManageVideoTorrentPayload } |
59 { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } 62 { type: 'move-to-object-storage', payload: MoveObjectStoragePayload }
60 63
61export type CreateJobOptions = { 64export type CreateJobOptions = {
@@ -79,6 +82,7 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = {
79 'actor-keys': processActorKeys, 82 'actor-keys': processActorKeys,
80 'video-redundancy': processVideoRedundancy, 83 'video-redundancy': processVideoRedundancy,
81 'move-to-object-storage': processMoveToObjectStorage, 84 'move-to-object-storage': processMoveToObjectStorage,
85 'manage-video-torrent': processManageVideoTorrent,
82 'video-edition': processVideoEdition 86 'video-edition': processVideoEdition
83} 87}
84 88
@@ -98,6 +102,7 @@ const jobTypes: JobType[] = [
98 'actor-keys', 102 'actor-keys',
99 'video-live-ending', 103 'video-live-ending',
100 'move-to-object-storage', 104 'move-to-object-storage',
105 'manage-video-torrent',
101 'video-edition' 106 'video-edition'
102] 107]
103 108
@@ -185,7 +190,7 @@ class JobQueue {
185 } 190 }
186 191
187 createJobWithPromise (obj: CreateJobArgument, options: CreateJobOptions = {}) { 192 createJobWithPromise (obj: CreateJobArgument, options: CreateJobOptions = {}) {
188 const queue = this.queues[obj.type] 193 const queue: Queue = this.queues[obj.type]
189 if (queue === undefined) { 194 if (queue === undefined) {
190 logger.error('Unknown queue %s: cannot create job.', obj.type) 195 logger.error('Unknown queue %s: cannot create job.', obj.type)
191 return 196 return
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index a4093ce3b..4147b3d62 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1683,6 +1683,24 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1683 return peertubeTruncate(this.description, { length: maxLength }) 1683 return peertubeTruncate(this.description, { length: maxLength })
1684 } 1684 }
1685 1685
1686 getAllFiles () {
1687 let files: MVideoFile[] = []
1688
1689 if (Array.isArray(this.VideoFiles)) {
1690 files = files.concat(this.VideoFiles)
1691 }
1692
1693 if (Array.isArray(this.VideoStreamingPlaylists)) {
1694 for (const p of this.VideoStreamingPlaylists) {
1695 if (Array.isArray(p.VideoFiles)) {
1696 files = files.concat(p.VideoFiles)
1697 }
1698 }
1699 }
1700
1701 return files
1702 }
1703
1686 probeMaxQualityFile () { 1704 probeMaxQualityFile () {
1687 const file = this.getMaxQualityFile() 1705 const file = this.getMaxQualityFile()
1688 const videoOrPlaylist = file.getVideoOrStreamingPlaylist() 1706 const videoOrPlaylist = file.getVideoOrStreamingPlaylist()