diff options
-rw-r--r-- | client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | 2 | ||||
-rw-r--r-- | client/src/app/+my-library/my-video-imports/my-video-imports.component.html | 6 | ||||
-rw-r--r-- | server/controllers/api/config.ts | 12 | ||||
-rw-r--r-- | server/controllers/api/videos/import.ts | 12 | ||||
-rw-r--r-- | server/controllers/static.ts | 3 | ||||
-rw-r--r-- | server/helpers/youtube-dl.ts | 53 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-import.ts | 6 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 13 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-imports.ts | 2 | ||||
-rw-r--r-- | server/tests/api/videos/video-imports.ts | 75 | ||||
-rw-r--r-- | server/tests/api/videos/video-transcoder.ts | 36 | ||||
-rw-r--r-- | server/tools/peertube-import-videos.ts | 4 | ||||
-rw-r--r-- | shared/core-utils/miscs/http-error-codes.ts | 8 | ||||
-rw-r--r-- | shared/extra-utils/videos/video-imports.ts | 24 | ||||
-rw-r--r-- | shared/models/server/job.model.ts | 1 |
15 files changed, 196 insertions, 61 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 72b7ceb73..09e7e96ac 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | |||
@@ -753,7 +753,7 @@ | |||
753 | i18n-labelText labelText="Allow additional extensions" | 753 | i18n-labelText labelText="Allow additional extensions" |
754 | > | 754 | > |
755 | <ng-container ngProjectAs="description"> | 755 | <ng-container ngProjectAs="description"> |
756 | <span i18n>Allows users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, m2ts, .mxf, or .nut videos.</span> | 756 | <span i18n>Allows users to upload .mkv, .mov, .avi, .wmv, .flv, .f4v, .3g2, .3gp, .mts, .m2ts, .mxf, or .nut videos.</span> |
757 | </ng-container> | 757 | </ng-container> |
758 | </my-peertube-checkbox> | 758 | </my-peertube-checkbox> |
759 | </div> | 759 | </div> |
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html index 1d3a45f76..9ae85c0ca 100644 --- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html | |||
@@ -23,8 +23,8 @@ | |||
23 | 23 | ||
24 | <ng-template pTemplate="body" let-expanded="expanded" let-videoImport> | 24 | <ng-template pTemplate="body" let-expanded="expanded" let-videoImport> |
25 | <tr> | 25 | <tr> |
26 | <td class="expand-cell"> | 26 | <td class="expand-cell c-hand" [pRowToggler]="videoImport" i18n-ngbTooltip ngbTooltip="See the error" placement="top-left" container="body"> |
27 | <span *ngIf="videoImport.error" class="expander" [pRowToggler]="videoImport" i18n-ngbTooltip ngbTooltip="See the error"> | 27 | <span *ngIf="videoImport.error" class="expander"> |
28 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | 28 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> |
29 | </span> | 29 | </span> |
30 | </td> | 30 | </td> |
@@ -51,7 +51,7 @@ | |||
51 | </td> | 51 | </td> |
52 | 52 | ||
53 | <td> | 53 | <td> |
54 | <span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state)"> | 54 | <span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state.id)"> |
55 | {{ videoImport.state.label }} | 55 | {{ videoImport.state.label }} |
56 | </span> | 56 | </span> |
57 | </td> | 57 | </td> |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 8bc3123bf..44f3d3ef7 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -10,6 +10,7 @@ import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '.. | |||
10 | import { objectConverter } from '../../helpers/core-utils' | 10 | import { objectConverter } from '../../helpers/core-utils' |
11 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' | 11 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' |
12 | import { getServerCommit } from '../../helpers/utils' | 12 | import { getServerCommit } from '../../helpers/utils' |
13 | import { getEnabledResolutions } from '../../lib/video-transcoding' | ||
13 | import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config' | 14 | import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config' |
14 | import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants' | 15 | import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants' |
15 | import { ClientHtml } from '../../lib/client-html' | 16 | import { ClientHtml } from '../../lib/client-html' |
@@ -285,16 +286,6 @@ function getRegisteredThemes () { | |||
285 | })) | 286 | })) |
286 | } | 287 | } |
287 | 288 | ||
288 | function getEnabledResolutions (type: 'vod' | 'live') { | ||
289 | const transcoding = type === 'vod' | ||
290 | ? CONFIG.TRANSCODING | ||
291 | : CONFIG.LIVE.TRANSCODING | ||
292 | |||
293 | return Object.keys(transcoding.RESOLUTIONS) | ||
294 | .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true) | ||
295 | .map(r => parseInt(r, 10)) | ||
296 | } | ||
297 | |||
298 | function getRegisteredPlugins () { | 289 | function getRegisteredPlugins () { |
299 | return PluginManager.Instance.getRegisteredPlugins() | 290 | return PluginManager.Instance.getRegisteredPlugins() |
300 | .map(p => ({ | 291 | .map(p => ({ |
@@ -345,7 +336,6 @@ function getExternalAuthsPlugins () { | |||
345 | 336 | ||
346 | export { | 337 | export { |
347 | configRouter, | 338 | configRouter, |
348 | getEnabledResolutions, | ||
349 | getRegisteredPlugins, | 339 | getRegisteredPlugins, |
350 | getRegisteredThemes | 340 | getRegisteredThemes |
351 | } | 341 | } |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 82e084c54..9702e219a 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -146,9 +146,10 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
146 | } catch (err) { | 146 | } catch (err) { |
147 | logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) | 147 | logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) |
148 | 148 | ||
149 | return res.status(HttpStatusCode.BAD_REQUEST_400).json({ | 149 | return res.status(HttpStatusCode.BAD_REQUEST_400) |
150 | error: 'Cannot fetch remote information of this URL.' | 150 | .json({ |
151 | }).end() | 151 | error: 'Cannot fetch remote information of this URL.' |
152 | }) | ||
152 | } | 153 | } |
153 | 154 | ||
154 | const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) | 155 | const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) |
@@ -219,9 +220,8 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
219 | videoImportId: videoImport.id, | 220 | videoImportId: videoImport.id, |
220 | generateThumbnail: !thumbnailModel, | 221 | generateThumbnail: !thumbnailModel, |
221 | generatePreview: !previewModel, | 222 | generatePreview: !previewModel, |
222 | fileExt: youtubeDLInfo.fileExt | 223 | fileExt: `.${youtubeDLInfo.ext || 'mp4'}`, |
223 | ? `.${youtubeDLInfo.fileExt}` | 224 | mergeExt: youtubeDLInfo.mergeExt ? `.${youtubeDLInfo.mergeExt}` : '' |
224 | : '.mp4' | ||
225 | } | 225 | } |
226 | await JobQueue.Instance.createJobWithPromise({ type: 'video-import', payload }) | 226 | await JobQueue.Instance.createJobWithPromise({ type: 'video-import', payload }) |
227 | 227 | ||
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index bdb9c3041..a7b28704c 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -19,13 +19,14 @@ import { VideoCommentModel } from '../models/video/video-comment' | |||
19 | import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' | 19 | import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' |
20 | import { join } from 'path' | 20 | import { join } from 'path' |
21 | import { root } from '../helpers/core-utils' | 21 | import { root } from '../helpers/core-utils' |
22 | import { getEnabledResolutions } from '../lib/video-transcoding' | ||
22 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 23 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
23 | import { getPreview, getVideoCaption } from './lazy-static' | 24 | import { getPreview, getVideoCaption } from './lazy-static' |
24 | import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type' | 25 | import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type' |
25 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | 26 | import { MVideoFile, MVideoFullLight } from '@server/types/models' |
26 | import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths' | 27 | import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths' |
27 | import { getThemeOrDefault } from '../lib/plugins/theme-utils' | 28 | import { getThemeOrDefault } from '../lib/plugins/theme-utils' |
28 | import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config' | 29 | import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config' |
29 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 30 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
30 | import { serveIndexHTML } from '@server/lib/client-html' | 31 | import { serveIndexHTML } from '@server/lib/client-html' |
31 | 32 | ||
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 74e5f896c..ebb788e8e 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' | 1 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' |
2 | import { logger } from './logger' | 2 | import { logger } from './logger' |
3 | import { generateVideoImportTmpPath } from './utils' | 3 | import { generateVideoImportTmpPath } from './utils' |
4 | import { getEnabledResolutions } from '../lib/video-transcoding' | ||
4 | import { join } from 'path' | 5 | import { join } from 'path' |
5 | import { peertubeTruncate, root } from './core-utils' | 6 | import { peertubeTruncate, root } from './core-utils' |
6 | import { ensureDir, remove, writeFile } from 'fs-extra' | 7 | import { ensureDir, remove, writeFile } from 'fs-extra' |
@@ -8,6 +9,7 @@ import * as request from 'request' | |||
8 | import { createWriteStream } from 'fs' | 9 | import { createWriteStream } from 'fs' |
9 | import { CONFIG } from '@server/initializers/config' | 10 | import { CONFIG } from '@server/initializers/config' |
10 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 11 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
12 | import { VideoResolution } from '../../shared/models/videos' | ||
11 | 13 | ||
12 | export type YoutubeDLInfo = { | 14 | export type YoutubeDLInfo = { |
13 | name?: string | 15 | name?: string |
@@ -18,7 +20,8 @@ export type YoutubeDLInfo = { | |||
18 | nsfw?: boolean | 20 | nsfw?: boolean |
19 | tags?: string[] | 21 | tags?: string[] |
20 | thumbnailUrl?: string | 22 | thumbnailUrl?: string |
21 | fileExt?: string | 23 | ext?: string |
24 | mergeExt?: string | ||
22 | originallyPublishedAt?: Date | 25 | originallyPublishedAt?: Date |
23 | } | 26 | } |
24 | 27 | ||
@@ -41,12 +44,21 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> | |||
41 | } | 44 | } |
42 | 45 | ||
43 | args = wrapWithProxyOptions(args) | 46 | args = wrapWithProxyOptions(args) |
47 | args = [ '-f', getYoutubeDLVideoFormat() ].concat(args) | ||
44 | 48 | ||
45 | safeGetYoutubeDL() | 49 | safeGetYoutubeDL() |
46 | .then(youtubeDL => { | 50 | .then(youtubeDL => { |
47 | youtubeDL.getInfo(url, args, processOptions, (err, info) => { | 51 | youtubeDL.getInfo(url, args, processOptions, (err, info) => { |
48 | if (err) return rej(err) | 52 | if (err) return rej(err) |
49 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) | 53 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) |
54 | if (info.format_id?.includes('+')) { | ||
55 | // this is a merge format and its extension will be appended | ||
56 | if (info.ext === 'mp4') { | ||
57 | info.mergeExt = 'mp4' | ||
58 | } else { | ||
59 | info.mergeExt = 'mkv' | ||
60 | } | ||
61 | } | ||
50 | 62 | ||
51 | const obj = buildVideoInfo(normalizeObject(info)) | 63 | const obj = buildVideoInfo(normalizeObject(info)) |
52 | if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' | 64 | if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' |
@@ -92,13 +104,40 @@ function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> { | |||
92 | }) | 104 | }) |
93 | } | 105 | } |
94 | 106 | ||
95 | function downloadYoutubeDLVideo (url: string, extension: string, timeout: number) { | 107 | function getYoutubeDLVideoFormat () { |
108 | /** | ||
109 | * list of format selectors in order or preference | ||
110 | * see https://github.com/ytdl-org/youtube-dl#format-selection | ||
111 | * | ||
112 | * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope | ||
113 | * of being able to do a "quick-transcode" | ||
114 | * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9) | ||
115 | * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback | ||
116 | * | ||
117 | * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499 | ||
118 | **/ | ||
119 | const enabledResolutions = getEnabledResolutions('vod') | ||
120 | const resolution = enabledResolutions.length === 0 | ||
121 | ? VideoResolution.H_720P | ||
122 | : Math.max(...enabledResolutions) | ||
123 | |||
124 | return [ | ||
125 | `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1 | ||
126 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2 | ||
127 | `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3 | ||
128 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`, | ||
129 | 'best[vcodec!*=av01][vcodec!*=vp9.2]' // case fallback | ||
130 | ].join('/') | ||
131 | } | ||
132 | |||
133 | function downloadYoutubeDLVideo (url: string, extension: string, timeout: number, mergeExtension?: string) { | ||
96 | const path = generateVideoImportTmpPath(url, extension) | 134 | const path = generateVideoImportTmpPath(url, extension) |
135 | const finalPath = mergeExtension ? path.replace(new RegExp(`${extension}$`), mergeExtension) : path | ||
97 | let timer | 136 | let timer |
98 | 137 | ||
99 | logger.info('Importing youtubeDL video %s to %s', url, path) | 138 | logger.info('Importing youtubeDL video %s to %s', url, finalPath) |
100 | 139 | ||
101 | let options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] | 140 | let options = [ '-f', getYoutubeDLVideoFormat(), '-o', path ] |
102 | options = wrapWithProxyOptions(options) | 141 | options = wrapWithProxyOptions(options) |
103 | 142 | ||
104 | if (process.env.FFMPEG_PATH) { | 143 | if (process.env.FFMPEG_PATH) { |
@@ -118,7 +157,7 @@ function downloadYoutubeDLVideo (url: string, extension: string, timeout: number | |||
118 | return rej(err) | 157 | return rej(err) |
119 | } | 158 | } |
120 | 159 | ||
121 | return res(path) | 160 | return res(finalPath) |
122 | }) | 161 | }) |
123 | 162 | ||
124 | timer = setTimeout(() => { | 163 | timer = setTimeout(() => { |
@@ -236,6 +275,7 @@ function buildOriginallyPublishedAt (obj: any) { | |||
236 | 275 | ||
237 | export { | 276 | export { |
238 | updateYoutubeDLBinary, | 277 | updateYoutubeDLBinary, |
278 | getYoutubeDLVideoFormat, | ||
239 | downloadYoutubeDLVideo, | 279 | downloadYoutubeDLVideo, |
240 | getYoutubeDLSubs, | 280 | getYoutubeDLSubs, |
241 | getYoutubeDLInfo, | 281 | getYoutubeDLInfo, |
@@ -275,7 +315,8 @@ function buildVideoInfo (obj: any): YoutubeDLInfo { | |||
275 | tags: getTags(obj.tags), | 315 | tags: getTags(obj.tags), |
276 | thumbnailUrl: obj.thumbnail || undefined, | 316 | thumbnailUrl: obj.thumbnail || undefined, |
277 | originallyPublishedAt: buildOriginallyPublishedAt(obj), | 317 | originallyPublishedAt: buildOriginallyPublishedAt(obj), |
278 | fileExt: obj.ext | 318 | ext: obj.ext, |
319 | mergeExt: obj.mergeExt | ||
279 | } | 320 | } |
280 | } | 321 | } |
281 | 322 | ||
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 5a82a8d2b..db3112418 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -79,7 +79,11 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub | |||
79 | generatePreview: payload.generatePreview | 79 | generatePreview: payload.generatePreview |
80 | } | 80 | } |
81 | 81 | ||
82 | return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options) | 82 | return processFile( |
83 | () => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT, payload.mergeExt), | ||
84 | videoImport, | ||
85 | options | ||
86 | ) | ||
83 | } | 87 | } |
84 | 88 | ||
85 | async function getVideoImportOrDie (videoImportId: number) { | 89 | async function getVideoImportOrDie (videoImportId: number) { |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 078e85acf..a6b79eaea 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -201,6 +201,16 @@ function generateHlsPlaylist (options: { | |||
201 | }) | 201 | }) |
202 | } | 202 | } |
203 | 203 | ||
204 | function getEnabledResolutions (type: 'vod' | 'live') { | ||
205 | const transcoding = type === 'vod' | ||
206 | ? CONFIG.TRANSCODING | ||
207 | : CONFIG.LIVE.TRANSCODING | ||
208 | |||
209 | return Object.keys(transcoding.RESOLUTIONS) | ||
210 | .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true) | ||
211 | .map(r => parseInt(r, 10)) | ||
212 | } | ||
213 | |||
204 | // --------------------------------------------------------------------------- | 214 | // --------------------------------------------------------------------------- |
205 | 215 | ||
206 | export { | 216 | export { |
@@ -208,7 +218,8 @@ export { | |||
208 | generateHlsPlaylistFromTS, | 218 | generateHlsPlaylistFromTS, |
209 | optimizeOriginalVideofile, | 219 | optimizeOriginalVideofile, |
210 | transcodeNewResolution, | 220 | transcodeNewResolution, |
211 | mergeAudioVideofile | 221 | mergeAudioVideofile, |
222 | getEnabledResolutions | ||
212 | } | 223 | } |
213 | 224 | ||
214 | // --------------------------------------------------------------------------- | 225 | // --------------------------------------------------------------------------- |
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index 0d41933a6..c53af3861 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -43,7 +43,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
43 | 43 | ||
44 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 44 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
45 | 45 | ||
46 | if (req.body.targetUrl && CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true) { | 46 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { |
47 | cleanUpReqFiles(req) | 47 | cleanUpReqFiles(req) |
48 | return res.status(HttpStatusCode.CONFLICT_409) | 48 | return res.status(HttpStatusCode.CONFLICT_409) |
49 | .json({ error: 'HTTP import is not enabled on this instance.' }) | 49 | .json({ error: 'HTTP import is not enabled on this instance.' }) |
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index 8d19a4274..61e7a81ee 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts | |||
@@ -14,12 +14,19 @@ import { | |||
14 | listVideoCaptions, | 14 | listVideoCaptions, |
15 | ServerInfo, | 15 | ServerInfo, |
16 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
17 | testCaptionFile | 17 | testCaptionFile, |
18 | updateCustomSubConfig | ||
18 | } from '../../../../shared/extra-utils' | 19 | } from '../../../../shared/extra-utils' |
19 | import { areHttpImportTestsDisabled, testImage } from '../../../../shared/extra-utils/miscs/miscs' | 20 | import { areHttpImportTestsDisabled, testImage } from '../../../../shared/extra-utils/miscs/miscs' |
20 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 21 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
21 | import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' | 22 | import { |
22 | import { VideoCaption, VideoDetails, VideoImport, VideoPrivacy } from '../../../../shared/models/videos' | 23 | getMagnetURI, |
24 | getMyVideoImports, | ||
25 | getYoutubeHDRVideoUrl, | ||
26 | getYoutubeVideoUrl, | ||
27 | importVideo | ||
28 | } from '../../../../shared/extra-utils/videos/video-imports' | ||
29 | import { VideoCaption, VideoDetails, VideoImport, VideoPrivacy, VideoResolution } from '../../../../shared/models/videos' | ||
23 | 30 | ||
24 | const expect = chai.expect | 31 | const expect = chai.expect |
25 | 32 | ||
@@ -90,7 +97,7 @@ describe('Test video imports', function () { | |||
90 | } | 97 | } |
91 | 98 | ||
92 | before(async function () { | 99 | before(async function () { |
93 | this.timeout(30000) | 100 | this.timeout(30_000) |
94 | 101 | ||
95 | // Run servers | 102 | // Run servers |
96 | servers = await flushAndRunMultipleServers(2) | 103 | servers = await flushAndRunMultipleServers(2) |
@@ -111,7 +118,7 @@ describe('Test video imports', function () { | |||
111 | }) | 118 | }) |
112 | 119 | ||
113 | it('Should import videos on server 1', async function () { | 120 | it('Should import videos on server 1', async function () { |
114 | this.timeout(60000) | 121 | this.timeout(60_000) |
115 | 122 | ||
116 | const baseAttributes = { | 123 | const baseAttributes = { |
117 | channelId: channelIdServer1, | 124 | channelId: channelIdServer1, |
@@ -223,7 +230,7 @@ Ajouter un sous-titre est vraiment facile`) | |||
223 | }) | 230 | }) |
224 | 231 | ||
225 | it('Should have the video listed on the two instances', async function () { | 232 | it('Should have the video listed on the two instances', async function () { |
226 | this.timeout(120000) | 233 | this.timeout(120_000) |
227 | 234 | ||
228 | await waitJobs(servers) | 235 | await waitJobs(servers) |
229 | 236 | ||
@@ -238,7 +245,7 @@ Ajouter un sous-titre est vraiment facile`) | |||
238 | }) | 245 | }) |
239 | 246 | ||
240 | it('Should import a video on server 2 with some fields', async function () { | 247 | it('Should import a video on server 2 with some fields', async function () { |
241 | this.timeout(60000) | 248 | this.timeout(60_000) |
242 | 249 | ||
243 | const attributes = { | 250 | const attributes = { |
244 | targetUrl: getYoutubeVideoUrl(), | 251 | targetUrl: getYoutubeVideoUrl(), |
@@ -256,7 +263,7 @@ Ajouter un sous-titre est vraiment facile`) | |||
256 | }) | 263 | }) |
257 | 264 | ||
258 | it('Should have the videos listed on the two instances', async function () { | 265 | it('Should have the videos listed on the two instances', async function () { |
259 | this.timeout(120000) | 266 | this.timeout(120_000) |
260 | 267 | ||
261 | await waitJobs(servers) | 268 | await waitJobs(servers) |
262 | 269 | ||
@@ -273,7 +280,7 @@ Ajouter un sous-titre est vraiment facile`) | |||
273 | }) | 280 | }) |
274 | 281 | ||
275 | it('Should import a video that will be transcoded', async function () { | 282 | it('Should import a video that will be transcoded', async function () { |
276 | this.timeout(120000) | 283 | this.timeout(120_000) |
277 | 284 | ||
278 | const attributes = { | 285 | const attributes = { |
279 | name: 'transcoded video', | 286 | name: 'transcoded video', |
@@ -295,6 +302,56 @@ Ajouter un sous-titre est vraiment facile`) | |||
295 | } | 302 | } |
296 | }) | 303 | }) |
297 | 304 | ||
305 | it('Should import no HDR version on a HDR video', async function () { | ||
306 | this.timeout(120_000) | ||
307 | |||
308 | const config = { | ||
309 | transcoding: { | ||
310 | enabled: true, | ||
311 | resolutions: { | ||
312 | '240p': false, | ||
313 | '360p': false, | ||
314 | '480p': false, | ||
315 | '720p': false, | ||
316 | '1080p': true, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01 | ||
317 | '1440p': false, | ||
318 | '2160p': false | ||
319 | }, | ||
320 | webtorrent: { enabled: true }, | ||
321 | hls: { enabled: false } | ||
322 | }, | ||
323 | import: { | ||
324 | videos: { | ||
325 | http: { | ||
326 | enabled: true | ||
327 | }, | ||
328 | torrent: { | ||
329 | enabled: true | ||
330 | } | ||
331 | } | ||
332 | } | ||
333 | } | ||
334 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config) | ||
335 | |||
336 | const attributes = { | ||
337 | name: 'hdr video', | ||
338 | targetUrl: getYoutubeHDRVideoUrl(), | ||
339 | channelId: channelIdServer1, | ||
340 | privacy: VideoPrivacy.PUBLIC | ||
341 | } | ||
342 | const res1 = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
343 | const videoUUID = res1.body.video.uuid | ||
344 | |||
345 | await waitJobs(servers) | ||
346 | |||
347 | // test resolution | ||
348 | const res2 = await getVideo(servers[0].url, videoUUID) | ||
349 | const video: VideoDetails = res2.body | ||
350 | expect(video.name).to.equal('hdr video') | ||
351 | const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id })) | ||
352 | expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_1080P) | ||
353 | }) | ||
354 | |||
298 | after(async function () { | 355 | after(async function () { |
299 | await cleanupTests(servers) | 356 | await cleanupTests(servers) |
300 | }) | 357 | }) |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 817d9faf2..32f566506 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -45,7 +45,7 @@ describe('Test video transcoding', function () { | |||
45 | let servers: ServerInfo[] = [] | 45 | let servers: ServerInfo[] = [] |
46 | 46 | ||
47 | before(async function () { | 47 | before(async function () { |
48 | this.timeout(30000) | 48 | this.timeout(30_000) |
49 | 49 | ||
50 | // Run servers | 50 | // Run servers |
51 | servers = await flushAndRunMultipleServers(2) | 51 | servers = await flushAndRunMultipleServers(2) |
@@ -56,7 +56,7 @@ describe('Test video transcoding', function () { | |||
56 | }) | 56 | }) |
57 | 57 | ||
58 | it('Should not transcode video on server 1', async function () { | 58 | it('Should not transcode video on server 1', async function () { |
59 | this.timeout(60000) | 59 | this.timeout(60_000) |
60 | 60 | ||
61 | const videoAttributes = { | 61 | const videoAttributes = { |
62 | name: 'my super name for server 1', | 62 | name: 'my super name for server 1', |
@@ -86,7 +86,7 @@ describe('Test video transcoding', function () { | |||
86 | }) | 86 | }) |
87 | 87 | ||
88 | it('Should transcode video on server 2', async function () { | 88 | it('Should transcode video on server 2', async function () { |
89 | this.timeout(120000) | 89 | this.timeout(120_000) |
90 | 90 | ||
91 | const videoAttributes = { | 91 | const videoAttributes = { |
92 | name: 'my super name for server 2', | 92 | name: 'my super name for server 2', |
@@ -117,7 +117,7 @@ describe('Test video transcoding', function () { | |||
117 | }) | 117 | }) |
118 | 118 | ||
119 | it('Should transcode high bit rate mp3 to proper bit rate', async function () { | 119 | it('Should transcode high bit rate mp3 to proper bit rate', async function () { |
120 | this.timeout(60000) | 120 | this.timeout(60_000) |
121 | 121 | ||
122 | const videoAttributes = { | 122 | const videoAttributes = { |
123 | name: 'mp3_256k', | 123 | name: 'mp3_256k', |
@@ -149,7 +149,7 @@ describe('Test video transcoding', function () { | |||
149 | }) | 149 | }) |
150 | 150 | ||
151 | it('Should transcode video with no audio and have no audio itself', async function () { | 151 | it('Should transcode video with no audio and have no audio itself', async function () { |
152 | this.timeout(60000) | 152 | this.timeout(60_000) |
153 | 153 | ||
154 | const videoAttributes = { | 154 | const videoAttributes = { |
155 | name: 'no_audio', | 155 | name: 'no_audio', |
@@ -174,7 +174,7 @@ describe('Test video transcoding', function () { | |||
174 | }) | 174 | }) |
175 | 175 | ||
176 | it('Should leave the audio untouched, but properly transcode the video', async function () { | 176 | it('Should leave the audio untouched, but properly transcode the video', async function () { |
177 | this.timeout(60000) | 177 | this.timeout(60_000) |
178 | 178 | ||
179 | const videoAttributes = { | 179 | const videoAttributes = { |
180 | name: 'untouched_audio', | 180 | name: 'untouched_audio', |
@@ -209,7 +209,7 @@ describe('Test video transcoding', function () { | |||
209 | }) | 209 | }) |
210 | 210 | ||
211 | it('Should transcode a 60 FPS video', async function () { | 211 | it('Should transcode a 60 FPS video', async function () { |
212 | this.timeout(60000) | 212 | this.timeout(60_000) |
213 | 213 | ||
214 | const videoAttributes = { | 214 | const videoAttributes = { |
215 | name: 'my super 30fps name for server 2', | 215 | name: 'my super 30fps name for server 2', |
@@ -248,7 +248,7 @@ describe('Test video transcoding', function () { | |||
248 | }) | 248 | }) |
249 | 249 | ||
250 | it('Should wait for transcoding before publishing the video', async function () { | 250 | it('Should wait for transcoding before publishing the video', async function () { |
251 | this.timeout(160000) | 251 | this.timeout(160_000) |
252 | 252 | ||
253 | { | 253 | { |
254 | // Upload the video, but wait transcoding | 254 | // Upload the video, but wait transcoding |
@@ -301,7 +301,7 @@ describe('Test video transcoding', function () { | |||
301 | }) | 301 | }) |
302 | 302 | ||
303 | it('Should respect maximum bitrate values', async function () { | 303 | it('Should respect maximum bitrate values', async function () { |
304 | this.timeout(160000) | 304 | this.timeout(160_000) |
305 | 305 | ||
306 | let tempFixturePath: string | 306 | let tempFixturePath: string |
307 | 307 | ||
@@ -341,7 +341,7 @@ describe('Test video transcoding', function () { | |||
341 | }) | 341 | }) |
342 | 342 | ||
343 | it('Should accept and transcode additional extensions', async function () { | 343 | it('Should accept and transcode additional extensions', async function () { |
344 | this.timeout(300000) | 344 | this.timeout(300_000) |
345 | 345 | ||
346 | let tempFixturePath: string | 346 | let tempFixturePath: string |
347 | 347 | ||
@@ -378,14 +378,14 @@ describe('Test video transcoding', function () { | |||
378 | }) | 378 | }) |
379 | 379 | ||
380 | it('Should correctly detect if quick transcode is possible', async function () { | 380 | it('Should correctly detect if quick transcode is possible', async function () { |
381 | this.timeout(10000) | 381 | this.timeout(10_000) |
382 | 382 | ||
383 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true | 383 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true |
384 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false | 384 | expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false |
385 | }) | 385 | }) |
386 | 386 | ||
387 | it('Should merge an audio file with the preview file', async function () { | 387 | it('Should merge an audio file with the preview file', async function () { |
388 | this.timeout(60000) | 388 | this.timeout(60_000) |
389 | 389 | ||
390 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 390 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } |
391 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | 391 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) |
@@ -410,7 +410,7 @@ describe('Test video transcoding', function () { | |||
410 | }) | 410 | }) |
411 | 411 | ||
412 | it('Should upload an audio file and choose a default background image', async function () { | 412 | it('Should upload an audio file and choose a default background image', async function () { |
413 | this.timeout(60000) | 413 | this.timeout(60_000) |
414 | 414 | ||
415 | const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } | 415 | const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } |
416 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | 416 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) |
@@ -435,7 +435,7 @@ describe('Test video transcoding', function () { | |||
435 | }) | 435 | }) |
436 | 436 | ||
437 | it('Should downscale to the closest divisor standard framerate', async function () { | 437 | it('Should downscale to the closest divisor standard framerate', async function () { |
438 | this.timeout(200000) | 438 | this.timeout(200_000) |
439 | 439 | ||
440 | let tempFixturePath: string | 440 | let tempFixturePath: string |
441 | 441 | ||
@@ -476,7 +476,7 @@ describe('Test video transcoding', function () { | |||
476 | }) | 476 | }) |
477 | 477 | ||
478 | it('Should not transcode to an higher bitrate than the original file', async function () { | 478 | it('Should not transcode to an higher bitrate than the original file', async function () { |
479 | this.timeout(160000) | 479 | this.timeout(160_000) |
480 | 480 | ||
481 | const config = { | 481 | const config = { |
482 | transcoding: { | 482 | transcoding: { |
@@ -508,12 +508,12 @@ describe('Test video transcoding', function () { | |||
508 | 508 | ||
509 | const resolutions = [ 240, 360, 480, 720, 1080 ] | 509 | const resolutions = [ 240, 360, 480, 720, 1080 ] |
510 | for (const r of resolutions) { | 510 | for (const r of resolutions) { |
511 | expect(await getServerFileSize(servers[1], `videos/${videoUUID}-${r}.mp4`)).to.be.below(60000) | 511 | expect(await getServerFileSize(servers[1], `videos/${videoUUID}-${r}.mp4`)).to.be.below(60_000) |
512 | } | 512 | } |
513 | }) | 513 | }) |
514 | 514 | ||
515 | it('Should provide valid ffprobe data', async function () { | 515 | it('Should provide valid ffprobe data', async function () { |
516 | this.timeout(160000) | 516 | this.timeout(160_000) |
517 | 517 | ||
518 | const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid | 518 | const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid |
519 | await waitJobs(servers) | 519 | await waitJobs(servers) |
@@ -570,7 +570,7 @@ describe('Test video transcoding', function () { | |||
570 | }) | 570 | }) |
571 | 571 | ||
572 | it('Should transcode a 4k video', async function () { | 572 | it('Should transcode a 4k video', async function () { |
573 | this.timeout(200000) | 573 | this.timeout(200_000) |
574 | 574 | ||
575 | const videoAttributes = { | 575 | const videoAttributes = { |
576 | name: '4k video', | 576 | name: '4k video', |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 5fc5a867c..3a82b3832 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -11,7 +11,7 @@ import * as prompt from 'prompt' | |||
11 | import { accessSync, constants } from 'fs' | 11 | import { accessSync, constants } from 'fs' |
12 | import { remove } from 'fs-extra' | 12 | import { remove } from 'fs-extra' |
13 | import { sha256 } from '../helpers/core-utils' | 13 | import { sha256 } from '../helpers/core-utils' |
14 | import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' | 14 | import { buildOriginallyPublishedAt, getYoutubeDLVideoFormat, safeGetYoutubeDL } from '../helpers/youtube-dl' |
15 | import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli' | 15 | import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli' |
16 | 16 | ||
17 | type UserInfo = { | 17 | type UserInfo = { |
@@ -156,7 +156,7 @@ function processVideo (parameters: { | |||
156 | 156 | ||
157 | log.info('Downloading video "%s"...', videoInfo.title) | 157 | log.info('Downloading video "%s"...', videoInfo.title) |
158 | 158 | ||
159 | const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', ...command.args, '-o', path ] | 159 | const options = [ '-f', getYoutubeDLVideoFormat(), ...command.args, '-o', path ] |
160 | try { | 160 | try { |
161 | const youtubeDL = await safeGetYoutubeDL() | 161 | const youtubeDL = await safeGetYoutubeDL() |
162 | youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { | 162 | youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { |
diff --git a/shared/core-utils/miscs/http-error-codes.ts b/shared/core-utils/miscs/http-error-codes.ts index 9ac8a6c83..b2fbdfc5a 100644 --- a/shared/core-utils/miscs/http-error-codes.ts +++ b/shared/core-utils/miscs/http-error-codes.ts | |||
@@ -196,6 +196,8 @@ export enum HttpStatusCode { | |||
196 | * | 196 | * |
197 | * Indicates that the request could not be processed because of conflict in the request, | 197 | * Indicates that the request could not be processed because of conflict in the request, |
198 | * such as an edit conflict between multiple simultaneous updates. | 198 | * such as an edit conflict between multiple simultaneous updates. |
199 | * | ||
200 | * @see HttpStatusCode.UNPROCESSABLE_ENTITY_422 to denote a disabled feature | ||
199 | */ | 201 | */ |
200 | CONFLICT_409 = 409, | 202 | CONFLICT_409 = 409, |
201 | 203 | ||
@@ -269,6 +271,12 @@ export enum HttpStatusCode { | |||
269 | * Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.3 | 271 | * Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.3 |
270 | * | 272 | * |
271 | * The request was well-formed but was unable to be followed due to semantic errors. | 273 | * The request was well-formed but was unable to be followed due to semantic errors. |
274 | * The server understands the content type of the request entity (hence a 415 (Unsupported Media Type) status code is inappropriate), | ||
275 | * and the syntax of the request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was unable to process | ||
276 | * the contained instructions. For example, this error condition may occur if an JSON request body contains well-formed (i.e., | ||
277 | * syntactically correct), but semantically erroneous, JSON instructions. | ||
278 | * | ||
279 | * Can also be used to denote disabled features (akin to disabled syntax). | ||
272 | * | 280 | * |
273 | * @see HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 if the `Content-Type` was not supported. | 281 | * @see HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 if the `Content-Type` was not supported. |
274 | * @see HttpStatusCode.BAD_REQUEST_400 if the request was not parsable (broken JSON, XML) | 282 | * @see HttpStatusCode.BAD_REQUEST_400 if the request was not parsable (broken JSON, XML) |
diff --git a/shared/extra-utils/videos/video-imports.ts b/shared/extra-utils/videos/video-imports.ts index 52e0075fb..259b8a314 100644 --- a/shared/extra-utils/videos/video-imports.ts +++ b/shared/extra-utils/videos/video-imports.ts | |||
@@ -4,7 +4,28 @@ import { makeGetRequest, makeUploadRequest } from '../requests/requests' | |||
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
5 | 5 | ||
6 | function getYoutubeVideoUrl () { | 6 | function getYoutubeVideoUrl () { |
7 | return 'http://www.youtube.com/watch?v=msX3jv1XdvM' | 7 | return 'https://www.youtube.com/watch?v=msX3jv1XdvM' |
8 | } | ||
9 | |||
10 | function getYoutubeHDRVideoUrl () { | ||
11 | /** | ||
12 | * The video is used to check format-selection correctness wrt. HDR, | ||
13 | * which brings its own set of oddities outside of a MediaSource. | ||
14 | * FIXME: refactor once HDR is supported at playback | ||
15 | * | ||
16 | * The video needs to have the following format_ids: | ||
17 | * (which you can check by using `youtube-dl <url> -F`): | ||
18 | * - 303 (1080p webm vp9) | ||
19 | * - 299 (1080p mp4 avc1) | ||
20 | * - 335 (1080p webm vp9.2 HDR) | ||
21 | * | ||
22 | * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING | ||
23 | * - 400 (1080p mp4 av01) | ||
24 | * - 315 (2160p webm vp9 HDR) | ||
25 | * - 337 (2160p webm vp9.2 HDR) | ||
26 | * - 401 (2160p mp4 av01 HDR) | ||
27 | */ | ||
28 | return 'https://www.youtube.com/watch?v=MSJ25EqI19c' | ||
8 | } | 29 | } |
9 | 30 | ||
10 | function getMagnetURI () { | 31 | function getMagnetURI () { |
@@ -61,6 +82,7 @@ function getMyVideoImports (url: string, token: string, sort?: string) { | |||
61 | export { | 82 | export { |
62 | getBadVideoUrl, | 83 | getBadVideoUrl, |
63 | getYoutubeVideoUrl, | 84 | getYoutubeVideoUrl, |
85 | getYoutubeHDRVideoUrl, | ||
64 | importVideo, | 86 | importVideo, |
65 | getMagnetURI, | 87 | getMagnetURI, |
66 | getMyVideoImports, | 88 | getMyVideoImports, |
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index b0ed860a7..2af2a25a6 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts | |||
@@ -82,6 +82,7 @@ export type VideoImportYoutubeDLPayload = { | |||
82 | generatePreview: boolean | 82 | generatePreview: boolean |
83 | 83 | ||
84 | fileExt?: string | 84 | fileExt?: string |
85 | mergeExt?: string | ||
85 | } | 86 | } |
86 | export type VideoImportTorrentPayload = { | 87 | export type VideoImportTorrentPayload = { |
87 | type: VideoImportTorrentPayloadType | 88 | type: VideoImportTorrentPayloadType |