]>
Commit | Line | Data |
---|---|---|
fbad87b0 | 1 | import * as express from 'express' |
990b6a0b C |
2 | import * as magnetUtil from 'magnet-uri' |
3 | import 'multer' | |
993cef4b | 4 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' |
7e5f9f00 | 5 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' |
14e2014a | 6 | import { CONFIG, MIMETYPES, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE } from '../../../initializers' |
fbad87b0 C |
7 | import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' |
8 | import { createReqFiles } from '../../../helpers/express-utils' | |
9 | import { logger } from '../../../helpers/logger' | |
10 | import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' | |
11 | import { VideoModel } from '../../../models/video/video' | |
12 | import { getVideoActivityPubUrl } from '../../../lib/activitypub' | |
13 | import { TagModel } from '../../../models/video/tag' | |
14 | import { VideoImportModel } from '../../../models/video/video-import' | |
15 | import { JobQueue } from '../../../lib/job-queue/job-queue' | |
16 | import { processImage } from '../../../helpers/image-utils' | |
17 | import { join } from 'path' | |
ce33919c C |
18 | import { isArray } from '../../../helpers/custom-validators/misc' |
19 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | |
20 | import { VideoChannelModel } from '../../../models/video/video-channel' | |
21 | import * as Bluebird from 'bluebird' | |
990b6a0b | 22 | import * as parseTorrent from 'parse-torrent' |
990b6a0b | 23 | import { getSecureTorrentName } from '../../../helpers/utils' |
f481c4f9 | 24 | import { readFile, move } from 'fs-extra' |
fbad87b0 C |
25 | |
26 | const auditLogger = auditLoggerFactory('video-imports') | |
27 | const videoImportsRouter = express.Router() | |
28 | ||
29 | const reqVideoFileImport = createReqFiles( | |
990b6a0b | 30 | [ 'thumbnailfile', 'previewfile', 'torrentfile' ], |
14e2014a | 31 | Object.assign({}, MIMETYPES.TORRENT.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), |
fbad87b0 | 32 | { |
6040f87d C |
33 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
34 | previewfile: CONFIG.STORAGE.TMP_DIR, | |
35 | torrentfile: CONFIG.STORAGE.TMP_DIR | |
fbad87b0 C |
36 | } |
37 | ) | |
38 | ||
39 | videoImportsRouter.post('/imports', | |
40 | authenticate, | |
41 | reqVideoFileImport, | |
42 | asyncMiddleware(videoImportAddValidator), | |
43 | asyncRetryTransactionMiddleware(addVideoImport) | |
44 | ) | |
45 | ||
fbad87b0 C |
46 | // --------------------------------------------------------------------------- |
47 | ||
48 | export { | |
49 | videoImportsRouter | |
50 | } | |
51 | ||
52 | // --------------------------------------------------------------------------- | |
53 | ||
ce33919c C |
54 | function addVideoImport (req: express.Request, res: express.Response) { |
55 | if (req.body.targetUrl) return addYoutubeDLImport(req, res) | |
56 | ||
a84b8fa5 | 57 | const file = req.files && req.files['torrentfile'] ? req.files['torrentfile'][0] : undefined |
990b6a0b | 58 | if (req.body.magnetUri || file) return addTorrentImport(req, res, file) |
ce33919c C |
59 | } |
60 | ||
990b6a0b | 61 | async function addTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { |
ce33919c | 62 | const body: VideoImportCreate = req.body |
a84b8fa5 | 63 | const user = res.locals.oauth.token.User |
ce33919c | 64 | |
990b6a0b C |
65 | let videoName: string |
66 | let torrentName: string | |
67 | let magnetUri: string | |
68 | ||
69 | if (torrentfile) { | |
70 | torrentName = torrentfile.originalname | |
71 | ||
72 | // Rename the torrent to a secured name | |
73 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) | |
f481c4f9 | 74 | await move(torrentfile.path, newTorrentPath) |
990b6a0b C |
75 | torrentfile.path = newTorrentPath |
76 | ||
62689b94 | 77 | const buf = await readFile(torrentfile.path) |
990b6a0b C |
78 | const parsedTorrent = parseTorrent(buf) |
79 | ||
80 | videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[ 0 ] : parsedTorrent.name as string | |
81 | } else { | |
82 | magnetUri = body.magnetUri | |
83 | ||
84 | const parsed = magnetUtil.decode(magnetUri) | |
85 | videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string | |
86 | } | |
ce33919c | 87 | |
990b6a0b | 88 | const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) |
ce33919c C |
89 | |
90 | await processThumbnail(req, video) | |
91 | await processPreview(req, video) | |
92 | ||
3e17515e | 93 | const tags = body.tags || undefined |
ce33919c C |
94 | const videoImportAttributes = { |
95 | magnetUri, | |
990b6a0b | 96 | torrentName, |
a84b8fa5 C |
97 | state: VideoImportState.PENDING, |
98 | userId: user.id | |
ce33919c C |
99 | } |
100 | const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) | |
101 | ||
102 | // Create job to import the video | |
103 | const payload = { | |
990b6a0b | 104 | type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri', |
ce33919c C |
105 | videoImportId: videoImport.id, |
106 | magnetUri | |
107 | } | |
108 | await JobQueue.Instance.createJob({ type: 'video-import', payload }) | |
109 | ||
993cef4b | 110 | auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) |
ce33919c C |
111 | |
112 | return res.json(videoImport.toFormattedJSON()).end() | |
113 | } | |
114 | ||
115 | async function addYoutubeDLImport (req: express.Request, res: express.Response) { | |
fbad87b0 C |
116 | const body: VideoImportCreate = req.body |
117 | const targetUrl = body.targetUrl | |
a84b8fa5 | 118 | const user = res.locals.oauth.token.User |
fbad87b0 C |
119 | |
120 | let youtubeDLInfo: YoutubeDLInfo | |
121 | try { | |
122 | youtubeDLInfo = await getYoutubeDLInfo(targetUrl) | |
123 | } catch (err) { | |
124 | logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) | |
125 | ||
126 | return res.status(400).json({ | |
127 | error: 'Cannot fetch remote information of this URL.' | |
128 | }).end() | |
129 | } | |
130 | ||
ce33919c C |
131 | const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) |
132 | ||
133 | const downloadThumbnail = !await processThumbnail(req, video) | |
134 | const downloadPreview = !await processPreview(req, video) | |
135 | ||
136 | const tags = body.tags || youtubeDLInfo.tags | |
137 | const videoImportAttributes = { | |
138 | targetUrl, | |
a84b8fa5 C |
139 | state: VideoImportState.PENDING, |
140 | userId: user.id | |
ce33919c C |
141 | } |
142 | const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) | |
143 | ||
144 | // Create job to import the video | |
145 | const payload = { | |
146 | type: 'youtube-dl' as 'youtube-dl', | |
147 | videoImportId: videoImport.id, | |
148 | thumbnailUrl: youtubeDLInfo.thumbnailUrl, | |
149 | downloadThumbnail, | |
150 | downloadPreview | |
151 | } | |
152 | await JobQueue.Instance.createJob({ type: 'video-import', payload }) | |
153 | ||
993cef4b | 154 | auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) |
ce33919c C |
155 | |
156 | return res.json(videoImport.toFormattedJSON()).end() | |
157 | } | |
158 | ||
159 | function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) { | |
fbad87b0 | 160 | const videoData = { |
ce33919c | 161 | name: body.name || importData.name || 'Unknown name', |
fbad87b0 | 162 | remote: false, |
ce33919c C |
163 | category: body.category || importData.category, |
164 | licence: body.licence || importData.licence, | |
590fb506 | 165 | language: body.language || undefined, |
fbad87b0 C |
166 | commentsEnabled: body.commentsEnabled || true, |
167 | waitTranscoding: body.waitTranscoding || false, | |
168 | state: VideoState.TO_IMPORT, | |
ce33919c C |
169 | nsfw: body.nsfw || importData.nsfw || false, |
170 | description: body.description || importData.description, | |
fbad87b0 C |
171 | support: body.support || null, |
172 | privacy: body.privacy || VideoPrivacy.PRIVATE, | |
173 | duration: 0, // duration will be set by the import job | |
ce33919c | 174 | channelId: channelId |
fbad87b0 C |
175 | } |
176 | const video = new VideoModel(videoData) | |
177 | video.url = getVideoActivityPubUrl(video) | |
178 | ||
ce33919c C |
179 | return video |
180 | } | |
181 | ||
182 | async function processThumbnail (req: express.Request, video: VideoModel) { | |
5d08a6a7 | 183 | const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined |
fbad87b0 C |
184 | if (thumbnailField) { |
185 | const thumbnailPhysicalFile = thumbnailField[ 0 ] | |
186 | await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE) | |
ce33919c C |
187 | |
188 | return true | |
fbad87b0 C |
189 | } |
190 | ||
ce33919c C |
191 | return false |
192 | } | |
193 | ||
194 | async function processPreview (req: express.Request, video: VideoModel) { | |
5d08a6a7 | 195 | const previewField = req.files ? req.files['previewfile'] : undefined |
fbad87b0 C |
196 | if (previewField) { |
197 | const previewPhysicalFile = previewField[0] | |
198 | await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE) | |
ce33919c C |
199 | |
200 | return true | |
fbad87b0 C |
201 | } |
202 | ||
ce33919c C |
203 | return false |
204 | } | |
205 | ||
206 | function insertIntoDB ( | |
207 | video: VideoModel, | |
208 | videoChannel: VideoChannelModel, | |
209 | tags: string[], | |
210 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> | |
211 | ): Bluebird<VideoImportModel> { | |
212 | return sequelizeTypescript.transaction(async t => { | |
fbad87b0 C |
213 | const sequelizeOptions = { transaction: t } |
214 | ||
215 | // Save video object in database | |
216 | const videoCreated = await video.save(sequelizeOptions) | |
ce33919c | 217 | videoCreated.VideoChannel = videoChannel |
fbad87b0 C |
218 | |
219 | // Set tags to the video | |
3e17515e | 220 | if (tags) { |
590fb506 | 221 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
fbad87b0 C |
222 | |
223 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | |
224 | videoCreated.Tags = tagInstances | |
3e17515e C |
225 | } else { |
226 | videoCreated.Tags = [] | |
fbad87b0 C |
227 | } |
228 | ||
229 | // Create video import object in database | |
ce33919c C |
230 | const videoImport = await VideoImportModel.create( |
231 | Object.assign({ videoId: videoCreated.id }, videoImportAttributes), | |
232 | sequelizeOptions | |
233 | ) | |
fbad87b0 C |
234 | videoImport.Video = videoCreated |
235 | ||
236 | return videoImport | |
237 | }) | |
fbad87b0 | 238 | } |