]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/api/videos/import.ts
Fix old DB enum names
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos / import.ts
CommitLineData
41fb13c3 1import express from 'express'
2a491182 2import { move, readFile } from 'fs-extra'
41fb13c3
C
3import { decode } from 'magnet-uri'
4import parseTorrent, { Instance } from 'parse-torrent'
1ef65f4c 5import { join } from 'path'
2a491182
F
6import { buildYoutubeDLImport, buildVideoFromImport, insertFromImportIntoDB, YoutubeDlImportError } from '@server/lib/video-import'
7import { MThumbnail, MVideoThumbnail } from '@server/types/models'
8import { HttpStatusCode, ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState } from '@shared/models'
1ef65f4c 9import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
1ef65f4c 10import { isArray } from '../../../helpers/custom-validators/misc'
32985a0a 11import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
1ef65f4c
C
12import { logger } from '../../../helpers/logger'
13import { getSecureTorrentName } from '../../../helpers/utils'
1ef65f4c
C
14import { CONFIG } from '../../../initializers/config'
15import { MIMETYPES } from '../../../initializers/constants'
1ef65f4c 16import { JobQueue } from '../../../lib/job-queue/job-queue'
2a491182 17import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
419b520c
C
18import {
19 asyncMiddleware,
20 asyncRetryTransactionMiddleware,
21 authenticate,
22 videoImportAddValidator,
23 videoImportCancelValidator,
24 videoImportDeleteValidator
25} from '../../../middlewares'
fbad87b0
C
26
27const auditLogger = auditLoggerFactory('video-imports')
28const videoImportsRouter = express.Router()
29
30const reqVideoFileImport = createReqFiles(
990b6a0b 31 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
d3d3deaa 32 { ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
fbad87b0
C
33)
34
35videoImportsRouter.post('/imports',
36 authenticate,
37 reqVideoFileImport,
38 asyncMiddleware(videoImportAddValidator),
2a491182 39 asyncRetryTransactionMiddleware(handleVideoImport)
fbad87b0
C
40)
41
419b520c
C
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
fbad87b0
C
54// ---------------------------------------------------------------------------
55
56export {
57 videoImportsRouter
58}
59
60// ---------------------------------------------------------------------------
61
419b520c
C
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
2a491182
F
79function handleVideoImport (req: express.Request, res: express.Response) {
80 if (req.body.targetUrl) return handleYoutubeDlImport(req, res)
ce33919c 81
faa9d434 82 const file = req.files?.['torrentfile']?.[0]
2a491182 83 if (req.body.magnetUri || file) return handleTorrentImport(req, res, file)
ce33919c
C
84}
85
2a491182 86async function handleTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
ce33919c 87 const body: VideoImportCreate = req.body
a84b8fa5 88 const user = res.locals.oauth.token.User
ce33919c 89
990b6a0b
C
90 let videoName: string
91 let torrentName: string
92 let magnetUri: string
93
94 if (torrentfile) {
c158a5fa
C
95 const result = await processTorrentOrAbortRequest(req, res, torrentfile)
96 if (!result) return
990b6a0b 97
c158a5fa
C
98 videoName = result.name
99 torrentName = result.torrentName
990b6a0b 100 } else {
c158a5fa
C
101 const result = processMagnetURI(body)
102 magnetUri = result.magnetUri
103 videoName = result.name
990b6a0b 104 }
ce33919c 105
2a491182
F
106 const video = await buildVideoFromImport({
107 channelId: res.locals.videoChannel.id,
108 importData: { name: videoName },
109 importDataOverride: body,
110 importType: 'torrent'
111 })
ce33919c 112
e8bafea3
C
113 const thumbnailModel = await processThumbnail(req, video)
114 const previewModel = await processPreview(req, video)
ce33919c 115
2a491182 116 const videoImport = await insertFromImportIntoDB({
e8bafea3
C
117 video,
118 thumbnailModel,
119 previewModel,
120 videoChannel: res.locals.videoChannel,
c158a5fa
C
121 tags: body.tags || undefined,
122 user,
123 videoImportAttributes: {
124 magnetUri,
125 torrentName,
126 state: VideoImportState.PENDING,
127 userId: user.id
128 }
e8bafea3 129 })
ce33919c 130
2a491182 131 const payload: VideoImportPayload = {
c158a5fa 132 type: torrentfile
2a491182
F
133 ? 'torrent-file'
134 : 'magnet-uri',
ce33919c 135 videoImportId: videoImport.id,
2a491182 136 preventException: false
ce33919c 137 }
bd911b54 138 await JobQueue.Instance.createJob({ type: 'video-import', payload })
ce33919c 139
993cef4b 140 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
ce33919c
C
141
142 return res.json(videoImport.toFormattedJSON()).end()
143}
144
2a491182
F
145function statusFromYtDlImportError (err: YoutubeDlImportError): number {
146 switch (err.code) {
147 case YoutubeDlImportError.CODE.NOT_ONLY_UNICAST_URL:
148 return HttpStatusCode.FORBIDDEN_403
149
150 case YoutubeDlImportError.CODE.FETCH_ERROR:
151 return HttpStatusCode.BAD_REQUEST_400
152
153 default:
154 return HttpStatusCode.INTERNAL_SERVER_ERROR_500
155 }
156}
157
158async function handleYoutubeDlImport (req: express.Request, res: express.Response) {
fbad87b0
C
159 const body: VideoImportCreate = req.body
160 const targetUrl = body.targetUrl
a84b8fa5 161 const user = res.locals.oauth.token.User
fbad87b0 162
fbad87b0 163 try {
2a491182
F
164 const { job, videoImport } = await buildYoutubeDLImport({
165 targetUrl,
166 channel: res.locals.videoChannel,
167 importDataOverride: body,
168 thumbnailFilePath: req.files?.['thumbnailfile']?.[0].path,
169 previewFilePath: req.files?.['previewfile']?.[0].path,
170 user
171 })
172 await JobQueue.Instance.createJob(job)
173
174 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
175
176 return res.json(videoImport.toFormattedJSON()).end()
fbad87b0 177 } catch (err) {
2a491182 178 logger.error('An error occurred while importing the video %s. ', targetUrl, { err })
fbad87b0 179
76148b27 180 return res.fail({
2a491182
F
181 message: err.message,
182 status: statusFromYtDlImportError(err),
76148b27
RK
183 data: {
184 targetUrl
185 }
186 })
fbad87b0 187 }
ce33919c
C
188}
189
6302d599 190async function processThumbnail (req: express.Request, video: MVideoThumbnail) {
5d08a6a7 191 const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
fbad87b0 192 if (thumbnailField) {
a1587156 193 const thumbnailPhysicalFile = thumbnailField[0]
ce33919c 194
91f8f8db 195 return updateVideoMiniatureFromExisting({
1ef65f4c
C
196 inputPath: thumbnailPhysicalFile.path,
197 video,
198 type: ThumbnailType.MINIATURE,
199 automaticallyGenerated: false
200 })
fbad87b0
C
201 }
202
e8bafea3 203 return undefined
ce33919c
C
204}
205
6302d599 206async function processPreview (req: express.Request, video: MVideoThumbnail): Promise<MThumbnail> {
5d08a6a7 207 const previewField = req.files ? req.files['previewfile'] : undefined
fbad87b0
C
208 if (previewField) {
209 const previewPhysicalFile = previewField[0]
ce33919c 210
91f8f8db 211 return updateVideoMiniatureFromExisting({
1ef65f4c
C
212 inputPath: previewPhysicalFile.path,
213 video,
214 type: ThumbnailType.PREVIEW,
215 automaticallyGenerated: false
216 })
fbad87b0
C
217 }
218
e8bafea3 219 return undefined
ce33919c
C
220}
221
c158a5fa
C
222async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
223 const torrentName = torrentfile.originalname
224
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
229
230 const buf = await readFile(torrentfile.path)
41fb13c3 231 const parsedTorrent = parseTorrent(buf) as Instance
c158a5fa
C
232
233 if (parsedTorrent.files.length !== 1) {
234 cleanUpReqFiles(req)
235
76148b27 236 res.fail({
3866ea02 237 type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
76148b27
RK
238 message: 'Torrents with only 1 file are supported.'
239 })
c158a5fa
C
240 return undefined
241 }
242
243 return {
244 name: extractNameFromArray(parsedTorrent.name),
245 torrentName
246 }
247}
248
249function processMagnetURI (body: VideoImportCreate) {
250 const magnetUri = body.magnetUri
41fb13c3 251 const parsed = decode(magnetUri)
c158a5fa
C
252
253 return {
254 name: extractNameFromArray(parsed.name),
255 magnetUri
256 }
257}
258
259function extractNameFromArray (name: string | string[]) {
260 return isArray(name) ? name[0] : name
261}