2021-08-27 07:32:44 -05:00
|
|
|
import express from 'express'
|
2022-08-10 02:53:39 -05:00
|
|
|
import { move, readFile } from 'fs-extra'
|
2021-08-27 07:32:44 -05:00
|
|
|
import { decode } from 'magnet-uri'
|
|
|
|
import parseTorrent, { Instance } from 'parse-torrent'
|
2020-09-17 03:00:46 -05:00
|
|
|
import { join } from 'path'
|
2022-10-31 05:45:08 -05:00
|
|
|
import { buildYoutubeDLImport, buildVideoFromImport, insertFromImportIntoDB, YoutubeDlImportError } from '@server/lib/video-pre-import'
|
2022-08-10 02:53:39 -05:00
|
|
|
import { MThumbnail, MVideoThumbnail } from '@server/types/models'
|
|
|
|
import { HttpStatusCode, ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState } from '@shared/models'
|
2020-09-17 03:00:46 -05:00
|
|
|
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
|
|
|
|
import { isArray } from '../../../helpers/custom-validators/misc'
|
2021-05-11 07:56:00 -05:00
|
|
|
import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
|
2020-09-17 03:00:46 -05:00
|
|
|
import { logger } from '../../../helpers/logger'
|
|
|
|
import { getSecureTorrentName } from '../../../helpers/utils'
|
|
|
|
import { CONFIG } from '../../../initializers/config'
|
|
|
|
import { MIMETYPES } from '../../../initializers/constants'
|
|
|
|
import { JobQueue } from '../../../lib/job-queue/job-queue'
|
2022-08-10 02:53:39 -05:00
|
|
|
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
|
2022-01-19 07:23:00 -06:00
|
|
|
import {
|
|
|
|
asyncMiddleware,
|
|
|
|
asyncRetryTransactionMiddleware,
|
|
|
|
authenticate,
|
|
|
|
videoImportAddValidator,
|
|
|
|
videoImportCancelValidator,
|
|
|
|
videoImportDeleteValidator
|
|
|
|
} from '../../../middlewares'
|
2018-08-02 08:34:09 -05:00
|
|
|
|
|
|
|
const auditLogger = auditLoggerFactory('video-imports')
|
|
|
|
const videoImportsRouter = express.Router()
|
|
|
|
|
|
|
|
const reqVideoFileImport = createReqFiles(
|
2018-08-07 02:54:36 -05:00
|
|
|
[ 'thumbnailfile', 'previewfile', 'torrentfile' ],
|
2022-03-04 03:57:36 -06:00
|
|
|
{ ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
|
2018-08-02 08:34:09 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
videoImportsRouter.post('/imports',
|
|
|
|
authenticate,
|
|
|
|
reqVideoFileImport,
|
|
|
|
asyncMiddleware(videoImportAddValidator),
|
2022-08-10 02:53:39 -05:00
|
|
|
asyncRetryTransactionMiddleware(handleVideoImport)
|
2018-08-02 08:34:09 -05:00
|
|
|
)
|
|
|
|
|
2022-01-19 07:23:00 -06:00
|
|
|
videoImportsRouter.post('/imports/:id/cancel',
|
|
|
|
authenticate,
|
|
|
|
asyncMiddleware(videoImportCancelValidator),
|
|
|
|
asyncRetryTransactionMiddleware(cancelVideoImport)
|
|
|
|
)
|
|
|
|
|
|
|
|
videoImportsRouter.delete('/imports/:id',
|
|
|
|
authenticate,
|
|
|
|
asyncMiddleware(videoImportDeleteValidator),
|
|
|
|
asyncRetryTransactionMiddleware(deleteVideoImport)
|
|
|
|
)
|
|
|
|
|
2018-08-02 08:34:09 -05:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
export {
|
|
|
|
videoImportsRouter
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
2022-01-19 07:23:00 -06:00
|
|
|
async function deleteVideoImport (req: express.Request, res: express.Response) {
|
|
|
|
const videoImport = res.locals.videoImport
|
|
|
|
|
|
|
|
await videoImport.destroy()
|
|
|
|
|
|
|
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function cancelVideoImport (req: express.Request, res: express.Response) {
|
|
|
|
const videoImport = res.locals.videoImport
|
|
|
|
|
|
|
|
videoImport.state = VideoImportState.CANCELLED
|
|
|
|
await videoImport.save()
|
|
|
|
|
|
|
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
|
|
|
}
|
|
|
|
|
2022-08-10 02:53:39 -05:00
|
|
|
function handleVideoImport (req: express.Request, res: express.Response) {
|
|
|
|
if (req.body.targetUrl) return handleYoutubeDlImport(req, res)
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2020-06-17 03:55:40 -05:00
|
|
|
const file = req.files?.['torrentfile']?.[0]
|
2022-08-10 02:53:39 -05:00
|
|
|
if (req.body.magnetUri || file) return handleTorrentImport(req, res, file)
|
2018-08-06 10:13:39 -05:00
|
|
|
}
|
|
|
|
|
2022-08-10 02:53:39 -05:00
|
|
|
async function handleTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
|
2018-08-06 10:13:39 -05:00
|
|
|
const body: VideoImportCreate = req.body
|
2018-08-07 03:07:53 -05:00
|
|
|
const user = res.locals.oauth.token.User
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2018-08-07 02:54:36 -05:00
|
|
|
let videoName: string
|
|
|
|
let torrentName: string
|
|
|
|
let magnetUri: string
|
|
|
|
|
|
|
|
if (torrentfile) {
|
2021-05-12 07:51:17 -05:00
|
|
|
const result = await processTorrentOrAbortRequest(req, res, torrentfile)
|
|
|
|
if (!result) return
|
2018-08-07 02:54:36 -05:00
|
|
|
|
2021-05-12 07:51:17 -05:00
|
|
|
videoName = result.name
|
|
|
|
torrentName = result.torrentName
|
2018-08-07 02:54:36 -05:00
|
|
|
} else {
|
2021-05-12 07:51:17 -05:00
|
|
|
const result = processMagnetURI(body)
|
|
|
|
magnetUri = result.magnetUri
|
|
|
|
videoName = result.name
|
2018-08-07 02:54:36 -05:00
|
|
|
}
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2022-08-10 02:53:39 -05:00
|
|
|
const video = await buildVideoFromImport({
|
|
|
|
channelId: res.locals.videoChannel.id,
|
|
|
|
importData: { name: videoName },
|
|
|
|
importDataOverride: body,
|
|
|
|
importType: 'torrent'
|
|
|
|
})
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2019-04-17 03:07:00 -05:00
|
|
|
const thumbnailModel = await processThumbnail(req, video)
|
|
|
|
const previewModel = await processPreview(req, video)
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2022-08-10 02:53:39 -05:00
|
|
|
const videoImport = await insertFromImportIntoDB({
|
2019-04-17 03:07:00 -05:00
|
|
|
video,
|
|
|
|
thumbnailModel,
|
|
|
|
previewModel,
|
|
|
|
videoChannel: res.locals.videoChannel,
|
2021-05-12 07:51:17 -05:00
|
|
|
tags: body.tags || undefined,
|
|
|
|
user,
|
|
|
|
videoImportAttributes: {
|
|
|
|
magnetUri,
|
|
|
|
torrentName,
|
|
|
|
state: VideoImportState.PENDING,
|
|
|
|
userId: user.id
|
|
|
|
}
|
2019-04-17 03:07:00 -05:00
|
|
|
})
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2022-08-10 02:53:39 -05:00
|
|
|
const payload: VideoImportPayload = {
|
2021-05-12 07:51:17 -05:00
|
|
|
type: torrentfile
|
2022-08-10 02:53:39 -05:00
|
|
|
? 'torrent-file'
|
|
|
|
: 'magnet-uri',
|
2018-08-06 10:13:39 -05:00
|
|
|
videoImportId: videoImport.id,
|
2022-08-10 02:53:39 -05:00
|
|
|
preventException: false
|
2018-08-06 10:13:39 -05:00
|
|
|
}
|
2022-08-08 08:48:17 -05:00
|
|
|
await JobQueue.Instance.createJob({ type: 'video-import', payload })
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2018-09-19 10:02:16 -05:00
|
|
|
auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
|
2018-08-06 10:13:39 -05:00
|
|
|
|
|
|
|
return res.json(videoImport.toFormattedJSON()).end()
|
|
|
|
}
|
|
|
|
|
2022-08-10 02:53:39 -05:00
|
|
|
function statusFromYtDlImportError (err: YoutubeDlImportError): number {
|
|
|
|
switch (err.code) {
|
|
|
|
case YoutubeDlImportError.CODE.NOT_ONLY_UNICAST_URL:
|
|
|
|
return HttpStatusCode.FORBIDDEN_403
|
|
|
|
|
|
|
|
case YoutubeDlImportError.CODE.FETCH_ERROR:
|
|
|
|
return HttpStatusCode.BAD_REQUEST_400
|
|
|
|
|
|
|
|
default:
|
|
|
|
return HttpStatusCode.INTERNAL_SERVER_ERROR_500
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function handleYoutubeDlImport (req: express.Request, res: express.Response) {
|
2018-08-02 08:34:09 -05:00
|
|
|
const body: VideoImportCreate = req.body
|
|
|
|
const targetUrl = body.targetUrl
|
2018-08-07 03:07:53 -05:00
|
|
|
const user = res.locals.oauth.token.User
|
2018-08-02 08:34:09 -05:00
|
|
|
|
|
|
|
try {
|
2022-08-10 02:53:39 -05:00
|
|
|
const { job, videoImport } = await buildYoutubeDLImport({
|
|
|
|
targetUrl,
|
|
|
|
channel: res.locals.videoChannel,
|
|
|
|
importDataOverride: body,
|
|
|
|
thumbnailFilePath: req.files?.['thumbnailfile']?.[0].path,
|
|
|
|
previewFilePath: req.files?.['previewfile']?.[0].path,
|
|
|
|
user
|
|
|
|
})
|
|
|
|
await JobQueue.Instance.createJob(job)
|
|
|
|
|
|
|
|
auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
|
|
|
|
|
|
|
|
return res.json(videoImport.toFormattedJSON()).end()
|
2018-08-02 08:34:09 -05:00
|
|
|
} catch (err) {
|
2022-08-10 02:53:39 -05:00
|
|
|
logger.error('An error occurred while importing the video %s. ', targetUrl, { err })
|
2018-08-02 08:34:09 -05:00
|
|
|
|
2021-05-31 18:36:53 -05:00
|
|
|
return res.fail({
|
2022-08-10 02:53:39 -05:00
|
|
|
message: err.message,
|
|
|
|
status: statusFromYtDlImportError(err),
|
2021-05-31 18:36:53 -05:00
|
|
|
data: {
|
|
|
|
targetUrl
|
|
|
|
}
|
|
|
|
})
|
2018-08-02 08:34:09 -05:00
|
|
|
}
|
2018-08-06 10:13:39 -05:00
|
|
|
}
|
|
|
|
|
2021-02-15 07:08:16 -06:00
|
|
|
async function processThumbnail (req: express.Request, video: MVideoThumbnail) {
|
2018-08-03 04:10:31 -05:00
|
|
|
const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
|
2018-08-02 08:34:09 -05:00
|
|
|
if (thumbnailField) {
|
2020-01-31 09:56:52 -06:00
|
|
|
const thumbnailPhysicalFile = thumbnailField[0]
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2021-06-04 02:19:01 -05:00
|
|
|
return updateVideoMiniatureFromExisting({
|
2020-09-17 03:00:46 -05:00
|
|
|
inputPath: thumbnailPhysicalFile.path,
|
|
|
|
video,
|
|
|
|
type: ThumbnailType.MINIATURE,
|
|
|
|
automaticallyGenerated: false
|
|
|
|
})
|
2018-08-02 08:34:09 -05:00
|
|
|
}
|
|
|
|
|
2019-04-17 03:07:00 -05:00
|
|
|
return undefined
|
2018-08-06 10:13:39 -05:00
|
|
|
}
|
|
|
|
|
2021-02-15 07:08:16 -06:00
|
|
|
async function processPreview (req: express.Request, video: MVideoThumbnail): Promise<MThumbnail> {
|
2018-08-03 04:10:31 -05:00
|
|
|
const previewField = req.files ? req.files['previewfile'] : undefined
|
2018-08-02 08:34:09 -05:00
|
|
|
if (previewField) {
|
|
|
|
const previewPhysicalFile = previewField[0]
|
2018-08-06 10:13:39 -05:00
|
|
|
|
2021-06-04 02:19:01 -05:00
|
|
|
return updateVideoMiniatureFromExisting({
|
2020-09-17 03:00:46 -05:00
|
|
|
inputPath: previewPhysicalFile.path,
|
|
|
|
video,
|
|
|
|
type: ThumbnailType.PREVIEW,
|
|
|
|
automaticallyGenerated: false
|
|
|
|
})
|
2018-08-02 08:34:09 -05:00
|
|
|
}
|
|
|
|
|
2019-04-17 03:07:00 -05:00
|
|
|
return undefined
|
2018-08-06 10:13:39 -05:00
|
|
|
}
|
|
|
|
|
2021-05-12 07:51:17 -05:00
|
|
|
async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
|
|
|
|
const torrentName = torrentfile.originalname
|
|
|
|
|
|
|
|
// Rename the torrent to a secured name
|
|
|
|
const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
|
|
|
|
await move(torrentfile.path, newTorrentPath, { overwrite: true })
|
|
|
|
torrentfile.path = newTorrentPath
|
|
|
|
|
|
|
|
const buf = await readFile(torrentfile.path)
|
2021-08-27 07:32:44 -05:00
|
|
|
const parsedTorrent = parseTorrent(buf) as Instance
|
2021-05-12 07:51:17 -05:00
|
|
|
|
|
|
|
if (parsedTorrent.files.length !== 1) {
|
|
|
|
cleanUpReqFiles(req)
|
|
|
|
|
2021-05-31 18:36:53 -05:00
|
|
|
res.fail({
|
2021-06-01 09:07:58 -05:00
|
|
|
type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
|
2021-05-31 18:36:53 -05:00
|
|
|
message: 'Torrents with only 1 file are supported.'
|
|
|
|
})
|
2021-05-12 07:51:17 -05:00
|
|
|
return undefined
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: extractNameFromArray(parsedTorrent.name),
|
|
|
|
torrentName
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function processMagnetURI (body: VideoImportCreate) {
|
|
|
|
const magnetUri = body.magnetUri
|
2021-08-27 07:32:44 -05:00
|
|
|
const parsed = decode(magnetUri)
|
2021-05-12 07:51:17 -05:00
|
|
|
|
|
|
|
return {
|
|
|
|
name: extractNameFromArray(parsed.name),
|
|
|
|
magnetUri
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function extractNameFromArray (name: string | string[]) {
|
|
|
|
return isArray(name) ? name[0] : name
|
|
|
|
}
|