1 import express from 'express'
2 import { move, readFile } from 'fs-extra'
3 import { decode } from 'magnet-uri'
4 import parseTorrent, { Instance } from 'parse-torrent'
5 import { join } from 'path'
6 import { buildYoutubeDLImport, buildVideoFromImport, insertFromImportIntoDB, YoutubeDlImportError } from '@server/lib/video-pre-import'
7 import { MThumbnail, MVideoThumbnail } from '@server/types/models'
8 import { HttpStatusCode, ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState } from '@shared/models'
9 import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
10 import { isArray } from '../../../helpers/custom-validators/misc'
11 import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
12 import { logger } from '../../../helpers/logger'
13 import { getSecureTorrentName } from '../../../helpers/utils'
14 import { CONFIG } from '../../../initializers/config'
15 import { MIMETYPES } from '../../../initializers/constants'
16 import { JobQueue } from '../../../lib/job-queue/job-queue'
17 import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
20 asyncRetryTransactionMiddleware,
22 videoImportAddValidator,
23 videoImportCancelValidator,
24 videoImportDeleteValidator
25 } from '../../../middlewares'
27 const auditLogger = auditLoggerFactory('video-imports')
28 const videoImportsRouter = express.Router()
30 const reqVideoFileImport = createReqFiles(
31 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
32 { ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
35 videoImportsRouter.post('/imports',
38 asyncMiddleware(videoImportAddValidator),
39 asyncRetryTransactionMiddleware(handleVideoImport)
42 videoImportsRouter.post('/imports/:id/cancel',
44 asyncMiddleware(videoImportCancelValidator),
45 asyncRetryTransactionMiddleware(cancelVideoImport)
48 videoImportsRouter.delete('/imports/:id',
50 asyncMiddleware(videoImportDeleteValidator),
51 asyncRetryTransactionMiddleware(deleteVideoImport)
54 // ---------------------------------------------------------------------------
60 // ---------------------------------------------------------------------------
62 async function deleteVideoImport (req: express.Request, res: express.Response) {
63 const videoImport = res.locals.videoImport
65 await videoImport.destroy()
67 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
70 async function cancelVideoImport (req: express.Request, res: express.Response) {
71 const videoImport = res.locals.videoImport
73 videoImport.state = VideoImportState.CANCELLED
74 await videoImport.save()
76 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
79 function handleVideoImport (req: express.Request, res: express.Response) {
80 if (req.body.targetUrl) return handleYoutubeDlImport(req, res)
82 const file = req.files?.['torrentfile']?.[0]
83 if (req.body.magnetUri || file) return handleTorrentImport(req, res, file)
86 async function handleTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
87 const body: VideoImportCreate = req.body
88 const user = res.locals.oauth.token.User
91 let torrentName: string
95 const result = await processTorrentOrAbortRequest(req, res, torrentfile)
98 videoName = result.name
99 torrentName = result.torrentName
101 const result = processMagnetURI(body)
102 magnetUri = result.magnetUri
103 videoName = result.name
106 const video = await buildVideoFromImport({
107 channelId: res.locals.videoChannel.id,
108 importData: { name: videoName },
109 importDataOverride: body,
110 importType: 'torrent'
113 const thumbnailModel = await processThumbnail(req, video)
114 const previewModel = await processPreview(req, video)
116 const videoImport = await insertFromImportIntoDB({
120 videoChannel: res.locals.videoChannel,
121 tags: body.tags || undefined,
123 videoImportAttributes: {
126 state: VideoImportState.PENDING,
131 const payload: VideoImportPayload = {
135 videoImportId: videoImport.id,
136 preventException: false
138 await JobQueue.Instance.createJob({ type: 'video-import', payload })
140 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
142 return res.json(videoImport.toFormattedJSON()).end()
145 function statusFromYtDlImportError (err: YoutubeDlImportError): number {
147 case YoutubeDlImportError.CODE.NOT_ONLY_UNICAST_URL:
148 return HttpStatusCode.FORBIDDEN_403
150 case YoutubeDlImportError.CODE.FETCH_ERROR:
151 return HttpStatusCode.BAD_REQUEST_400
154 return HttpStatusCode.INTERNAL_SERVER_ERROR_500
158 async function handleYoutubeDlImport (req: express.Request, res: express.Response) {
159 const body: VideoImportCreate = req.body
160 const targetUrl = body.targetUrl
161 const user = res.locals.oauth.token.User
164 const { job, videoImport } = await buildYoutubeDLImport({
166 channel: res.locals.videoChannel,
167 importDataOverride: body,
168 thumbnailFilePath: req.files?.['thumbnailfile']?.[0].path,
169 previewFilePath: req.files?.['previewfile']?.[0].path,
172 await JobQueue.Instance.createJob(job)
174 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
176 return res.json(videoImport.toFormattedJSON()).end()
178 logger.error('An error occurred while importing the video %s. ', targetUrl, { err })
181 message: err.message,
182 status: statusFromYtDlImportError(err),
190 async function processThumbnail (req: express.Request, video: MVideoThumbnail) {
191 const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
192 if (thumbnailField) {
193 const thumbnailPhysicalFile = thumbnailField[0]
195 return updateVideoMiniatureFromExisting({
196 inputPath: thumbnailPhysicalFile.path,
198 type: ThumbnailType.MINIATURE,
199 automaticallyGenerated: false
206 async function processPreview (req: express.Request, video: MVideoThumbnail): Promise<MThumbnail> {
207 const previewField = req.files ? req.files['previewfile'] : undefined
209 const previewPhysicalFile = previewField[0]
211 return updateVideoMiniatureFromExisting({
212 inputPath: previewPhysicalFile.path,
214 type: ThumbnailType.PREVIEW,
215 automaticallyGenerated: false
222 async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
223 const torrentName = torrentfile.originalname
225 // Rename the torrent to a secured name
226 const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
227 await move(torrentfile.path, newTorrentPath, { overwrite: true })
228 torrentfile.path = newTorrentPath
230 const buf = await readFile(torrentfile.path)
231 const parsedTorrent = parseTorrent(buf) as Instance
233 if (parsedTorrent.files.length !== 1) {
237 type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
238 message: 'Torrents with only 1 file are supported.'
244 name: extractNameFromArray(parsedTorrent.name),
249 function processMagnetURI (body: VideoImportCreate) {
250 const magnetUri = body.magnetUri
251 const parsed = decode(magnetUri)
254 name: extractNameFromArray(parsed.name),
259 function extractNameFromArray (name: string | string[]) {
260 return isArray(name) ? name[0] : name