]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/api/videos/import.ts
Channel sync (#5135)
[github/Chocobozzz/PeerTube.git] / server / controllers / api / videos / import.ts
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-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'
18 import {
19 asyncMiddleware,
20 asyncRetryTransactionMiddleware,
21 authenticate,
22 videoImportAddValidator,
23 videoImportCancelValidator,
24 videoImportDeleteValidator
25 } from '../../../middlewares'
26
27 const auditLogger = auditLoggerFactory('video-imports')
28 const videoImportsRouter = express.Router()
29
30 const reqVideoFileImport = createReqFiles(
31 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
32 { ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
33 )
34
35 videoImportsRouter.post('/imports',
36 authenticate,
37 reqVideoFileImport,
38 asyncMiddleware(videoImportAddValidator),
39 asyncRetryTransactionMiddleware(handleVideoImport)
40 )
41
42 videoImportsRouter.post('/imports/:id/cancel',
43 authenticate,
44 asyncMiddleware(videoImportCancelValidator),
45 asyncRetryTransactionMiddleware(cancelVideoImport)
46 )
47
48 videoImportsRouter.delete('/imports/:id',
49 authenticate,
50 asyncMiddleware(videoImportDeleteValidator),
51 asyncRetryTransactionMiddleware(deleteVideoImport)
52 )
53
54 // ---------------------------------------------------------------------------
55
56 export {
57 videoImportsRouter
58 }
59
60 // ---------------------------------------------------------------------------
61
62 async 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
70 async 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
79 function handleVideoImport (req: express.Request, res: express.Response) {
80 if (req.body.targetUrl) return handleYoutubeDlImport(req, res)
81
82 const file = req.files?.['torrentfile']?.[0]
83 if (req.body.magnetUri || file) return handleTorrentImport(req, res, file)
84 }
85
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
89
90 let videoName: string
91 let torrentName: string
92 let magnetUri: string
93
94 if (torrentfile) {
95 const result = await processTorrentOrAbortRequest(req, res, torrentfile)
96 if (!result) return
97
98 videoName = result.name
99 torrentName = result.torrentName
100 } else {
101 const result = processMagnetURI(body)
102 magnetUri = result.magnetUri
103 videoName = result.name
104 }
105
106 const video = await buildVideoFromImport({
107 channelId: res.locals.videoChannel.id,
108 importData: { name: videoName },
109 importDataOverride: body,
110 importType: 'torrent'
111 })
112
113 const thumbnailModel = await processThumbnail(req, video)
114 const previewModel = await processPreview(req, video)
115
116 const videoImport = await insertFromImportIntoDB({
117 video,
118 thumbnailModel,
119 previewModel,
120 videoChannel: res.locals.videoChannel,
121 tags: body.tags || undefined,
122 user,
123 videoImportAttributes: {
124 magnetUri,
125 torrentName,
126 state: VideoImportState.PENDING,
127 userId: user.id
128 }
129 })
130
131 const payload: VideoImportPayload = {
132 type: torrentfile
133 ? 'torrent-file'
134 : 'magnet-uri',
135 videoImportId: videoImport.id,
136 preventException: false
137 }
138 await JobQueue.Instance.createJob({ type: 'video-import', payload })
139
140 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
141
142 return res.json(videoImport.toFormattedJSON()).end()
143 }
144
145 function 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
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
162
163 try {
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()
177 } catch (err) {
178 logger.error('An error occurred while importing the video %s. ', targetUrl, { err })
179
180 return res.fail({
181 message: err.message,
182 status: statusFromYtDlImportError(err),
183 data: {
184 targetUrl
185 }
186 })
187 }
188 }
189
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]
194
195 return updateVideoMiniatureFromExisting({
196 inputPath: thumbnailPhysicalFile.path,
197 video,
198 type: ThumbnailType.MINIATURE,
199 automaticallyGenerated: false
200 })
201 }
202
203 return undefined
204 }
205
206 async function processPreview (req: express.Request, video: MVideoThumbnail): Promise<MThumbnail> {
207 const previewField = req.files ? req.files['previewfile'] : undefined
208 if (previewField) {
209 const previewPhysicalFile = previewField[0]
210
211 return updateVideoMiniatureFromExisting({
212 inputPath: previewPhysicalFile.path,
213 video,
214 type: ThumbnailType.PREVIEW,
215 automaticallyGenerated: false
216 })
217 }
218
219 return undefined
220 }
221
222 async 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)
231 const parsedTorrent = parseTorrent(buf) as Instance
232
233 if (parsedTorrent.files.length !== 1) {
234 cleanUpReqFiles(req)
235
236 res.fail({
237 type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
238 message: 'Torrents with only 1 file are supported.'
239 })
240 return undefined
241 }
242
243 return {
244 name: extractNameFromArray(parsedTorrent.name),
245 torrentName
246 }
247 }
248
249 function processMagnetURI (body: VideoImportCreate) {
250 const magnetUri = body.magnetUri
251 const parsed = decode(magnetUri)
252
253 return {
254 name: extractNameFromArray(parsed.name),
255 magnetUri
256 }
257 }
258
259 function extractNameFromArray (name: string | string[]) {
260 return isArray(name) ? name[0] : name
261 }